@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
package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Creating a custom PCF wrapper
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to create a sophisticated PCF wrapper
|
|
5
|
+
* that can be used in various Dynamics 365 contexts, with smart API
|
|
6
|
+
* service detection and comprehensive error handling.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // Use in your PCF component's init method
|
|
11
|
+
* export class CustomPCFControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
|
|
12
|
+
* public init(context: ComponentFramework.Context<IInputs>): void {
|
|
13
|
+
* ReactDOM.render(
|
|
14
|
+
* React.createElement(CustomPCFWrapper, {
|
|
15
|
+
* context,
|
|
16
|
+
* entityType: 'contact',
|
|
17
|
+
* enableLogging: true
|
|
18
|
+
* }),
|
|
19
|
+
* this.container
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
|
27
|
+
import { ContactManagement } from '../../components/ContactManagement';
|
|
28
|
+
import { AccountManagement } from '../../components/AccountManagement';
|
|
29
|
+
import { DynamicsProvider } from '../../providers/DynamicsProvider';
|
|
30
|
+
import { ServiceFactory } from '../../services/ServiceFactory';
|
|
31
|
+
import { Logger } from '../../components/Logging/logger';
|
|
32
|
+
import { LoggingProvider } from '../../components/Logging/LoggingProvider';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* PCF Context interface that matches ComponentFramework.Context
|
|
36
|
+
*/
|
|
37
|
+
interface PCFContextType {
|
|
38
|
+
/** PCF WebAPI object for Dynamics 365 operations */
|
|
39
|
+
webAPI: {
|
|
40
|
+
createRecord: (entityName: string, data: any) => Promise<any>;
|
|
41
|
+
retrieveRecord: (
|
|
42
|
+
entityName: string,
|
|
43
|
+
id: string,
|
|
44
|
+
select?: string
|
|
45
|
+
) => Promise<any>;
|
|
46
|
+
updateRecord: (entityName: string, id: string, data: any) => Promise<any>;
|
|
47
|
+
deleteRecord: (entityName: string, id: string) => Promise<void>;
|
|
48
|
+
retrieveMultipleRecords: (
|
|
49
|
+
entityName: string,
|
|
50
|
+
query?: string
|
|
51
|
+
) => Promise<any>;
|
|
52
|
+
};
|
|
53
|
+
/** PCF utility functions */
|
|
54
|
+
utils: {
|
|
55
|
+
lookupObjects: (lookupOptions: any) => Promise<any>;
|
|
56
|
+
openAlertDialog: (
|
|
57
|
+
alertStrings: any,
|
|
58
|
+
useShowModal?: boolean
|
|
59
|
+
) => Promise<any>;
|
|
60
|
+
openConfirmDialog: (
|
|
61
|
+
confirmStrings: any,
|
|
62
|
+
useShowModal?: boolean
|
|
63
|
+
) => Promise<any>;
|
|
64
|
+
openErrorDialog: (errorOptions: any) => Promise<any>;
|
|
65
|
+
openFileDialog: (fileOptions: any) => Promise<any>;
|
|
66
|
+
openUrl: (url: string, options?: any) => void;
|
|
67
|
+
};
|
|
68
|
+
/** PCF parameters */
|
|
69
|
+
parameters: {
|
|
70
|
+
[key: string]: any;
|
|
71
|
+
};
|
|
72
|
+
/** PCF mode information */
|
|
73
|
+
mode: {
|
|
74
|
+
isControlDisabled: boolean;
|
|
75
|
+
isVisible: boolean;
|
|
76
|
+
label: string;
|
|
77
|
+
};
|
|
78
|
+
/** PCF resources */
|
|
79
|
+
resources: {
|
|
80
|
+
getString: (id: string) => string;
|
|
81
|
+
getResource: (
|
|
82
|
+
id: string,
|
|
83
|
+
success: (data: string) => void,
|
|
84
|
+
failure?: () => void
|
|
85
|
+
) => void;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Props for the CustomPCFWrapper component
|
|
91
|
+
*/
|
|
92
|
+
interface CustomPCFWrapperProps {
|
|
93
|
+
/** PCF context from the framework */
|
|
94
|
+
context: PCFContextType;
|
|
95
|
+
/** Entity type to manage ('contact' | 'account' | 'auto') */
|
|
96
|
+
entityType?: 'contact' | 'account' | 'auto';
|
|
97
|
+
/** Whether to enable comprehensive logging */
|
|
98
|
+
enableLogging?: boolean;
|
|
99
|
+
/** Whether to show tabs for multi-entity support */
|
|
100
|
+
showTabs?: boolean;
|
|
101
|
+
/** Custom title for the component */
|
|
102
|
+
title?: string;
|
|
103
|
+
/** Whether to enable debug mode */
|
|
104
|
+
debugMode?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Custom PCF Wrapper Component
|
|
109
|
+
*
|
|
110
|
+
* A sophisticated wrapper that provides:
|
|
111
|
+
* - Smart API service creation with fallback handling
|
|
112
|
+
* - Environment detection and logging integration
|
|
113
|
+
* - Multi-entity support with dynamic switching
|
|
114
|
+
* - Error boundary and recovery mechanisms
|
|
115
|
+
* - Performance monitoring and optimization
|
|
116
|
+
*
|
|
117
|
+
* @param props - Component properties
|
|
118
|
+
* @returns JSX.Element
|
|
119
|
+
*/
|
|
120
|
+
export const CustomPCFWrapper: React.FC<CustomPCFWrapperProps> = ({
|
|
121
|
+
context,
|
|
122
|
+
entityType = 'auto',
|
|
123
|
+
enableLogging = true,
|
|
124
|
+
showTabs = true,
|
|
125
|
+
title,
|
|
126
|
+
debugMode = false,
|
|
127
|
+
}) => {
|
|
128
|
+
const [selectedEntity, setSelectedEntity] = useState<'contact' | 'account'>(
|
|
129
|
+
'contact'
|
|
130
|
+
);
|
|
131
|
+
const [apiServiceReady, setApiServiceReady] = useState(false);
|
|
132
|
+
const [error, setError] = useState<string | null>(null);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Initialize logging if enabled
|
|
136
|
+
*/
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (enableLogging) {
|
|
139
|
+
Logger.log('CustomPCFWrapper initialized', 'CustomPCFWrapper');
|
|
140
|
+
Logger.userAction(
|
|
141
|
+
'PCF component loaded',
|
|
142
|
+
{
|
|
143
|
+
entityType,
|
|
144
|
+
showTabs,
|
|
145
|
+
debugMode,
|
|
146
|
+
hasWebAPI: !!context.webAPI,
|
|
147
|
+
},
|
|
148
|
+
'CustomPCFWrapper'
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}, [enableLogging, entityType, showTabs, debugMode, context.webAPI]);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create API service with comprehensive error handling and fallbacks
|
|
155
|
+
*/
|
|
156
|
+
const apiService = useMemo(() => {
|
|
157
|
+
try {
|
|
158
|
+
// Check if we're in PCF context with webAPI available
|
|
159
|
+
if (context.webAPI) {
|
|
160
|
+
if (enableLogging) {
|
|
161
|
+
Logger.log(
|
|
162
|
+
'PCF WebAPI available, creating custom PCF API service',
|
|
163
|
+
'CustomPCFWrapper'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create a comprehensive API service that uses PCF's webAPI
|
|
168
|
+
return {
|
|
169
|
+
/**
|
|
170
|
+
* Create a new record using PCF WebAPI
|
|
171
|
+
*/
|
|
172
|
+
createRecord: async (entityName: string, data: any) => {
|
|
173
|
+
try {
|
|
174
|
+
if (enableLogging) {
|
|
175
|
+
Logger.apiOperation(
|
|
176
|
+
'CREATE',
|
|
177
|
+
entityName,
|
|
178
|
+
data,
|
|
179
|
+
'CustomPCFWrapper.createRecord'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const result = await context.webAPI.createRecord(
|
|
183
|
+
entityName,
|
|
184
|
+
data
|
|
185
|
+
);
|
|
186
|
+
if (enableLogging) {
|
|
187
|
+
Logger.log(
|
|
188
|
+
`Successfully created ${entityName} record`,
|
|
189
|
+
'CustomPCFWrapper.createRecord'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (enableLogging) {
|
|
195
|
+
Logger.error(
|
|
196
|
+
`Failed to create ${entityName} record`,
|
|
197
|
+
'CustomPCFWrapper.createRecord',
|
|
198
|
+
error
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Retrieve a single record by ID
|
|
207
|
+
*/
|
|
208
|
+
retrieveRecord: async (
|
|
209
|
+
entityName: string,
|
|
210
|
+
id: string,
|
|
211
|
+
select?: string
|
|
212
|
+
) => {
|
|
213
|
+
try {
|
|
214
|
+
if (enableLogging) {
|
|
215
|
+
Logger.apiOperation(
|
|
216
|
+
'READ',
|
|
217
|
+
entityName,
|
|
218
|
+
{ id, select },
|
|
219
|
+
'CustomPCFWrapper.retrieveRecord'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const result = await context.webAPI.retrieveRecord(
|
|
223
|
+
entityName,
|
|
224
|
+
id,
|
|
225
|
+
select
|
|
226
|
+
);
|
|
227
|
+
if (enableLogging) {
|
|
228
|
+
Logger.log(
|
|
229
|
+
`Successfully retrieved ${entityName} record`,
|
|
230
|
+
'CustomPCFWrapper.retrieveRecord'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (enableLogging) {
|
|
236
|
+
Logger.error(
|
|
237
|
+
`Failed to retrieve ${entityName} record`,
|
|
238
|
+
'CustomPCFWrapper.retrieveRecord',
|
|
239
|
+
error
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Update an existing record
|
|
248
|
+
*/
|
|
249
|
+
updateRecord: async (entityName: string, id: string, data: any) => {
|
|
250
|
+
try {
|
|
251
|
+
if (enableLogging) {
|
|
252
|
+
Logger.apiOperation(
|
|
253
|
+
'UPDATE',
|
|
254
|
+
entityName,
|
|
255
|
+
{ id, data },
|
|
256
|
+
'CustomPCFWrapper.updateRecord'
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const result = await context.webAPI.updateRecord(
|
|
260
|
+
entityName,
|
|
261
|
+
id,
|
|
262
|
+
data
|
|
263
|
+
);
|
|
264
|
+
if (enableLogging) {
|
|
265
|
+
Logger.log(
|
|
266
|
+
`Successfully updated ${entityName} record`,
|
|
267
|
+
'CustomPCFWrapper.updateRecord'
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
if (enableLogging) {
|
|
273
|
+
Logger.error(
|
|
274
|
+
`Failed to update ${entityName} record`,
|
|
275
|
+
'CustomPCFWrapper.updateRecord',
|
|
276
|
+
error
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Delete a record
|
|
285
|
+
*/
|
|
286
|
+
deleteRecord: async (entityName: string, id: string) => {
|
|
287
|
+
try {
|
|
288
|
+
if (enableLogging) {
|
|
289
|
+
Logger.apiOperation(
|
|
290
|
+
'DELETE',
|
|
291
|
+
entityName,
|
|
292
|
+
{ id },
|
|
293
|
+
'CustomPCFWrapper.deleteRecord'
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
await context.webAPI.deleteRecord(entityName, id);
|
|
297
|
+
if (enableLogging) {
|
|
298
|
+
Logger.log(
|
|
299
|
+
`Successfully deleted ${entityName} record`,
|
|
300
|
+
'CustomPCFWrapper.deleteRecord'
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (enableLogging) {
|
|
305
|
+
Logger.error(
|
|
306
|
+
`Failed to delete ${entityName} record`,
|
|
307
|
+
'CustomPCFWrapper.deleteRecord',
|
|
308
|
+
error
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Retrieve multiple records using FetchXML
|
|
317
|
+
*/
|
|
318
|
+
retrieveMultipleRecords: async (
|
|
319
|
+
entityName: string,
|
|
320
|
+
fetchXml: string
|
|
321
|
+
) => {
|
|
322
|
+
try {
|
|
323
|
+
if (enableLogging) {
|
|
324
|
+
Logger.fetchXml(
|
|
325
|
+
fetchXml,
|
|
326
|
+
'CustomPCFWrapper.retrieveMultipleRecords'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const encodedFetchXml = encodeURIComponent(fetchXml);
|
|
330
|
+
const result = await context.webAPI.retrieveMultipleRecords(
|
|
331
|
+
entityName,
|
|
332
|
+
`?fetchXml=${encodedFetchXml}`
|
|
333
|
+
);
|
|
334
|
+
if (enableLogging) {
|
|
335
|
+
Logger.log(
|
|
336
|
+
`Successfully retrieved ${result.entities?.length || 0} ${entityName} records`,
|
|
337
|
+
'CustomPCFWrapper.retrieveMultipleRecords'
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return result;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (enableLogging) {
|
|
343
|
+
Logger.error(
|
|
344
|
+
`Failed to retrieve ${entityName} records`,
|
|
345
|
+
'CustomPCFWrapper.retrieveMultipleRecords',
|
|
346
|
+
error
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Execute custom request (not supported in PCF WebAPI)
|
|
355
|
+
*/
|
|
356
|
+
executeRequest: async (requestName: string, requestData: any) => {
|
|
357
|
+
const errorMessage = `Custom requests not supported in PCF WebAPI: ${requestName}`;
|
|
358
|
+
if (enableLogging) {
|
|
359
|
+
Logger.warn(errorMessage, 'CustomPCFWrapper.executeRequest');
|
|
360
|
+
}
|
|
361
|
+
throw new Error(errorMessage);
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Upload file (not supported in PCF WebAPI)
|
|
366
|
+
*/
|
|
367
|
+
uploadFile: async (file: File) => {
|
|
368
|
+
const errorMessage = 'File upload not supported in PCF WebAPI';
|
|
369
|
+
if (enableLogging) {
|
|
370
|
+
Logger.warn(errorMessage, 'CustomPCFWrapper.uploadFile');
|
|
371
|
+
}
|
|
372
|
+
throw new Error(errorMessage);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Fallback to ServiceFactory for environment detection
|
|
378
|
+
if (enableLogging) {
|
|
379
|
+
Logger.log(
|
|
380
|
+
'PCF WebAPI not available, falling back to ServiceFactory',
|
|
381
|
+
'CustomPCFWrapper'
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const xrmGlobal = ServiceFactory.isDynamics365Context()
|
|
386
|
+
? (window as any).Xrm
|
|
387
|
+
: undefined;
|
|
388
|
+
return ServiceFactory.createApiService(xrmGlobal);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
const errorMessage = 'Failed to create API service in PCF context';
|
|
391
|
+
if (enableLogging) {
|
|
392
|
+
Logger.error(errorMessage, 'CustomPCFWrapper', error);
|
|
393
|
+
}
|
|
394
|
+
setError(errorMessage);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}, [context.webAPI, enableLogging]);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Set API service ready state
|
|
401
|
+
*/
|
|
402
|
+
useEffect(() => {
|
|
403
|
+
setApiServiceReady(!!apiService);
|
|
404
|
+
}, [apiService]);
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Determine which entity to show based on entityType prop
|
|
408
|
+
*/
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
if (entityType === 'auto') {
|
|
411
|
+
// Auto-detect based on form context if available
|
|
412
|
+
// This is a simplified example - in practice, you'd check context.parameters
|
|
413
|
+
setSelectedEntity('contact');
|
|
414
|
+
} else {
|
|
415
|
+
setSelectedEntity(entityType);
|
|
416
|
+
}
|
|
417
|
+
}, [entityType]);
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Handle entity tab change
|
|
421
|
+
*/
|
|
422
|
+
const handleEntityChange = useCallback(
|
|
423
|
+
(newEntity: 'contact' | 'account') => {
|
|
424
|
+
if (enableLogging) {
|
|
425
|
+
Logger.userAction(
|
|
426
|
+
'Entity tab changed in PCF wrapper',
|
|
427
|
+
{ from: selectedEntity, to: newEntity },
|
|
428
|
+
'CustomPCFWrapper.handleEntityChange'
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
setSelectedEntity(newEntity);
|
|
432
|
+
},
|
|
433
|
+
[selectedEntity, enableLogging]
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Render the appropriate management component
|
|
438
|
+
*/
|
|
439
|
+
const renderManagementComponent = () => {
|
|
440
|
+
if (!apiServiceReady) {
|
|
441
|
+
return (
|
|
442
|
+
<div style={{ padding: '20px', textAlign: 'center' }}>
|
|
443
|
+
<div>Loading...</div>
|
|
444
|
+
{debugMode && (
|
|
445
|
+
<div style={{ marginTop: '10px', fontSize: '12px', color: '#666' }}>
|
|
446
|
+
Initializing API service...
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
switch (selectedEntity) {
|
|
454
|
+
case 'contact':
|
|
455
|
+
return <ContactManagement />;
|
|
456
|
+
case 'account':
|
|
457
|
+
return <AccountManagement />;
|
|
458
|
+
default:
|
|
459
|
+
return <ContactManagement />;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Render error boundary
|
|
465
|
+
*/
|
|
466
|
+
if (error) {
|
|
467
|
+
return (
|
|
468
|
+
<div
|
|
469
|
+
style={{
|
|
470
|
+
padding: '20px',
|
|
471
|
+
border: '1px solid #d13438',
|
|
472
|
+
borderRadius: '4px',
|
|
473
|
+
backgroundColor: '#fef7f1',
|
|
474
|
+
color: '#d13438',
|
|
475
|
+
}}
|
|
476
|
+
>
|
|
477
|
+
<h3>PCF Integration Error</h3>
|
|
478
|
+
<p>{error}</p>
|
|
479
|
+
{debugMode && (
|
|
480
|
+
<details style={{ marginTop: '10px' }}>
|
|
481
|
+
<summary>Debug Information</summary>
|
|
482
|
+
<pre style={{ fontSize: '12px', marginTop: '10px' }}>
|
|
483
|
+
{JSON.stringify(
|
|
484
|
+
{
|
|
485
|
+
hasWebAPI: !!context.webAPI,
|
|
486
|
+
hasUtils: !!context.utils,
|
|
487
|
+
hasParameters: !!context.parameters,
|
|
488
|
+
entityType,
|
|
489
|
+
enableLogging,
|
|
490
|
+
showTabs,
|
|
491
|
+
},
|
|
492
|
+
null,
|
|
493
|
+
2
|
|
494
|
+
)}
|
|
495
|
+
</pre>
|
|
496
|
+
</details>
|
|
497
|
+
)}
|
|
498
|
+
</div>
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Container styles
|
|
504
|
+
*/
|
|
505
|
+
const containerStyle: React.CSSProperties = {
|
|
506
|
+
width: '100%',
|
|
507
|
+
height: '100%',
|
|
508
|
+
display: 'flex',
|
|
509
|
+
flexDirection: 'column',
|
|
510
|
+
fontFamily: '"Segoe UI", Tahoma, Arial, sans-serif',
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const tabContainerStyle: React.CSSProperties = {
|
|
514
|
+
borderBottom: '1px solid #edebe9',
|
|
515
|
+
backgroundColor: '#faf9f8',
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const tabStyle: React.CSSProperties = {
|
|
519
|
+
display: 'inline-block',
|
|
520
|
+
padding: '8px 16px',
|
|
521
|
+
cursor: 'pointer',
|
|
522
|
+
borderBottom: '2px solid transparent',
|
|
523
|
+
fontSize: '14px',
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const activeTabStyle: React.CSSProperties = {
|
|
527
|
+
...tabStyle,
|
|
528
|
+
borderBottomColor: '#0078d4',
|
|
529
|
+
backgroundColor: 'white',
|
|
530
|
+
fontWeight: '600',
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const contentStyle: React.CSSProperties = {
|
|
534
|
+
flex: 1,
|
|
535
|
+
overflow: 'auto',
|
|
536
|
+
backgroundColor: 'white',
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
return (
|
|
540
|
+
<DynamicsProvider customApiService={apiService || undefined}>
|
|
541
|
+
{enableLogging ? (
|
|
542
|
+
<LoggingProvider>
|
|
543
|
+
<div style={containerStyle}>
|
|
544
|
+
{/* Title */}
|
|
545
|
+
{title && (
|
|
546
|
+
<div
|
|
547
|
+
style={{
|
|
548
|
+
padding: '12px 16px',
|
|
549
|
+
borderBottom: '1px solid #edebe9',
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>
|
|
553
|
+
{title}
|
|
554
|
+
</h3>
|
|
555
|
+
</div>
|
|
556
|
+
)}
|
|
557
|
+
|
|
558
|
+
{/* Tabs */}
|
|
559
|
+
{showTabs && (entityType === 'auto' || showTabs) && (
|
|
560
|
+
<div style={tabContainerStyle}>
|
|
561
|
+
<div
|
|
562
|
+
style={
|
|
563
|
+
selectedEntity === 'contact' ? activeTabStyle : tabStyle
|
|
564
|
+
}
|
|
565
|
+
onClick={() => handleEntityChange('contact')}
|
|
566
|
+
>
|
|
567
|
+
Contacts
|
|
568
|
+
</div>
|
|
569
|
+
<div
|
|
570
|
+
style={
|
|
571
|
+
selectedEntity === 'account' ? activeTabStyle : tabStyle
|
|
572
|
+
}
|
|
573
|
+
onClick={() => handleEntityChange('account')}
|
|
574
|
+
>
|
|
575
|
+
Accounts
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
)}
|
|
579
|
+
|
|
580
|
+
{/* Content */}
|
|
581
|
+
<div style={contentStyle}>{renderManagementComponent()}</div>
|
|
582
|
+
|
|
583
|
+
{/* Debug Info */}
|
|
584
|
+
{debugMode && (
|
|
585
|
+
<div
|
|
586
|
+
style={{
|
|
587
|
+
padding: '8px',
|
|
588
|
+
borderTop: '1px solid #edebe9',
|
|
589
|
+
backgroundColor: '#f3f2f1',
|
|
590
|
+
fontSize: '11px',
|
|
591
|
+
color: '#666',
|
|
592
|
+
}}
|
|
593
|
+
>
|
|
594
|
+
Debug: {ServiceFactory.getEnvironmentType()} | API Ready:{' '}
|
|
595
|
+
{apiServiceReady ? 'Yes' : 'No'}
|
|
596
|
+
</div>
|
|
597
|
+
)}
|
|
598
|
+
</div>
|
|
599
|
+
</LoggingProvider>
|
|
600
|
+
) : (
|
|
601
|
+
<div style={containerStyle}>
|
|
602
|
+
{/* Same structure but without LoggingProvider */}
|
|
603
|
+
{title && (
|
|
604
|
+
<div
|
|
605
|
+
style={{
|
|
606
|
+
padding: '12px 16px',
|
|
607
|
+
borderBottom: '1px solid #edebe9',
|
|
608
|
+
}}
|
|
609
|
+
>
|
|
610
|
+
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: '600' }}>
|
|
611
|
+
{title}
|
|
612
|
+
</h3>
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
616
|
+
{showTabs && (
|
|
617
|
+
<div style={tabContainerStyle}>
|
|
618
|
+
<div
|
|
619
|
+
style={selectedEntity === 'contact' ? activeTabStyle : tabStyle}
|
|
620
|
+
onClick={() => handleEntityChange('contact')}
|
|
621
|
+
>
|
|
622
|
+
Contacts
|
|
623
|
+
</div>
|
|
624
|
+
<div
|
|
625
|
+
style={selectedEntity === 'account' ? activeTabStyle : tabStyle}
|
|
626
|
+
onClick={() => handleEntityChange('account')}
|
|
627
|
+
>
|
|
628
|
+
Accounts
|
|
629
|
+
</div>
|
|
630
|
+
</div>
|
|
631
|
+
)}
|
|
632
|
+
|
|
633
|
+
<div style={contentStyle}>{renderManagementComponent()}</div>
|
|
634
|
+
|
|
635
|
+
{debugMode && (
|
|
636
|
+
<div
|
|
637
|
+
style={{
|
|
638
|
+
padding: '8px',
|
|
639
|
+
borderTop: '1px solid #edebe9',
|
|
640
|
+
backgroundColor: '#f3f2f1',
|
|
641
|
+
fontSize: '11px',
|
|
642
|
+
color: '#666',
|
|
643
|
+
}}
|
|
644
|
+
>
|
|
645
|
+
Debug: {ServiceFactory.getEnvironmentType()} | API Ready:{' '}
|
|
646
|
+
{apiServiceReady ? 'Yes' : 'No'}
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
</DynamicsProvider>
|
|
652
|
+
);
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Export for PCF integration
|
|
657
|
+
* This is the main export that should be used in PCF components
|
|
658
|
+
*/
|
|
659
|
+
export default CustomPCFWrapper;
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Example PCF Control Implementation
|
|
663
|
+
*
|
|
664
|
+
* This shows how to use the CustomPCFWrapper in a real PCF component.
|
|
665
|
+
* Copy this pattern to your PCF project's index.ts file.
|
|
666
|
+
*/
|
|
667
|
+
export const examplePCFImplementation = `
|
|
668
|
+
import * as React from 'react';
|
|
669
|
+
import * as ReactDOM from 'react-dom';
|
|
670
|
+
import { CustomPCFWrapper } from './CustomPCFWrapper';
|
|
671
|
+
|
|
672
|
+
export class CustomPCFControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
|
|
673
|
+
private container: HTMLDivElement;
|
|
674
|
+
private notifyOutputChanged: () => void;
|
|
675
|
+
|
|
676
|
+
public init(
|
|
677
|
+
context: ComponentFramework.Context<IInputs>,
|
|
678
|
+
notifyOutputChanged: () => void,
|
|
679
|
+
state: ComponentFramework.Dictionary,
|
|
680
|
+
container: HTMLDivElement
|
|
681
|
+
): void {
|
|
682
|
+
this.container = container;
|
|
683
|
+
this.notifyOutputChanged = notifyOutputChanged;
|
|
684
|
+
|
|
685
|
+
// Render the CustomPCFWrapper
|
|
686
|
+
ReactDOM.render(
|
|
687
|
+
React.createElement(CustomPCFWrapper, {
|
|
688
|
+
context,
|
|
689
|
+
entityType: 'auto', // or 'contact', 'account'
|
|
690
|
+
enableLogging: true,
|
|
691
|
+
showTabs: true,
|
|
692
|
+
title: 'Customer Management',
|
|
693
|
+
debugMode: false // Set to true for development
|
|
694
|
+
}),
|
|
695
|
+
this.container
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
public updateView(context: ComponentFramework.Context<IInputs>): void {
|
|
700
|
+
// Re-render if needed
|
|
701
|
+
ReactDOM.render(
|
|
702
|
+
React.createElement(CustomPCFWrapper, {
|
|
703
|
+
context,
|
|
704
|
+
entityType: 'auto',
|
|
705
|
+
enableLogging: true,
|
|
706
|
+
showTabs: true,
|
|
707
|
+
title: 'Customer Management',
|
|
708
|
+
debugMode: false
|
|
709
|
+
}),
|
|
710
|
+
this.container
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
public getOutputs(): IOutputs {
|
|
715
|
+
return {};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
public destroy(): void {
|
|
719
|
+
ReactDOM.unmountComponentAtNode(this.container);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
`;
|