@khester/create-dynamics-app 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-dynamics-app.js +1 -1
- package/dist/index.js +140 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/consultingHelpers.d.ts +13 -0
- package/dist/utils/consultingHelpers.d.ts.map +1 -0
- package/dist/utils/consultingHelpers.js +569 -0
- package/dist/utils/consultingHelpers.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/initGit.d.ts.map +1 -1
- package/dist/utils/initGit.js.map +1 -1
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +3 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +1 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +11 -1
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +1 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
- package/templates/dynamics-365-starter/README.md +566 -137
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
- package/templates/dynamics-365-starter/deployment/README.md +484 -0
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
- package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
- package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
- package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
- package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
- package/templates/dynamics-365-starter/package.json +22 -1
- package/templates/dynamics-365-starter/public/index.html +8 -11
- package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
- package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
- package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
- package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
- package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
- package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
- package/templates/dynamics-365-starter/src/examples/README.md +52 -0
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
- package/templates/dynamics-365-starter/src/index.tsx +107 -19
- package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
- package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
- package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
- package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
- package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
- package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
- package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
- package/templates/dynamics-365-starter/src/styles/index.css +74 -7
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
- package/templates/dynamics-365-starter/tsconfig.json +11 -8
- package/templates/dynamics-365-starter/webpack.config.js +8 -9
- package/templates/power-pages-starter/README.md +7 -1
- package/templates/power-pages-starter/public/index.html +8 -11
- package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
- package/templates/power-pages-starter/src/index.tsx +3 -3
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
- package/templates/power-pages-starter/tsconfig.json +3 -9
- package/templates/power-pages-starter/webpack.config.js +8 -3
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
2
|
+
import { MockApiService } from './MockApiService';
|
|
3
|
+
import { XrmApiService } from './XrmApiService';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Factory class for creating API service instances based on the current environment.
|
|
7
|
+
* Automatically detects mock vs production environments and returns the appropriate service.
|
|
8
|
+
*/
|
|
9
|
+
export class ServiceFactory {
|
|
10
|
+
/**
|
|
11
|
+
* Determines if the current environment is a mock/development environment.
|
|
12
|
+
* Checks for localhost or 127.0.0.1 hostnames.
|
|
13
|
+
*/
|
|
14
|
+
public static get isMockEnvironment(): boolean {
|
|
15
|
+
const hostname = window.location.hostname;
|
|
16
|
+
return hostname === 'localhost' || hostname === '127.0.0.1';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates an API service instance appropriate for the current environment.
|
|
21
|
+
* @param Xrm The Xrm object from Dynamics 365 (required in production)
|
|
22
|
+
* @returns An IApiService implementation
|
|
23
|
+
* @throws Error if Xrm is not provided in production environment
|
|
24
|
+
*/
|
|
25
|
+
public static createApiService(Xrm?: any): IApiService {
|
|
26
|
+
if (this.isMockEnvironment) {
|
|
27
|
+
console.log(
|
|
28
|
+
'ServiceFactory: Running in mock environment - using MockApiService'
|
|
29
|
+
);
|
|
30
|
+
return new MockApiService();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!Xrm) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'ServiceFactory: Xrm object is required in production environment. ' +
|
|
36
|
+
'Please ensure this code is running within Dynamics 365 context.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
'ServiceFactory: Running in production environment - using XrmApiService'
|
|
42
|
+
);
|
|
43
|
+
return new XrmApiService(Xrm);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gets the current environment type as a string.
|
|
48
|
+
* @returns 'mock' or 'production'
|
|
49
|
+
*/
|
|
50
|
+
public static getEnvironmentType(): 'mock' | 'production' {
|
|
51
|
+
return this.isMockEnvironment ? 'mock' : 'production';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if the code is running in a Dynamics 365 context.
|
|
56
|
+
* @returns True if Xrm global is available
|
|
57
|
+
*/
|
|
58
|
+
public static isDynamics365Context(): boolean {
|
|
59
|
+
return (
|
|
60
|
+
typeof window !== 'undefined' &&
|
|
61
|
+
Object.prototype.hasOwnProperty.call(window, 'Xrm') &&
|
|
62
|
+
(window as any).Xrm !== undefined
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implementation of IApiService that uses Dynamics 365's Xrm.WebApi
|
|
5
|
+
* for direct integration with the platform.
|
|
6
|
+
*/
|
|
7
|
+
export class XrmApiService implements IApiService {
|
|
8
|
+
private xrm: any;
|
|
9
|
+
|
|
10
|
+
constructor(xrm: any) {
|
|
11
|
+
if (!xrm || !xrm.WebApi) {
|
|
12
|
+
throw new Error('Valid Xrm object with WebApi is required');
|
|
13
|
+
}
|
|
14
|
+
this.xrm = xrm;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async createRecord(entityName: string, record: any): Promise<any> {
|
|
18
|
+
try {
|
|
19
|
+
console.log(`XrmApiService: Creating ${entityName} record`, record);
|
|
20
|
+
|
|
21
|
+
// Remove any system fields that shouldn't be in create payload
|
|
22
|
+
const createPayload = { ...record };
|
|
23
|
+
delete createPayload.createdon;
|
|
24
|
+
delete createPayload.modifiedon;
|
|
25
|
+
delete createPayload.createdby;
|
|
26
|
+
delete createPayload.modifiedby;
|
|
27
|
+
|
|
28
|
+
const result = await this.xrm.WebApi.createRecord(
|
|
29
|
+
entityName,
|
|
30
|
+
createPayload
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Xrm.WebApi.createRecord returns an object with id property
|
|
34
|
+
// We need to fetch the full record to return it
|
|
35
|
+
const createdRecord = await this.xrm.WebApi.retrieveRecord(
|
|
36
|
+
entityName,
|
|
37
|
+
result.id
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return createdRecord;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`XrmApiService: Error creating ${entityName}`, error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async updateRecord(
|
|
48
|
+
entityName: string,
|
|
49
|
+
id: string,
|
|
50
|
+
record: any
|
|
51
|
+
): Promise<any> {
|
|
52
|
+
try {
|
|
53
|
+
console.log(`XrmApiService: Updating ${entityName} record ${id}`, record);
|
|
54
|
+
|
|
55
|
+
// Remove any system fields and primary key from update payload
|
|
56
|
+
const updatePayload = { ...record };
|
|
57
|
+
const primaryKey = this.getPrimaryKeyName(entityName);
|
|
58
|
+
delete updatePayload[primaryKey];
|
|
59
|
+
delete updatePayload.createdon;
|
|
60
|
+
delete updatePayload.modifiedon;
|
|
61
|
+
delete updatePayload.createdby;
|
|
62
|
+
delete updatePayload.modifiedby;
|
|
63
|
+
|
|
64
|
+
await this.xrm.WebApi.updateRecord(entityName, id, updatePayload);
|
|
65
|
+
|
|
66
|
+
// Fetch and return the updated record
|
|
67
|
+
const updatedRecord = await this.xrm.WebApi.retrieveRecord(
|
|
68
|
+
entityName,
|
|
69
|
+
id
|
|
70
|
+
);
|
|
71
|
+
return updatedRecord;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`XrmApiService: Error updating ${entityName}`, error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async deleteRecord(entityName: string, id: string): Promise<void> {
|
|
79
|
+
try {
|
|
80
|
+
console.log(`XrmApiService: Deleting ${entityName} record ${id}`);
|
|
81
|
+
await this.xrm.WebApi.deleteRecord(entityName, id);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`XrmApiService: Error deleting ${entityName}`, error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async retrieveMultipleRecords(
|
|
89
|
+
entityName: string,
|
|
90
|
+
fetchXml: string
|
|
91
|
+
): Promise<{ entities: any[] }> {
|
|
92
|
+
try {
|
|
93
|
+
console.log(
|
|
94
|
+
`XrmApiService: Retrieving ${entityName} records with FetchXML`,
|
|
95
|
+
fetchXml
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Convert FetchXML to OData query if needed
|
|
99
|
+
// For now, we'll use Xrm.WebApi.retrieveMultipleRecords with options
|
|
100
|
+
const options = `?fetchXml=${encodeURIComponent(fetchXml)}`;
|
|
101
|
+
|
|
102
|
+
const result = await this.xrm.WebApi.retrieveMultipleRecords(
|
|
103
|
+
entityName,
|
|
104
|
+
options
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
entities: result.entities || [],
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(
|
|
112
|
+
`XrmApiService: Error retrieving ${entityName} records`,
|
|
113
|
+
error
|
|
114
|
+
);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async executeRequest(requestName: string, requestData: any): Promise<any> {
|
|
120
|
+
try {
|
|
121
|
+
console.log(
|
|
122
|
+
`XrmApiService: Executing request ${requestName}`,
|
|
123
|
+
requestData
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Xrm.WebApi.execute expects a request object with specific structure
|
|
127
|
+
const request = {
|
|
128
|
+
...requestData,
|
|
129
|
+
getMetadata: () => ({
|
|
130
|
+
boundParameter: null,
|
|
131
|
+
operationType: 0, // Action
|
|
132
|
+
operationName: requestName,
|
|
133
|
+
}),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const response = await this.xrm.WebApi.execute(request);
|
|
137
|
+
|
|
138
|
+
// Parse the response
|
|
139
|
+
if (response.ok) {
|
|
140
|
+
const responseData = await response.json();
|
|
141
|
+
return responseData;
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error(`Request failed with status: ${response.status}`);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(
|
|
147
|
+
`XrmApiService: Error executing request ${requestName}`,
|
|
148
|
+
error
|
|
149
|
+
);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async uploadFile(file: File): Promise<string> {
|
|
155
|
+
try {
|
|
156
|
+
console.log(`XrmApiService: Uploading file ${file.name}`);
|
|
157
|
+
|
|
158
|
+
// File upload in Dynamics 365 typically involves:
|
|
159
|
+
// 1. Creating an annotation (note) record
|
|
160
|
+
// 2. Attaching the file content as base64
|
|
161
|
+
|
|
162
|
+
// Convert file to base64
|
|
163
|
+
const base64Content = await this.fileToBase64(file);
|
|
164
|
+
|
|
165
|
+
// Create annotation record
|
|
166
|
+
const annotation = {
|
|
167
|
+
subject: file.name,
|
|
168
|
+
filename: file.name,
|
|
169
|
+
mimetype: file.type,
|
|
170
|
+
documentbody: base64Content,
|
|
171
|
+
isdocument: true,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = await this.xrm.WebApi.createRecord(
|
|
175
|
+
'annotation',
|
|
176
|
+
annotation
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Return the annotation ID as the file reference
|
|
180
|
+
return result.id;
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error(`XrmApiService: Error uploading file`, error);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Helper method to convert File to base64 string
|
|
189
|
+
*/
|
|
190
|
+
private fileToBase64(file: File): Promise<string> {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const reader = new FileReader();
|
|
193
|
+
reader.readAsDataURL(file);
|
|
194
|
+
reader.onload = () => {
|
|
195
|
+
// Remove data URL prefix to get just the base64 content
|
|
196
|
+
const base64 = (reader.result as string).split(',')[1];
|
|
197
|
+
resolve(base64);
|
|
198
|
+
};
|
|
199
|
+
reader.onerror = (error) => reject(error);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Helper method to get the primary key name for an entity
|
|
205
|
+
*/
|
|
206
|
+
private getPrimaryKeyName(entityName: string): string {
|
|
207
|
+
// Remove trailing 's' if present and add 'id'
|
|
208
|
+
const singularName = entityName.endsWith('s')
|
|
209
|
+
? entityName.slice(0, -1)
|
|
210
|
+
: entityName;
|
|
211
|
+
return `${singularName}id`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -28,18 +28,57 @@ body {
|
|
|
28
28
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
.app-
|
|
31
|
+
.app-header__content {
|
|
32
|
+
display: flex;
|
|
33
|
+
justify-content: space-between;
|
|
34
|
+
align-items: center;
|
|
35
|
+
max-width: 1200px;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.app-header__title h1 {
|
|
32
40
|
margin: 0 0 4px 0;
|
|
33
41
|
font-size: 24px;
|
|
34
42
|
font-weight: 600;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
.app-
|
|
45
|
+
.app-header__title p {
|
|
38
46
|
margin: 0;
|
|
39
47
|
font-size: 14px;
|
|
40
48
|
opacity: 0.9;
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
.app-header__actions {
|
|
52
|
+
display: flex;
|
|
53
|
+
gap: 12px;
|
|
54
|
+
align-items: center;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.debug-toggle-btn {
|
|
58
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
59
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
60
|
+
color: white;
|
|
61
|
+
padding: 6px 12px;
|
|
62
|
+
border-radius: 4px;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
transition: background-color 0.2s ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.debug-toggle-btn:hover {
|
|
69
|
+
background-color: rgba(255, 255, 255, 0.2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.app-navigation {
|
|
73
|
+
background-color: white;
|
|
74
|
+
border-bottom: 1px solid #edebe9;
|
|
75
|
+
max-width: 1200px;
|
|
76
|
+
margin: 0 auto;
|
|
77
|
+
margin-top: 16px;
|
|
78
|
+
border-radius: 8px 8px 0 0;
|
|
79
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
80
|
+
}
|
|
81
|
+
|
|
43
82
|
.app-main {
|
|
44
83
|
flex: 1;
|
|
45
84
|
padding: 0;
|
|
@@ -47,12 +86,25 @@ body {
|
|
|
47
86
|
margin: 0 auto;
|
|
48
87
|
width: 100%;
|
|
49
88
|
background-color: white;
|
|
50
|
-
margin-top: 16px;
|
|
51
89
|
margin-bottom: 16px;
|
|
52
|
-
border-radius: 8px;
|
|
90
|
+
border-radius: 0 0 8px 8px;
|
|
53
91
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
54
92
|
}
|
|
55
93
|
|
|
94
|
+
.debug-panel-overlay {
|
|
95
|
+
position: fixed;
|
|
96
|
+
top: 0;
|
|
97
|
+
left: 0;
|
|
98
|
+
right: 0;
|
|
99
|
+
bottom: 0;
|
|
100
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
101
|
+
z-index: 1000;
|
|
102
|
+
display: flex;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
align-items: center;
|
|
105
|
+
padding: 20px;
|
|
106
|
+
}
|
|
107
|
+
|
|
56
108
|
.app-footer {
|
|
57
109
|
background-color: #f3f2f1;
|
|
58
110
|
border-top: 1px solid #edebe9;
|
|
@@ -93,12 +145,27 @@ body {
|
|
|
93
145
|
padding: 12px 16px;
|
|
94
146
|
}
|
|
95
147
|
|
|
96
|
-
.app-
|
|
148
|
+
.app-header__content {
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 12px;
|
|
151
|
+
align-items: flex-start;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.app-header__title h1 {
|
|
97
155
|
font-size: 20px;
|
|
98
156
|
}
|
|
99
157
|
|
|
100
|
-
.app-
|
|
158
|
+
.app-navigation {
|
|
101
159
|
margin: 8px;
|
|
102
|
-
border-radius: 4px;
|
|
160
|
+
border-radius: 4px 4px 0 0;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.app-main {
|
|
164
|
+
margin: 0 8px 8px 8px;
|
|
165
|
+
border-radius: 0 0 4px 4px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.debug-panel-overlay {
|
|
169
|
+
padding: 10px;
|
|
103
170
|
}
|
|
104
171
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { program } = require('commander');
|
|
6
|
+
|
|
7
|
+
// Entity Generator CLI Tool for Dynamics 365
|
|
8
|
+
// Generates complete entity stack from templates
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('entity-generator')
|
|
12
|
+
.description('Generate complete entity stack for Dynamics 365')
|
|
13
|
+
.version('1.0.0');
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command('generate')
|
|
17
|
+
.description('Generate entity files')
|
|
18
|
+
.requiredOption(
|
|
19
|
+
'-e, --entity <name>',
|
|
20
|
+
'Entity logical name (e.g., customentity)'
|
|
21
|
+
)
|
|
22
|
+
.requiredOption(
|
|
23
|
+
'-d, --display-name <name>',
|
|
24
|
+
'Entity display name (e.g., "Custom Entity")'
|
|
25
|
+
)
|
|
26
|
+
.option(
|
|
27
|
+
'-p, --plural <name>',
|
|
28
|
+
'Entity plural name (defaults to entity + "s")'
|
|
29
|
+
)
|
|
30
|
+
.option('--output <path>', 'Output directory (defaults to src/)')
|
|
31
|
+
.action(generateEntity);
|
|
32
|
+
|
|
33
|
+
async function generateEntity(options) {
|
|
34
|
+
const { entity, displayName, plural, output } = options;
|
|
35
|
+
|
|
36
|
+
console.log(`🚀 Generating entity stack for: ${displayName} (${entity})`);
|
|
37
|
+
|
|
38
|
+
const config = {
|
|
39
|
+
entityName: entity.toLowerCase(),
|
|
40
|
+
entityNamePascal: toPascalCase(entity),
|
|
41
|
+
displayName,
|
|
42
|
+
pluralName: plural || entity.toLowerCase() + 's',
|
|
43
|
+
pluralNamePascal: toPascalCase(plural || entity + 's'),
|
|
44
|
+
outputDir: output || 'src',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await generateEntityFiles(config);
|
|
49
|
+
console.log('✅ Entity stack generated successfully!');
|
|
50
|
+
console.log('\n📁 Generated files:');
|
|
51
|
+
console.log(` • src/models/${config.entityNamePascal}.ts`);
|
|
52
|
+
console.log(` • src/constants/${config.entityName}.ts`);
|
|
53
|
+
console.log(` • src/components/${config.entityNamePascal}Management.tsx`);
|
|
54
|
+
console.log(` • src/components/${config.entityNamePascal}Management.css`);
|
|
55
|
+
console.log(` • src/components/${config.entityNamePascal}Form.tsx`);
|
|
56
|
+
console.log(` • src/components/${config.entityNamePascal}Form.css`);
|
|
57
|
+
console.log('\n🎯 Next steps:');
|
|
58
|
+
console.log(' 1. Update your D365 field constants in the constants file');
|
|
59
|
+
console.log(' 2. Add the new components to your main application');
|
|
60
|
+
console.log(' 3. Run npm run build to compile');
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('❌ Error generating entity:', error.message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function generateEntityFiles(config) {
|
|
68
|
+
const templatesDir = path.join(__dirname, 'templates');
|
|
69
|
+
const projectRoot = findProjectRoot();
|
|
70
|
+
const outputDir = path.join(projectRoot, config.outputDir);
|
|
71
|
+
|
|
72
|
+
// Ensure output directories exist
|
|
73
|
+
ensureDirectoryExists(path.join(outputDir, 'models'));
|
|
74
|
+
ensureDirectoryExists(path.join(outputDir, 'constants'));
|
|
75
|
+
ensureDirectoryExists(path.join(outputDir, 'components'));
|
|
76
|
+
|
|
77
|
+
// Generate model file
|
|
78
|
+
await generateFromTemplate(
|
|
79
|
+
path.join(templatesDir, 'model.template.ts'),
|
|
80
|
+
path.join(outputDir, 'models', `${config.entityNamePascal}.ts`),
|
|
81
|
+
config
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Generate constants file
|
|
85
|
+
await generateFromTemplate(
|
|
86
|
+
path.join(templatesDir, 'constants.template.ts'),
|
|
87
|
+
path.join(outputDir, 'constants', `${config.entityName}.ts`),
|
|
88
|
+
config
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Generate management component
|
|
92
|
+
await generateFromTemplate(
|
|
93
|
+
path.join(templatesDir, 'management.template.tsx'),
|
|
94
|
+
path.join(
|
|
95
|
+
outputDir,
|
|
96
|
+
'components',
|
|
97
|
+
`${config.entityNamePascal}Management.tsx`
|
|
98
|
+
),
|
|
99
|
+
config
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Generate management CSS
|
|
103
|
+
await generateFromTemplate(
|
|
104
|
+
path.join(templatesDir, 'management.template.css'),
|
|
105
|
+
path.join(
|
|
106
|
+
outputDir,
|
|
107
|
+
'components',
|
|
108
|
+
`${config.entityNamePascal}Management.css`
|
|
109
|
+
),
|
|
110
|
+
config
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Generate form component
|
|
114
|
+
await generateFromTemplate(
|
|
115
|
+
path.join(templatesDir, 'form.template.tsx'),
|
|
116
|
+
path.join(outputDir, 'components', `${config.entityNamePascal}Form.tsx`),
|
|
117
|
+
config
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Generate form CSS
|
|
121
|
+
await generateFromTemplate(
|
|
122
|
+
path.join(templatesDir, 'form.template.css'),
|
|
123
|
+
path.join(outputDir, 'components', `${config.entityNamePascal}Form.css`),
|
|
124
|
+
config
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function generateFromTemplate(templatePath, outputPath, config) {
|
|
129
|
+
const template = fs.readFileSync(templatePath, 'utf8');
|
|
130
|
+
const generated = replaceTemplateVariables(template, config);
|
|
131
|
+
fs.writeFileSync(outputPath, generated);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function replaceTemplateVariables(template, config) {
|
|
135
|
+
return template
|
|
136
|
+
.replace(/\{\{entityName\}\}/g, config.entityName)
|
|
137
|
+
.replace(/\{\{entityNamePascal\}\}/g, config.entityNamePascal)
|
|
138
|
+
.replace(/\{\{displayName\}\}/g, config.displayName)
|
|
139
|
+
.replace(/\{\{pluralName\}\}/g, config.pluralName)
|
|
140
|
+
.replace(/\{\{pluralNamePascal\}\}/g, config.pluralNamePascal);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toPascalCase(str) {
|
|
144
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureDirectoryExists(dirPath) {
|
|
148
|
+
if (!fs.existsSync(dirPath)) {
|
|
149
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findProjectRoot() {
|
|
154
|
+
let dir = process.cwd();
|
|
155
|
+
while (dir !== '/') {
|
|
156
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
|
157
|
+
return dir;
|
|
158
|
+
}
|
|
159
|
+
dir = path.dirname(dir);
|
|
160
|
+
}
|
|
161
|
+
return process.cwd();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (require.main === module) {
|
|
165
|
+
program.parse();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { generateEntity };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{displayName}} Entity Constants
|
|
3
|
+
* Generated by Entity Generator
|
|
4
|
+
*
|
|
5
|
+
* Update these constants with actual field names from your Dynamics 365 entity
|
|
6
|
+
*/
|
|
7
|
+
// Replace 'SampleEntity' with your actual entity PascalCase name during template processing
|
|
8
|
+
export class SampleEntityConstants {
|
|
9
|
+
/** Entity logical name */
|
|
10
|
+
public static readonly EntityLogicalName: string = "{{entityName}}";
|
|
11
|
+
|
|
12
|
+
/** Entity collection name for Web API */
|
|
13
|
+
public static readonly EntityCollectionName: string = "{{pluralName}}";
|
|
14
|
+
|
|
15
|
+
/** Primary ID attribute */
|
|
16
|
+
public static readonly PrimaryIdAttribute: string = "{{entityName}}id";
|
|
17
|
+
|
|
18
|
+
/** Primary name attribute */
|
|
19
|
+
public static readonly PrimaryNameAttribute: string = "{{entityName}}_name";
|
|
20
|
+
|
|
21
|
+
// System fields
|
|
22
|
+
/** Type: DateTime, ReadOnly: true */
|
|
23
|
+
public static readonly CreatedOn: string = "createdon";
|
|
24
|
+
|
|
25
|
+
/** Type: DateTime, ReadOnly: true */
|
|
26
|
+
public static readonly ModifiedOn: string = "modifiedon";
|
|
27
|
+
|
|
28
|
+
/** Type: Lookup, ReadOnly: true */
|
|
29
|
+
public static readonly CreatedBy: string = "createdby";
|
|
30
|
+
|
|
31
|
+
/** Type: Lookup, ReadOnly: true */
|
|
32
|
+
public static readonly ModifiedBy: string = "modifiedby";
|
|
33
|
+
|
|
34
|
+
/** Type: Lookup, ReadOnly: true */
|
|
35
|
+
public static readonly OwnerId: string = "ownerid";
|
|
36
|
+
|
|
37
|
+
/** Type: Picklist, ReadOnly: true */
|
|
38
|
+
public static readonly StateCode: string = "statecode";
|
|
39
|
+
|
|
40
|
+
/** Type: Picklist, ReadOnly: true */
|
|
41
|
+
public static readonly StatusCode: string = "statuscode";
|
|
42
|
+
|
|
43
|
+
// Custom fields - Update these with your actual field names
|
|
44
|
+
/** Type: String, RequiredLevel: ApplicationRequired, MaxLength: 100 */
|
|
45
|
+
public static readonly Description: string = "{{entityName}}_description";
|
|
46
|
+
|
|
47
|
+
/** Type: DateTime, RequiredLevel: None */
|
|
48
|
+
public static readonly DueDate: string = "{{entityName}}_duedate";
|
|
49
|
+
|
|
50
|
+
/** Type: Picklist, RequiredLevel: None */
|
|
51
|
+
public static readonly Priority: string = "{{entityName}}_priority";
|
|
52
|
+
|
|
53
|
+
/** Type: Lookup, RequiredLevel: None, Target: contact */
|
|
54
|
+
public static readonly ContactId: string = "{{entityName}}_contactid";
|
|
55
|
+
|
|
56
|
+
/** Type: Lookup, RequiredLevel: None, Target: account */
|
|
57
|
+
public static readonly AccountId: string = "{{entityName}}_accountid";
|
|
58
|
+
|
|
59
|
+
/** Type: Money, RequiredLevel: None */
|
|
60
|
+
public static readonly Amount: string = "{{entityName}}_amount";
|
|
61
|
+
|
|
62
|
+
/** Type: Boolean, RequiredLevel: None */
|
|
63
|
+
public static readonly IsActive: string = "{{entityName}}_isactive";
|
|
64
|
+
|
|
65
|
+
// Option Set Values - Update these with your actual option set values
|
|
66
|
+
public static readonly PriorityOptions = {
|
|
67
|
+
Low: 1,
|
|
68
|
+
Medium: 2,
|
|
69
|
+
High: 3,
|
|
70
|
+
Critical: 4
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
public static readonly StateCodeOptions = {
|
|
74
|
+
Active: 0,
|
|
75
|
+
Inactive: 1
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
public static readonly StatusCodeOptions = {
|
|
79
|
+
Active: 1,
|
|
80
|
+
Inactive: 2
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Field display names for UI
|
|
84
|
+
public static readonly FieldDisplayNames = {
|
|
85
|
+
[this.PrimaryNameAttribute]: "Name",
|
|
86
|
+
[this.Description]: "Description",
|
|
87
|
+
[this.DueDate]: "Due Date",
|
|
88
|
+
[this.Priority]: "Priority",
|
|
89
|
+
[this.ContactId]: "Contact",
|
|
90
|
+
[this.AccountId]: "Account",
|
|
91
|
+
[this.Amount]: "Amount",
|
|
92
|
+
[this.IsActive]: "Is Active",
|
|
93
|
+
[this.CreatedOn]: "Created On",
|
|
94
|
+
[this.ModifiedOn]: "Modified On",
|
|
95
|
+
[this.CreatedBy]: "Created By",
|
|
96
|
+
[this.ModifiedBy]: "Modified By"
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Required fields
|
|
100
|
+
public static readonly RequiredFields = [
|
|
101
|
+
this.PrimaryNameAttribute
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Fields for list view
|
|
105
|
+
public static readonly ListViewColumns = [
|
|
106
|
+
this.PrimaryNameAttribute,
|
|
107
|
+
this.Description,
|
|
108
|
+
this.Priority,
|
|
109
|
+
this.DueDate,
|
|
110
|
+
this.CreatedOn
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// Fields for form view
|
|
114
|
+
public static readonly FormFields = [
|
|
115
|
+
this.PrimaryNameAttribute,
|
|
116
|
+
this.Description,
|
|
117
|
+
this.Priority,
|
|
118
|
+
this.DueDate,
|
|
119
|
+
this.ContactId,
|
|
120
|
+
this.AccountId,
|
|
121
|
+
this.Amount,
|
|
122
|
+
this.IsActive
|
|
123
|
+
];
|
|
124
|
+
}
|