@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
|
@@ -1,114 +1,329 @@
|
|
|
1
1
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
3
|
+
import { ServiceFactory } from '../services/ServiceFactory';
|
|
4
|
+
import { Logger } from '../components/Logging/logger';
|
|
3
5
|
|
|
4
6
|
interface DynamicsContextType {
|
|
5
|
-
apiService:
|
|
7
|
+
apiService: IApiService | null;
|
|
6
8
|
createRecord: (entityName: string, data: any) => Promise<any>;
|
|
7
|
-
retrieveRecord: (
|
|
9
|
+
retrieveRecord: (
|
|
10
|
+
entityName: string,
|
|
11
|
+
id: string,
|
|
12
|
+
select?: string
|
|
13
|
+
) => Promise<any>;
|
|
8
14
|
updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
|
|
9
15
|
deleteRecord: (entityName: string, id: string) => Promise<void>;
|
|
10
16
|
retrieveMultiple: (entityName: string, query?: string) => Promise<any>;
|
|
17
|
+
isEnvironmentMock: boolean;
|
|
18
|
+
environmentType: 'mock' | 'production';
|
|
11
19
|
}
|
|
12
20
|
|
|
13
|
-
const DynamicsContext = createContext<DynamicsContextType | undefined>(
|
|
21
|
+
const DynamicsContext = createContext<DynamicsContextType | undefined>(
|
|
22
|
+
undefined
|
|
23
|
+
);
|
|
14
24
|
|
|
15
25
|
interface DynamicsProviderProps {
|
|
16
26
|
children: React.ReactNode;
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
/** Optional Xrm object for Dynamics 365 environment (will be auto-detected if not provided) */
|
|
28
|
+
xrm?: any;
|
|
29
|
+
/** Optional custom API service for PCF or other integrations */
|
|
30
|
+
customApiService?: IApiService;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
|
|
22
|
-
children,
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
|
|
34
|
+
children,
|
|
35
|
+
xrm,
|
|
36
|
+
customApiService,
|
|
25
37
|
}) => {
|
|
26
|
-
const [apiService, setApiService] = useState<
|
|
38
|
+
const [apiService, setApiService] = useState<IApiService | null>(null);
|
|
39
|
+
const [environmentType, setEnvironmentType] = useState<'mock' | 'production'>(
|
|
40
|
+
'mock'
|
|
41
|
+
);
|
|
27
42
|
|
|
28
43
|
useEffect(() => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
try {
|
|
45
|
+
Logger.log(
|
|
46
|
+
'DynamicsProvider: Initializing API service',
|
|
47
|
+
'DynamicsProvider'
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Use custom API service if provided (for PCF integration)
|
|
51
|
+
if (customApiService) {
|
|
52
|
+
Logger.log(
|
|
53
|
+
'Using custom API service provided via props',
|
|
54
|
+
'DynamicsProvider'
|
|
55
|
+
);
|
|
56
|
+
setApiService(customApiService);
|
|
57
|
+
setEnvironmentType('production'); // Assume production for custom services
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detect Xrm object if not provided
|
|
62
|
+
const xrmObject =
|
|
63
|
+
xrm || (typeof window !== 'undefined' && (window as any).Xrm);
|
|
64
|
+
|
|
65
|
+
// Use ServiceFactory to create the appropriate service
|
|
66
|
+
const service = ServiceFactory.createApiService(xrmObject);
|
|
67
|
+
const envType = ServiceFactory.getEnvironmentType();
|
|
68
|
+
|
|
69
|
+
setApiService(service);
|
|
70
|
+
setEnvironmentType(envType);
|
|
71
|
+
|
|
72
|
+
Logger.log(
|
|
73
|
+
`DynamicsProvider: Initialized with ${envType} environment`,
|
|
74
|
+
'DynamicsProvider'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (envType === 'production' && !ServiceFactory.isDynamics365Context()) {
|
|
78
|
+
Logger.warn(
|
|
79
|
+
'Running in production mode but Xrm object may not be available',
|
|
80
|
+
'DynamicsProvider'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
Logger.error(
|
|
85
|
+
'Failed to initialize API service',
|
|
86
|
+
'DynamicsProvider',
|
|
87
|
+
error
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Fallback to mock service for development
|
|
91
|
+
try {
|
|
92
|
+
const mockService = ServiceFactory.createApiService();
|
|
93
|
+
setApiService(mockService);
|
|
94
|
+
setEnvironmentType('mock');
|
|
95
|
+
Logger.warn(
|
|
96
|
+
'Falling back to mock service due to initialization error',
|
|
97
|
+
'DynamicsProvider'
|
|
98
|
+
);
|
|
99
|
+
} catch (fallbackError) {
|
|
100
|
+
Logger.error(
|
|
101
|
+
'Failed to initialize fallback mock service',
|
|
102
|
+
'DynamicsProvider',
|
|
103
|
+
fallbackError
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, [xrm, customApiService]);
|
|
42
108
|
|
|
43
109
|
const createRecord = async (entityName: string, data: any) => {
|
|
44
|
-
if (!apiService)
|
|
45
|
-
|
|
110
|
+
if (!apiService) {
|
|
111
|
+
const error = 'API service not initialized';
|
|
112
|
+
Logger.error(error, 'DynamicsProvider.createRecord');
|
|
113
|
+
throw new Error(error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
Logger.apiOperation(
|
|
118
|
+
'CREATE',
|
|
119
|
+
entityName,
|
|
120
|
+
data,
|
|
121
|
+
'DynamicsProvider.createRecord'
|
|
122
|
+
);
|
|
123
|
+
const result = await apiService.createRecord(entityName, data);
|
|
124
|
+
Logger.log(
|
|
125
|
+
`Successfully created ${entityName} record`,
|
|
126
|
+
'DynamicsProvider.createRecord'
|
|
127
|
+
);
|
|
128
|
+
return result;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
Logger.error(
|
|
131
|
+
`Failed to create ${entityName} record`,
|
|
132
|
+
'DynamicsProvider.createRecord',
|
|
133
|
+
error
|
|
134
|
+
);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
46
137
|
};
|
|
47
138
|
|
|
48
|
-
const retrieveRecord = async (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
139
|
+
const retrieveRecord = async (
|
|
140
|
+
entityName: string,
|
|
141
|
+
id: string,
|
|
142
|
+
select?: string
|
|
143
|
+
) => {
|
|
144
|
+
if (!apiService) {
|
|
145
|
+
const error = 'API service not initialized';
|
|
146
|
+
Logger.error(error, 'DynamicsProvider.retrieveRecord');
|
|
147
|
+
throw new Error(error);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
Logger.apiOperation(
|
|
152
|
+
'READ',
|
|
153
|
+
entityName,
|
|
154
|
+
{ id, select },
|
|
155
|
+
'DynamicsProvider.retrieveRecord'
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Use retrieveMultipleRecords with FetchXML to get a single record
|
|
159
|
+
const selectAttributes = select
|
|
160
|
+
? select.split(',').map((attr) => attr.trim())
|
|
161
|
+
: ['*'];
|
|
162
|
+
const attributes = selectAttributes
|
|
163
|
+
.map((attr) => (attr === '*' ? '' : `<attribute name="${attr}" />`))
|
|
164
|
+
.join('');
|
|
165
|
+
const fetchXml = `
|
|
166
|
+
<fetch top="1">
|
|
167
|
+
<entity name="${entityName}">
|
|
168
|
+
${attributes}
|
|
169
|
+
<filter>
|
|
170
|
+
<condition attribute="${entityName}id" operator="eq" value="${id}" />
|
|
171
|
+
</filter>
|
|
172
|
+
</entity>
|
|
173
|
+
</fetch>
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
Logger.fetchXml(fetchXml, 'DynamicsProvider.retrieveRecord');
|
|
177
|
+
const result = await apiService.retrieveMultipleRecords(
|
|
178
|
+
entityName,
|
|
179
|
+
fetchXml
|
|
180
|
+
);
|
|
181
|
+
const record = result.entities.length > 0 ? result.entities[0] : null;
|
|
182
|
+
|
|
183
|
+
Logger.log(
|
|
184
|
+
`Retrieved ${entityName} record: ${record ? 'found' : 'not found'}`,
|
|
185
|
+
'DynamicsProvider.retrieveRecord'
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
return record;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
Logger.error(
|
|
191
|
+
`Failed to retrieve ${entityName} record`,
|
|
192
|
+
'DynamicsProvider.retrieveRecord',
|
|
193
|
+
error
|
|
194
|
+
);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
65
197
|
};
|
|
66
198
|
|
|
67
199
|
const updateRecord = async (entityName: string, id: string, data: any) => {
|
|
68
|
-
if (!apiService)
|
|
69
|
-
|
|
200
|
+
if (!apiService) {
|
|
201
|
+
const error = 'API service not initialized';
|
|
202
|
+
Logger.error(error, 'DynamicsProvider.updateRecord');
|
|
203
|
+
throw new Error(error);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
Logger.apiOperation(
|
|
208
|
+
'UPDATE',
|
|
209
|
+
entityName,
|
|
210
|
+
{ id, data },
|
|
211
|
+
'DynamicsProvider.updateRecord'
|
|
212
|
+
);
|
|
213
|
+
const result = await apiService.updateRecord(entityName, id, data);
|
|
214
|
+
Logger.log(
|
|
215
|
+
`Successfully updated ${entityName} record`,
|
|
216
|
+
'DynamicsProvider.updateRecord'
|
|
217
|
+
);
|
|
218
|
+
return result;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
Logger.error(
|
|
221
|
+
`Failed to update ${entityName} record`,
|
|
222
|
+
'DynamicsProvider.updateRecord',
|
|
223
|
+
error
|
|
224
|
+
);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
70
227
|
};
|
|
71
228
|
|
|
72
229
|
const deleteRecord = async (entityName: string, id: string) => {
|
|
73
|
-
if (!apiService)
|
|
74
|
-
|
|
230
|
+
if (!apiService) {
|
|
231
|
+
const error = 'API service not initialized';
|
|
232
|
+
Logger.error(error, 'DynamicsProvider.deleteRecord');
|
|
233
|
+
throw new Error(error);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
Logger.apiOperation(
|
|
238
|
+
'DELETE',
|
|
239
|
+
entityName,
|
|
240
|
+
{ id },
|
|
241
|
+
'DynamicsProvider.deleteRecord'
|
|
242
|
+
);
|
|
243
|
+
await apiService.deleteRecord(entityName, id);
|
|
244
|
+
Logger.log(
|
|
245
|
+
`Successfully deleted ${entityName} record`,
|
|
246
|
+
'DynamicsProvider.deleteRecord'
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
Logger.error(
|
|
250
|
+
`Failed to delete ${entityName} record`,
|
|
251
|
+
'DynamicsProvider.deleteRecord',
|
|
252
|
+
error
|
|
253
|
+
);
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
75
256
|
};
|
|
76
257
|
|
|
77
258
|
const retrieveMultiple = async (entityName: string, query?: string) => {
|
|
78
|
-
if (!apiService)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
259
|
+
if (!apiService) {
|
|
260
|
+
const error = 'API service not initialized';
|
|
261
|
+
Logger.error(error, 'DynamicsProvider.retrieveMultiple');
|
|
262
|
+
throw new Error(error);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
Logger.apiOperation(
|
|
267
|
+
'QUERY',
|
|
268
|
+
entityName,
|
|
269
|
+
query,
|
|
270
|
+
'DynamicsProvider.retrieveMultiple'
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
// Convert OData-style query to FetchXML
|
|
274
|
+
let fetchXml = `<fetch>`;
|
|
275
|
+
|
|
276
|
+
if (query) {
|
|
277
|
+
// Parse basic OData query parameters
|
|
278
|
+
const selectMatch = query.match(/\$select=([^&]*)/i);
|
|
279
|
+
const orderByMatch = query.match(/\$orderby=([^&]*)/i);
|
|
280
|
+
const topMatch = query.match(/\$top=(\d+)/i);
|
|
281
|
+
|
|
282
|
+
if (topMatch) {
|
|
283
|
+
fetchXml = `<fetch top="${topMatch[1]}">`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fetchXml += `<entity name="${entityName}">`;
|
|
287
|
+
|
|
288
|
+
if (selectMatch) {
|
|
289
|
+
const attributes = selectMatch[1]
|
|
290
|
+
.split(',')
|
|
291
|
+
.map((attr) => attr.trim());
|
|
292
|
+
attributes.forEach((attr) => {
|
|
293
|
+
fetchXml += `<attribute name="${attr}" />`;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (orderByMatch) {
|
|
298
|
+
const [field, direction] = orderByMatch[1].split(' ');
|
|
299
|
+
fetchXml += `<order attribute="${field.trim()}" descending="${direction?.toLowerCase() === 'desc'}" />`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fetchXml += `</entity></fetch>`;
|
|
303
|
+
} else {
|
|
304
|
+
fetchXml = `<fetch><entity name="${entityName}"></entity></fetch>`;
|
|
104
305
|
}
|
|
105
|
-
|
|
106
|
-
fetchXml
|
|
107
|
-
|
|
108
|
-
|
|
306
|
+
|
|
307
|
+
Logger.fetchXml(fetchXml, 'DynamicsProvider.retrieveMultiple');
|
|
308
|
+
const result = await apiService.retrieveMultipleRecords(
|
|
309
|
+
entityName,
|
|
310
|
+
fetchXml
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
Logger.log(
|
|
314
|
+
`Retrieved ${result.entities.length} ${entityName} records`,
|
|
315
|
+
'DynamicsProvider.retrieveMultiple'
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return result;
|
|
319
|
+
} catch (error) {
|
|
320
|
+
Logger.error(
|
|
321
|
+
`Failed to retrieve ${entityName} records`,
|
|
322
|
+
'DynamicsProvider.retrieveMultiple',
|
|
323
|
+
error
|
|
324
|
+
);
|
|
325
|
+
throw error;
|
|
109
326
|
}
|
|
110
|
-
|
|
111
|
-
return await apiService.retrieveMultipleRecords(entityName, fetchXml);
|
|
112
327
|
};
|
|
113
328
|
|
|
114
329
|
const value: DynamicsContextType = {
|
|
@@ -118,6 +333,8 @@ export const DynamicsProvider: React.FC<DynamicsProviderProps> = ({
|
|
|
118
333
|
updateRecord,
|
|
119
334
|
deleteRecord,
|
|
120
335
|
retrieveMultiple,
|
|
336
|
+
isEnvironmentMock: environmentType === 'mock',
|
|
337
|
+
environmentType,
|
|
121
338
|
};
|
|
122
339
|
|
|
123
340
|
return (
|
|
@@ -133,4 +350,4 @@ export const useDynamicsApi = (): DynamicsContextType => {
|
|
|
133
350
|
throw new Error('useDynamicsApi must be used within a DynamicsProvider');
|
|
134
351
|
}
|
|
135
352
|
return context;
|
|
136
|
-
};
|
|
353
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { IApiService } from '@khester/dynamics-ui-api-client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mock implementation of IApiService for local development and testing.
|
|
5
|
+
* Provides in-memory storage and simulated API responses.
|
|
6
|
+
*/
|
|
7
|
+
export class MockApiService implements IApiService {
|
|
8
|
+
private storage: Map<string, Map<string, any>> = new Map();
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
// Initialize with some mock data
|
|
12
|
+
this.initializeMockData();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async createRecord(entityName: string, record: any): Promise<any> {
|
|
16
|
+
console.log(`MockApiService: Creating ${entityName} record`, record);
|
|
17
|
+
|
|
18
|
+
// Simulate network delay
|
|
19
|
+
await this.simulateDelay();
|
|
20
|
+
|
|
21
|
+
// Get or create entity collection
|
|
22
|
+
if (!this.storage.has(entityName)) {
|
|
23
|
+
this.storage.set(entityName, new Map());
|
|
24
|
+
}
|
|
25
|
+
const collection = this.storage.get(entityName)!;
|
|
26
|
+
|
|
27
|
+
// Generate ID based on entity type
|
|
28
|
+
const id = this.generateId(entityName);
|
|
29
|
+
const primaryKey = this.getPrimaryKey(entityName);
|
|
30
|
+
|
|
31
|
+
// Create record with ID
|
|
32
|
+
const newRecord = {
|
|
33
|
+
...record,
|
|
34
|
+
[primaryKey]: id,
|
|
35
|
+
createdon: new Date().toISOString(),
|
|
36
|
+
modifiedon: new Date().toISOString(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
collection.set(id, newRecord);
|
|
40
|
+
|
|
41
|
+
return newRecord;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async updateRecord(
|
|
45
|
+
entityName: string,
|
|
46
|
+
id: string,
|
|
47
|
+
record: any
|
|
48
|
+
): Promise<any> {
|
|
49
|
+
console.log(`MockApiService: Updating ${entityName} record ${id}`, record);
|
|
50
|
+
|
|
51
|
+
await this.simulateDelay();
|
|
52
|
+
|
|
53
|
+
const collection = this.storage.get(entityName);
|
|
54
|
+
if (!collection || !collection.has(id)) {
|
|
55
|
+
throw new Error(`Record not found: ${entityName} with ID ${id}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const existing = collection.get(id);
|
|
59
|
+
const updated = {
|
|
60
|
+
...existing,
|
|
61
|
+
...record,
|
|
62
|
+
modifiedon: new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
collection.set(id, updated);
|
|
66
|
+
|
|
67
|
+
return updated;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async deleteRecord(entityName: string, id: string): Promise<void> {
|
|
71
|
+
console.log(`MockApiService: Deleting ${entityName} record ${id}`);
|
|
72
|
+
|
|
73
|
+
await this.simulateDelay();
|
|
74
|
+
|
|
75
|
+
const collection = this.storage.get(entityName);
|
|
76
|
+
if (!collection || !collection.has(id)) {
|
|
77
|
+
throw new Error(`Record not found: ${entityName} with ID ${id}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
collection.delete(id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async retrieveMultipleRecords(
|
|
84
|
+
entityName: string,
|
|
85
|
+
fetchXml: string
|
|
86
|
+
): Promise<{ entities: any[] }> {
|
|
87
|
+
console.log(
|
|
88
|
+
`MockApiService: Retrieving ${entityName} records with FetchXML`,
|
|
89
|
+
fetchXml
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
await this.simulateDelay();
|
|
93
|
+
|
|
94
|
+
const collection = this.storage.get(entityName);
|
|
95
|
+
if (!collection) {
|
|
96
|
+
return { entities: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Convert Map values to array
|
|
100
|
+
const entities = Array.from(collection.values());
|
|
101
|
+
|
|
102
|
+
// Simple filter parsing from FetchXML (basic implementation)
|
|
103
|
+
const filteredEntities = this.applyBasicFetchXmlFilter(entities, fetchXml);
|
|
104
|
+
|
|
105
|
+
return { entities: filteredEntities };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async executeRequest(requestName: string, requestData: any): Promise<any> {
|
|
109
|
+
console.log(
|
|
110
|
+
`MockApiService: Executing request ${requestName}`,
|
|
111
|
+
requestData
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await this.simulateDelay();
|
|
115
|
+
|
|
116
|
+
// Mock implementation - return success response
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
requestName,
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
data: requestData,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async uploadFile(file: File): Promise<string> {
|
|
126
|
+
console.log(`MockApiService: Uploading file ${file.name}`);
|
|
127
|
+
|
|
128
|
+
await this.simulateDelay(1000); // Longer delay for file upload
|
|
129
|
+
|
|
130
|
+
// Return mock file URL
|
|
131
|
+
return `https://mock-storage.dynamics365.com/files/${this.generateId('file')}-${file.name}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialize mock data for development
|
|
136
|
+
*/
|
|
137
|
+
private initializeMockData(): void {
|
|
138
|
+
// Create accounts collection
|
|
139
|
+
const accounts = new Map<string, any>();
|
|
140
|
+
accounts.set('00000000-0000-0000-0000-000000000001', {
|
|
141
|
+
accountid: '00000000-0000-0000-0000-000000000001',
|
|
142
|
+
name: 'Contoso Ltd',
|
|
143
|
+
emailaddress1: 'info@contoso.com',
|
|
144
|
+
telephone1: '555-0100',
|
|
145
|
+
address1_city: 'Seattle',
|
|
146
|
+
address1_stateorprovince: 'WA',
|
|
147
|
+
address1_country: 'USA',
|
|
148
|
+
revenue: 5000000,
|
|
149
|
+
numberofemployees: 250,
|
|
150
|
+
createdon: '2024-01-15T10:00:00Z',
|
|
151
|
+
modifiedon: '2024-01-15T10:00:00Z',
|
|
152
|
+
});
|
|
153
|
+
accounts.set('00000000-0000-0000-0000-000000000002', {
|
|
154
|
+
accountid: '00000000-0000-0000-0000-000000000002',
|
|
155
|
+
name: 'Adventure Works',
|
|
156
|
+
emailaddress1: 'contact@adventureworks.com',
|
|
157
|
+
telephone1: '555-0200',
|
|
158
|
+
address1_city: 'Redmond',
|
|
159
|
+
address1_stateorprovince: 'WA',
|
|
160
|
+
address1_country: 'USA',
|
|
161
|
+
revenue: 10000000,
|
|
162
|
+
numberofemployees: 500,
|
|
163
|
+
createdon: '2024-01-20T14:30:00Z',
|
|
164
|
+
modifiedon: '2024-01-20T14:30:00Z',
|
|
165
|
+
});
|
|
166
|
+
this.storage.set('accounts', accounts);
|
|
167
|
+
|
|
168
|
+
// Create contacts collection
|
|
169
|
+
const contacts = new Map<string, any>();
|
|
170
|
+
contacts.set('00000000-0000-0000-0000-000000000101', {
|
|
171
|
+
contactid: '00000000-0000-0000-0000-000000000101',
|
|
172
|
+
firstname: 'John',
|
|
173
|
+
lastname: 'Doe',
|
|
174
|
+
emailaddress1: 'john.doe@contoso.com',
|
|
175
|
+
telephone1: '555-0101',
|
|
176
|
+
parentcustomerid: '00000000-0000-0000-0000-000000000001',
|
|
177
|
+
parentcustomerid_account: {
|
|
178
|
+
accountid: '00000000-0000-0000-0000-000000000001',
|
|
179
|
+
name: 'Contoso Ltd',
|
|
180
|
+
},
|
|
181
|
+
createdon: '2024-01-16T09:00:00Z',
|
|
182
|
+
modifiedon: '2024-01-16T09:00:00Z',
|
|
183
|
+
});
|
|
184
|
+
contacts.set('00000000-0000-0000-0000-000000000102', {
|
|
185
|
+
contactid: '00000000-0000-0000-0000-000000000102',
|
|
186
|
+
firstname: 'Jane',
|
|
187
|
+
lastname: 'Smith',
|
|
188
|
+
emailaddress1: 'jane.smith@adventureworks.com',
|
|
189
|
+
telephone1: '555-0201',
|
|
190
|
+
parentcustomerid: '00000000-0000-0000-0000-000000000002',
|
|
191
|
+
parentcustomerid_account: {
|
|
192
|
+
accountid: '00000000-0000-0000-0000-000000000002',
|
|
193
|
+
name: 'Adventure Works',
|
|
194
|
+
},
|
|
195
|
+
createdon: '2024-01-21T11:00:00Z',
|
|
196
|
+
modifiedon: '2024-01-21T11:00:00Z',
|
|
197
|
+
});
|
|
198
|
+
this.storage.set('contacts', contacts);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Generate a mock ID for an entity
|
|
203
|
+
*/
|
|
204
|
+
private generateId(entityName: string): string {
|
|
205
|
+
const timestamp = Date.now();
|
|
206
|
+
const random = Math.floor(Math.random() * 1000000);
|
|
207
|
+
return `${entityName}-${timestamp}-${random}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the primary key field name for an entity
|
|
212
|
+
*/
|
|
213
|
+
private getPrimaryKey(entityName: string): string {
|
|
214
|
+
const primaryKeys: Record<string, string> = {
|
|
215
|
+
accounts: 'accountid',
|
|
216
|
+
contacts: 'contactid',
|
|
217
|
+
opportunities: 'opportunityid',
|
|
218
|
+
leads: 'leadid',
|
|
219
|
+
incidents: 'incidentid',
|
|
220
|
+
quotes: 'quoteid',
|
|
221
|
+
salesorders: 'salesorderid',
|
|
222
|
+
invoices: 'invoiceid',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return primaryKeys[entityName] || `${entityName.replace(/s$/, '')}id`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Apply basic FetchXML filtering (simplified implementation)
|
|
230
|
+
*/
|
|
231
|
+
private applyBasicFetchXmlFilter(entities: any[], fetchXml: string): any[] {
|
|
232
|
+
// Extract attribute names from FetchXML
|
|
233
|
+
const attributeMatches = fetchXml.match(/<attribute name="([^"]+)"/g);
|
|
234
|
+
if (!attributeMatches || attributeMatches.length === 0) {
|
|
235
|
+
return entities;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const attributes = attributeMatches.map((match) =>
|
|
239
|
+
match.replace('<attribute name="', '').replace('"', '')
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Filter entities to only include requested attributes
|
|
243
|
+
return entities.map((entity) => {
|
|
244
|
+
const filtered: any = {};
|
|
245
|
+
attributes.forEach((attr) => {
|
|
246
|
+
if (Object.prototype.hasOwnProperty.call(entity, attr)) {
|
|
247
|
+
filtered[attr] = entity[attr];
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return filtered;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Simulate network delay for realistic behavior
|
|
256
|
+
*/
|
|
257
|
+
private async simulateDelay(ms: number = 200): Promise<void> {
|
|
258
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
|
+
}
|
|
260
|
+
}
|