@khester/create-dynamics-app 1.1.0 → 2.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.
Files changed (210) hide show
  1. package/README.md +74 -0
  2. package/dist/artifacts/registry.d.ts +18 -0
  3. package/dist/artifacts/registry.d.ts.map +1 -0
  4. package/dist/artifacts/registry.js +340 -0
  5. package/dist/artifacts/registry.js.map +1 -0
  6. package/dist/artifacts/types.d.ts +122 -0
  7. package/dist/artifacts/types.d.ts.map +1 -0
  8. package/dist/artifacts/types.js +7 -0
  9. package/dist/artifacts/types.js.map +1 -0
  10. package/dist/artifacts/validators.d.ts +16 -0
  11. package/dist/artifacts/validators.d.ts.map +1 -0
  12. package/dist/artifacts/validators.js +45 -0
  13. package/dist/artifacts/validators.js.map +1 -0
  14. package/dist/fromDesign.d.ts +5 -0
  15. package/dist/fromDesign.d.ts.map +1 -0
  16. package/dist/fromDesign.js +98 -0
  17. package/dist/fromDesign.js.map +1 -0
  18. package/dist/index.js +129 -177
  19. package/dist/index.js.map +1 -1
  20. package/dist/injectDevTools.d.ts +28 -0
  21. package/dist/injectDevTools.d.ts.map +1 -0
  22. package/dist/injectDevTools.js +148 -0
  23. package/dist/injectDevTools.js.map +1 -0
  24. package/dist/scaffold.d.ts +48 -0
  25. package/dist/scaffold.d.ts.map +1 -0
  26. package/dist/scaffold.js +180 -0
  27. package/dist/scaffold.js.map +1 -0
  28. package/dist/templatePlan.d.ts +3 -0
  29. package/dist/templatePlan.d.ts.map +1 -0
  30. package/dist/templatePlan.js +43 -0
  31. package/dist/templatePlan.js.map +1 -0
  32. package/dist/utils/copyTemplate.d.ts +13 -1
  33. package/dist/utils/copyTemplate.d.ts.map +1 -1
  34. package/dist/utils/copyTemplate.js +98 -4
  35. package/dist/utils/copyTemplate.js.map +1 -1
  36. package/dist/utils/updatePackageJson.d.ts +11 -1
  37. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  38. package/dist/utils/updatePackageJson.js +12 -10
  39. package/dist/utils/updatePackageJson.js.map +1 -1
  40. package/package.json +10 -7
  41. package/templates/_shared/dev-tools/auth/get-token.js +72 -0
  42. package/templates/_shared/dev-tools/dev/mock-xrm.js +42 -0
  43. package/templates/_shared/dev-tools/metadata-sync/index.js +152 -0
  44. package/templates/_shared/dev-tools/smoke/test-retrieve.js +44 -0
  45. package/templates/dialog-form/README.md +27 -0
  46. package/templates/dialog-form/_variants/App.v8.tsx +39 -0
  47. package/templates/dialog-form/_variants/App.v9.tsx +41 -0
  48. package/templates/dialog-form/gitignore +5 -0
  49. package/templates/dialog-form/package.json +27 -0
  50. package/templates/dialog-form/public/index.html +11 -0
  51. package/templates/dialog-form/src/index.tsx +10 -0
  52. package/templates/dialog-form/src/services/dataverse.ts +30 -0
  53. package/templates/dialog-form/tsconfig.json +15 -0
  54. package/templates/dialog-form/webpack.config.js +17 -0
  55. package/templates/grid-customizer/README.md +28 -0
  56. package/templates/grid-customizer/gitignore +4 -0
  57. package/templates/grid-customizer/package.json +25 -0
  58. package/templates/grid-customizer/src/GridCustomizer.ts +28 -0
  59. package/templates/grid-customizer/src/cell-renderers.tsx +35 -0
  60. package/templates/grid-customizer/src/index.ts +4 -0
  61. package/templates/grid-customizer/src/types/grid-types.ts +30 -0
  62. package/templates/grid-customizer/src/utils/color-utils.ts +24 -0
  63. package/templates/grid-customizer/tsconfig.json +15 -0
  64. package/templates/grid-customizer/webpack.config.js +17 -0
  65. package/templates/pcf-dataset/ControlManifest.Input.xml +16 -0
  66. package/templates/pcf-dataset/README.md +21 -0
  67. package/templates/pcf-dataset/gitignore +5 -0
  68. package/templates/pcf-dataset/index.ts +39 -0
  69. package/templates/pcf-dataset/package.json +30 -0
  70. package/templates/pcf-dataset/strings/{{componentName}}.1033.resx +47 -0
  71. package/templates/pcf-dataset/tsconfig.json +8 -0
  72. package/templates/pcf-dataset/{{componentName}}Component.tsx +39 -0
  73. package/templates/pcf-field/ControlManifest.Input.xml +17 -0
  74. package/templates/pcf-field/README.md +95 -0
  75. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +24 -0
  76. package/templates/pcf-field/_variants/ValueInput.date.tsx +27 -0
  77. package/templates/pcf-field/_variants/ValueInput.number.tsx +35 -0
  78. package/templates/pcf-field/_variants/ValueInput.text.tsx +27 -0
  79. package/templates/pcf-field/gitignore +5 -0
  80. package/templates/pcf-field/index.ts +61 -0
  81. package/templates/pcf-field/package.json +30 -0
  82. package/templates/pcf-field/strings/{{componentName}}.1033.resx +47 -0
  83. package/templates/pcf-field/tsconfig.json +8 -0
  84. package/templates/pcf-field/{{componentName}}Component.tsx +35 -0
  85. package/templates/power-pages-starter/gitignore +5 -0
  86. package/templates/react-custom-page/gitignore +5 -0
  87. package/templates/{dynamics-365-starter → react-custom-page}/package.json +3 -3
  88. package/templates/react-custom-page/tools/metadata-sync/index.js +152 -0
  89. package/templates/static-web-app/README.md +36 -0
  90. package/templates/static-web-app/_variants/App.v8.tsx +32 -0
  91. package/templates/static-web-app/_variants/App.v9.tsx +31 -0
  92. package/templates/static-web-app/api/host.json +12 -0
  93. package/templates/static-web-app/api/package.json +19 -0
  94. package/templates/static-web-app/api/src/functions/hello.ts +16 -0
  95. package/templates/static-web-app/api/tsconfig.json +14 -0
  96. package/templates/static-web-app/frontend/index.html +12 -0
  97. package/templates/static-web-app/frontend/package.json +23 -0
  98. package/templates/static-web-app/frontend/src/index.tsx +8 -0
  99. package/templates/static-web-app/frontend/tsconfig.json +16 -0
  100. package/templates/static-web-app/frontend/vite.config.ts +13 -0
  101. package/templates/static-web-app/gitignore +8 -0
  102. package/templates/static-web-app/package.json +15 -0
  103. package/templates/static-web-app/staticwebapp.config.json +7 -0
  104. package/templates/teams-app/README.md +27 -0
  105. package/templates/teams-app/_variants/graph.off.ts +7 -0
  106. package/templates/teams-app/_variants/graph.on.ts +22 -0
  107. package/templates/teams-app/appPackage/manifest.json +26 -0
  108. package/templates/teams-app/gitignore +5 -0
  109. package/templates/teams-app/index.html +12 -0
  110. package/templates/teams-app/package.json +26 -0
  111. package/templates/teams-app/src/App.tsx +25 -0
  112. package/templates/teams-app/src/index.tsx +8 -0
  113. package/templates/teams-app/tsconfig.json +16 -0
  114. package/templates/teams-app/vite.config.ts +9 -0
  115. package/templates/web-resource/README.md +39 -0
  116. package/templates/web-resource/_variants/App.v8.tsx +29 -0
  117. package/templates/web-resource/_variants/App.v9.tsx +28 -0
  118. package/templates/web-resource/gitignore +5 -0
  119. package/templates/web-resource/package.json +27 -0
  120. package/templates/web-resource/public/index.html +11 -0
  121. package/templates/web-resource/src/index.tsx +10 -0
  122. package/templates/web-resource/src/services/dataverse.ts +30 -0
  123. package/templates/web-resource/tsconfig.json +15 -0
  124. package/templates/web-resource/webpack.config.js +17 -0
  125. package/dist/utils/consultingHelpers.d.ts +0 -13
  126. package/dist/utils/consultingHelpers.d.ts.map +0 -1
  127. package/dist/utils/consultingHelpers.js +0 -569
  128. package/dist/utils/consultingHelpers.js.map +0 -1
  129. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +0 -302
  130. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +0 -305
  131. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +0 -507
  132. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +0 -372
  133. package/templates/dynamics-365-starter/deployment/pipelines/README.md +0 -375
  134. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +0 -330
  135. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +0 -422
  136. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +0 -636
  137. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +0 -417
  138. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +0 -582
  139. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +0 -486
  140. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +0 -567
  141. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +0 -703
  142. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +0 -671
  143. package/templates/dynamics-365-starter/docs/team-standards/README.md +0 -273
  144. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +0 -577
  145. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +0 -359
  146. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +0 -700
  147. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +0 -736
  148. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +0 -727
  149. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +0 -758
  150. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +0 -878
  151. package/templates/dynamics-365-starter/src/client-project-template/README.md +0 -234
  152. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +0 -114
  153. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +0 -186
  154. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +0 -667
  155. package/templates/dynamics-365-starter/src/examples/README.md +0 -52
  156. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +0 -625
  157. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +0 -545
  158. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +0 -722
  159. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +0 -662
  160. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +0 -519
  161. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +0 -456
  162. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +0 -406
  163. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +0 -578
  164. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +0 -629
  165. package/templates/dynamics-365-starter/tools/entity-generator/index.js +0 -168
  166. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +0 -124
  167. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +0 -283
  168. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +0 -275
  169. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +0 -204
  170. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +0 -413
  171. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +0 -250
  172. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +0 -410
  173. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +0 -512
  174. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +0 -675
  175. /package/templates/{dynamics-365-starter → react-custom-page}/README.md +0 -0
  176. /package/templates/{dynamics-365-starter → react-custom-page}/deployment/README.md +0 -0
  177. /package/templates/{dynamics-365-starter → react-custom-page}/docs/ARCHITECTURE_OVERVIEW.md +0 -0
  178. /package/templates/{dynamics-365-starter → react-custom-page}/docs/BEST_PRACTICES.md +0 -0
  179. /package/templates/{dynamics-365-starter → react-custom-page}/docs/MIGRATION_GUIDE.md +0 -0
  180. /package/templates/{dynamics-365-starter → react-custom-page}/public/index.html +0 -0
  181. /package/templates/{dynamics-365-starter → react-custom-page}/scripts/custom-build.js +0 -0
  182. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountForm.css +0 -0
  183. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountForm.tsx +0 -0
  184. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountManagement.css +0 -0
  185. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/AccountManagement.tsx +0 -0
  186. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactForm.css +0 -0
  187. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactForm.tsx +0 -0
  188. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactManagement.css +0 -0
  189. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/ContactManagement.tsx +0 -0
  190. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LogDialog.tsx +0 -0
  191. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingContext.tsx +0 -0
  192. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingDebugPanel.css +0 -0
  193. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingDebugPanel.tsx +0 -0
  194. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/LoggingProvider.tsx +0 -0
  195. /package/templates/{dynamics-365-starter → react-custom-page}/src/components/Logging/logger.ts +0 -0
  196. /package/templates/{dynamics-365-starter → react-custom-page}/src/constants/account.ts +0 -0
  197. /package/templates/{dynamics-365-starter → react-custom-page}/src/constants/contact.ts +0 -0
  198. /package/templates/{dynamics-365-starter → react-custom-page}/src/index.tsx +0 -0
  199. /package/templates/{dynamics-365-starter → react-custom-page}/src/models/Account.ts +0 -0
  200. /package/templates/{dynamics-365-starter → react-custom-page}/src/models/BaseEntity.ts +0 -0
  201. /package/templates/{dynamics-365-starter → react-custom-page}/src/models/Contact.ts +0 -0
  202. /package/templates/{dynamics-365-starter → react-custom-page}/src/pcf/ContactControlWrapper.tsx +0 -0
  203. /package/templates/{dynamics-365-starter → react-custom-page}/src/pcf/MultiEntityControlWrapper.tsx +0 -0
  204. /package/templates/{dynamics-365-starter → react-custom-page}/src/providers/DynamicsProvider.tsx +0 -0
  205. /package/templates/{dynamics-365-starter → react-custom-page}/src/services/MockApiService.ts +0 -0
  206. /package/templates/{dynamics-365-starter → react-custom-page}/src/services/ServiceFactory.ts +0 -0
  207. /package/templates/{dynamics-365-starter → react-custom-page}/src/services/XrmApiService.ts +0 -0
  208. /package/templates/{dynamics-365-starter → react-custom-page}/src/styles/index.css +0 -0
  209. /package/templates/{dynamics-365-starter → react-custom-page}/tsconfig.json +0 -0
  210. /package/templates/{dynamics-365-starter → react-custom-page}/webpack.config.js +0 -0
@@ -1,578 +0,0 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import {
3
- Stack,
4
- Text,
5
- Pivot,
6
- PivotItem,
7
- DetailsList,
8
- IColumn,
9
- SelectionMode,
10
- CommandBar,
11
- ICommandBarItemProps,
12
- MessageBar,
13
- MessageBarType,
14
- Spinner,
15
- SpinnerSize,
16
- Panel,
17
- PanelType,
18
- SearchBox,
19
- Breadcrumb,
20
- IBreadcrumbItem,
21
- } from '@fluentui/react';
22
- import { BaseEntity } from '../models/BaseEntity';
23
- import { useDynamicsApi } from '../providers/DynamicsProvider';
24
- import { Logger } from '../components/Logging/logger';
25
-
26
- interface RelatedEntityConfig<T extends BaseEntity> {
27
- key: string;
28
- title: string;
29
- entityLogicalName: string;
30
- entityClass: any;
31
- columns: IColumn[];
32
- fetchXmlTemplate: string; // Should include {{parentId}} placeholder
33
- relationshipField: string;
34
- allowCreate?: boolean;
35
- allowEdit?: boolean;
36
- allowDelete?: boolean;
37
- renderForm?: (
38
- entity: T | null,
39
- onSave: (entity: T) => void,
40
- onCancel: () => void
41
- ) => React.ReactNode;
42
- customCommands?: ICommandBarItemProps[];
43
- }
44
-
45
- interface RelatedEntitiesPageProps<TParent extends BaseEntity> {
46
- parentEntity: TParent;
47
- parentTitle: string;
48
- relatedEntityConfigs: RelatedEntityConfig<any>[];
49
- onBack?: () => void;
50
- breadcrumbItems?: IBreadcrumbItem[];
51
- enableSearch?: boolean;
52
- }
53
-
54
- /**
55
- * Generic related entities page template for Dynamics 365
56
- * Displays master-detail relationships with tabbed navigation
57
- */
58
- export function RelatedEntitiesPage<TParent extends BaseEntity>({
59
- parentEntity,
60
- parentTitle,
61
- relatedEntityConfigs,
62
- onBack,
63
- breadcrumbItems = [],
64
- enableSearch = true,
65
- }: RelatedEntitiesPageProps<TParent>) {
66
- const { apiService } = useDynamicsApi();
67
- const [selectedTab, setSelectedTab] = useState(
68
- relatedEntityConfigs[0]?.key || ''
69
- );
70
- const [relatedData, setRelatedData] = useState<{ [key: string]: any[] }>({});
71
- const [filteredData, setFilteredData] = useState<{ [key: string]: any[] }>(
72
- {}
73
- );
74
- const [loading, setLoading] = useState<{ [key: string]: boolean }>({});
75
- const [error, setError] = useState<string | null>(null);
76
- const [searchQuery, setSearchQuery] = useState('');
77
- const [selectedItems, setSelectedItems] = useState<{ [key: string]: any }>(
78
- {}
79
- );
80
- const [showPanel, setShowPanel] = useState(false);
81
- const [panelConfig, setPanelConfig] =
82
- useState<RelatedEntityConfig<any> | null>(null);
83
- const [isEditing, setIsEditing] = useState(false);
84
- const [refreshTriggers, setRefreshTriggers] = useState<{
85
- [key: string]: number;
86
- }>({});
87
-
88
- // Load related entities for a specific tab
89
- const loadRelatedEntities = useCallback(
90
- async (config: RelatedEntityConfig<any>) => {
91
- if (!apiService) {
92
- setError('API service not available');
93
- return;
94
- }
95
-
96
- try {
97
- setLoading((prev) => ({ ...prev, [config.key]: true }));
98
- setError(null);
99
- Logger.log(`Loading ${config.title}...`);
100
-
101
- const parentId =
102
- (parentEntity as any).id ||
103
- (parentEntity as any)[
104
- Object.keys(parentEntity).find((key) => key.includes('id')) || ''
105
- ];
106
- const fetchXml = config.fetchXmlTemplate.replace(
107
- '{{parentId}}',
108
- parentId
109
- );
110
-
111
- const result = await apiService.retrieveMultipleRecords(
112
- config.entityLogicalName,
113
- fetchXml
114
- );
115
- const entities = (result.entities || []).map(
116
- (data: any) => new config.entityClass(data)
117
- );
118
-
119
- setRelatedData((prev) => ({ ...prev, [config.key]: entities }));
120
- setFilteredData((prev) => ({ ...prev, [config.key]: entities }));
121
-
122
- Logger.log(`Loaded ${entities.length} ${config.title}`);
123
- } catch (err) {
124
- const errorMessage =
125
- err instanceof Error ? err.message : `Failed to load ${config.title}`;
126
- setError(errorMessage);
127
- Logger.error(
128
- `Error loading ${config.title}:`,
129
- 'RelatedEntitiesPage',
130
- err
131
- );
132
- } finally {
133
- setLoading((prev) => ({ ...prev, [config.key]: false }));
134
- }
135
- },
136
- [apiService, parentEntity]
137
- );
138
-
139
- // Load data when tab changes or refresh triggers
140
- useEffect(() => {
141
- const config = relatedEntityConfigs.find((c) => c.key === selectedTab);
142
- if (config) {
143
- loadRelatedEntities(config);
144
- }
145
- }, [selectedTab, relatedEntityConfigs, loadRelatedEntities, refreshTriggers]);
146
-
147
- // Handle search
148
- useEffect(() => {
149
- if (!enableSearch) return;
150
-
151
- const newFilteredData = { ...relatedData };
152
-
153
- if (searchQuery.trim()) {
154
- for (const [key, entities] of Object.entries(relatedData)) {
155
- newFilteredData[key] = entities.filter((entity) =>
156
- Object.values(entity).some(
157
- (value) =>
158
- typeof value === 'string' &&
159
- value.toLowerCase().includes(searchQuery.toLowerCase())
160
- )
161
- );
162
- }
163
- }
164
-
165
- setFilteredData(newFilteredData);
166
- }, [relatedData, searchQuery, enableSearch]);
167
-
168
- // Handle item selection
169
- const handleItemSelection = (config: RelatedEntityConfig<any>, item: any) => {
170
- setSelectedItems((prev) => ({ ...prev, [config.key]: item }));
171
- };
172
-
173
- // Handle refresh
174
- const handleRefresh = (configKey: string) => {
175
- setRefreshTriggers((prev) => ({
176
- ...prev,
177
- [configKey]: (prev[configKey] || 0) + 1,
178
- }));
179
- };
180
-
181
- // Handle new entity
182
- const handleNew = (config: RelatedEntityConfig<any>) => {
183
- setPanelConfig(config);
184
- setSelectedItems((prev) => ({ ...prev, [config.key]: null }));
185
- setIsEditing(false);
186
- setShowPanel(true);
187
- };
188
-
189
- // Handle edit entity
190
- const handleEdit = (config: RelatedEntityConfig<any>) => {
191
- const selectedItem = selectedItems[config.key];
192
- if (selectedItem) {
193
- setPanelConfig(config);
194
- setIsEditing(true);
195
- setShowPanel(true);
196
- }
197
- };
198
-
199
- // Handle delete entity
200
- const handleDelete = async (config: RelatedEntityConfig<any>) => {
201
- const selectedItem = selectedItems[config.key];
202
- if (!selectedItem || !apiService) {
203
- setError('Selected item or API service not available');
204
- return;
205
- }
206
-
207
- try {
208
- // Use the entity class static method or apiService directly
209
- if (typeof selectedItem.delete === 'function') {
210
- await selectedItem.delete(apiService);
211
- } else {
212
- // Fallback to direct API call
213
- const entityId =
214
- selectedItem.id || selectedItem[`${config.entityLogicalName}id`];
215
- if (entityId) {
216
- await apiService.deleteRecord(config.entityLogicalName, entityId);
217
- } else {
218
- throw new Error('Could not determine entity ID for deletion');
219
- }
220
- }
221
-
222
- setSelectedItems((prev) => ({ ...prev, [config.key]: null }));
223
- handleRefresh(config.key);
224
- Logger.log(`${config.title} item deleted successfully`);
225
- } catch (err) {
226
- const errorMessage =
227
- err instanceof Error
228
- ? err.message
229
- : `Failed to delete ${config.title} item`;
230
- setError(errorMessage);
231
- Logger.error(
232
- `Error deleting ${config.title} item:`,
233
- 'RelatedEntitiesPage',
234
- err
235
- );
236
- }
237
- };
238
-
239
- // Handle save
240
- const handleSave = async (entity: any) => {
241
- if (!panelConfig || !apiService) {
242
- setError('Panel configuration or API service not available');
243
- return;
244
- }
245
-
246
- try {
247
- if (isEditing) {
248
- // Update existing entity
249
- if (typeof entity.update === 'function') {
250
- await entity.update(apiService);
251
- } else {
252
- // Fallback to direct API call
253
- const entityId =
254
- entity.id || entity[`${panelConfig.entityLogicalName}id`];
255
- if (entityId) {
256
- const entityData = { ...entity };
257
- delete entityData.id; // Remove id from update data
258
- await apiService.updateRecord(
259
- panelConfig.entityLogicalName,
260
- entityId,
261
- entityData
262
- );
263
- } else {
264
- throw new Error('Could not determine entity ID for update');
265
- }
266
- }
267
- } else {
268
- // Create new entity
269
- const parentId =
270
- (parentEntity as any).id ||
271
- (parentEntity as any)[
272
- Object.keys(parentEntity).find((key) => key.includes('id')) || ''
273
- ];
274
- entity[panelConfig.relationshipField] = parentId;
275
-
276
- if (typeof entity.constructor.create === 'function') {
277
- await entity.constructor.create(apiService, entity);
278
- } else {
279
- // Fallback to direct API call
280
- await apiService.createRecord(panelConfig.entityLogicalName, entity);
281
- }
282
- }
283
-
284
- setShowPanel(false);
285
- setPanelConfig(null);
286
- setIsEditing(false);
287
- handleRefresh(panelConfig.key);
288
-
289
- Logger.log(`${panelConfig.title} saved successfully`);
290
- } catch (err) {
291
- const errorMessage =
292
- err instanceof Error
293
- ? err.message
294
- : `Failed to save ${panelConfig.title}`;
295
- setError(errorMessage);
296
- Logger.error(
297
- `Error saving ${panelConfig.title}:`,
298
- 'RelatedEntitiesPage',
299
- err
300
- );
301
- }
302
- };
303
-
304
- // Handle panel dismiss
305
- const handlePanelDismiss = () => {
306
- setShowPanel(false);
307
- setPanelConfig(null);
308
- setIsEditing(false);
309
- };
310
-
311
- // Get current config and data
312
- const currentConfig = relatedEntityConfigs.find((c) => c.key === selectedTab);
313
- const currentData = filteredData[selectedTab] || [];
314
- const currentSelectedItem = selectedItems[selectedTab];
315
- const isCurrentLoading = loading[selectedTab] || false;
316
-
317
- // Create command bar for current tab
318
- const createCommandBar = (
319
- config: RelatedEntityConfig<any>
320
- ): ICommandBarItemProps[] => {
321
- const items: ICommandBarItemProps[] = [
322
- {
323
- key: 'refresh',
324
- text: 'Refresh',
325
- iconProps: { iconName: 'Refresh' },
326
- onClick: () => handleRefresh(config.key),
327
- },
328
- ];
329
-
330
- if (config.allowCreate !== false) {
331
- items.unshift({
332
- key: 'new',
333
- text: `New ${config.title}`,
334
- iconProps: { iconName: 'Add' },
335
- onClick: () => handleNew(config),
336
- });
337
- }
338
-
339
- if (config.allowEdit !== false && currentSelectedItem) {
340
- items.push({
341
- key: 'edit',
342
- text: 'Edit',
343
- iconProps: { iconName: 'Edit' },
344
- onClick: () => handleEdit(config),
345
- });
346
- }
347
-
348
- if (config.allowDelete !== false && currentSelectedItem) {
349
- items.push({
350
- key: 'delete',
351
- text: 'Delete',
352
- iconProps: { iconName: 'Delete' },
353
- onClick: () => {
354
- handleDelete(config);
355
- },
356
- });
357
- }
358
-
359
- return [...items, ...(config.customCommands || [])];
360
- };
361
-
362
- // Enhanced columns with click handlers
363
- const createEnhancedColumns = (
364
- config: RelatedEntityConfig<any>
365
- ): IColumn[] => {
366
- return config.columns.map((column) => ({
367
- ...column,
368
- onRender:
369
- column.onRender ||
370
- ((item: any) => {
371
- const value = item[column.fieldName || ''];
372
- if (column.key === config.columns[0].key) {
373
- // Make first column clickable
374
- return (
375
- <span
376
- style={{ color: '#0078d4', cursor: 'pointer', fontWeight: 500 }}
377
- onClick={() => handleItemSelection(config, item)}
378
- >
379
- {value || ''}
380
- </span>
381
- );
382
- }
383
- return <span>{value || ''}</span>;
384
- }),
385
- }));
386
- };
387
-
388
- // Default breadcrumb
389
- const defaultBreadcrumbItems: IBreadcrumbItem[] = [
390
- { text: parentTitle, key: 'parent', onClick: onBack },
391
- { text: 'Related Records', key: 'related', isCurrentItem: true },
392
- ];
393
-
394
- const finalBreadcrumbItems =
395
- breadcrumbItems.length > 0 ? breadcrumbItems : defaultBreadcrumbItems;
396
-
397
- return (
398
- <div style={{ padding: '20px', maxWidth: '100%' }}>
399
- <Stack tokens={{ childrenGap: 20 }}>
400
- {/* Breadcrumb */}
401
- {onBack && <Breadcrumb items={finalBreadcrumbItems} />}
402
-
403
- {/* Header */}
404
- <Stack>
405
- <Text variant="xxLarge" style={{ fontWeight: 600, color: '#323130' }}>
406
- {(parentEntity as any).name || parentTitle} - Related Records
407
- </Text>
408
- <Text variant="medium" style={{ color: '#605e5c' }}>
409
- Manage related entities and relationships
410
- </Text>
411
- </Stack>
412
-
413
- {/* Error Message */}
414
- {error && (
415
- <MessageBar
416
- messageBarType={MessageBarType.error}
417
- onDismiss={() => setError(null)}
418
- >
419
- {error}
420
- </MessageBar>
421
- )}
422
-
423
- {/* Search */}
424
- {enableSearch && (
425
- <SearchBox
426
- placeholder="Search related records..."
427
- value={searchQuery}
428
- onChange={(_, newValue) => setSearchQuery(newValue || '')}
429
- style={{ maxWidth: 400 }}
430
- />
431
- )}
432
-
433
- {/* Tabs and Content */}
434
- <Pivot
435
- selectedKey={selectedTab}
436
- onLinkClick={(item) => setSelectedTab(item?.props.itemKey || '')}
437
- >
438
- {relatedEntityConfigs.map((config) => (
439
- <PivotItem
440
- key={config.key}
441
- headerText={`${config.title} (${(filteredData[config.key] || []).length})`}
442
- itemKey={config.key}
443
- >
444
- <Stack tokens={{ childrenGap: 16 }} style={{ paddingTop: 20 }}>
445
- {/* Command Bar */}
446
- <CommandBar items={createCommandBar(config)} />
447
-
448
- {/* Content */}
449
- {isCurrentLoading ? (
450
- <div
451
- style={{
452
- display: 'flex',
453
- justifyContent: 'center',
454
- alignItems: 'center',
455
- minHeight: 200,
456
- flexDirection: 'column',
457
- }}
458
- >
459
- <Spinner
460
- size={SpinnerSize.large}
461
- label={`Loading ${config.title}...`}
462
- />
463
- </div>
464
- ) : currentData.length === 0 ? (
465
- <div
466
- style={{
467
- textAlign: 'center',
468
- padding: '40px 20px',
469
- color: '#605e5c',
470
- border: '1px solid #edebe9',
471
- borderRadius: 4,
472
- }}
473
- >
474
- <div
475
- style={{
476
- fontSize: 48,
477
- marginBottom: 16,
478
- color: '#a19f9d',
479
- }}
480
- >
481
- 📋
482
- </div>
483
- <Text
484
- variant="large"
485
- style={{
486
- fontWeight: 600,
487
- marginBottom: 8,
488
- color: '#323130',
489
- }}
490
- >
491
- {searchQuery
492
- ? 'No matching records found'
493
- : `No ${config.title} found`}
494
- </Text>
495
- <Text variant="medium">
496
- {searchQuery
497
- ? 'Try adjusting your search criteria'
498
- : config.allowCreate !== false
499
- ? `Click "New ${config.title}" to get started`
500
- : 'No records available'}
501
- </Text>
502
- </div>
503
- ) : (
504
- <DetailsList
505
- items={currentData}
506
- columns={createEnhancedColumns(config)}
507
- setKey={config.key}
508
- layoutMode={0}
509
- selectionMode={SelectionMode.single}
510
- onActiveItemChanged={(item) =>
511
- handleItemSelection(config, item)
512
- }
513
- isHeaderVisible={true}
514
- />
515
- )}
516
- </Stack>
517
- </PivotItem>
518
- ))}
519
- </Pivot>
520
-
521
- {/* Form Panel */}
522
- {panelConfig && (
523
- <Panel
524
- headerText={
525
- isEditing
526
- ? `Edit ${panelConfig.title}`
527
- : `New ${panelConfig.title}`
528
- }
529
- isOpen={showPanel}
530
- onDismiss={handlePanelDismiss}
531
- type={PanelType.medium}
532
- closeButtonAriaLabel="Close"
533
- >
534
- <div style={{ paddingTop: 20 }}>
535
- {panelConfig.renderForm ? (
536
- panelConfig.renderForm(
537
- isEditing ? selectedItems[panelConfig.key] : null,
538
- handleSave,
539
- handlePanelDismiss
540
- )
541
- ) : (
542
- <div
543
- style={{ padding: 20, textAlign: 'center', color: '#605e5c' }}
544
- >
545
- <Text>Form not configured for {panelConfig.title}</Text>
546
- </div>
547
- )}
548
- </div>
549
- </Panel>
550
- )}
551
- </Stack>
552
- </div>
553
- );
554
- }
555
-
556
- // Helper function to create related entity FetchXML
557
- export function createRelatedEntityFetchXml(
558
- entityName: string,
559
- attributes: string[],
560
- relationshipField: string,
561
- orderBy?: string
562
- ): string {
563
- const attributeElements = attributes
564
- .map((attr) => `<attribute name="${attr}" />`)
565
- .join('\n ');
566
- const orderElement = orderBy ? `<order attribute="${orderBy}" />` : '';
567
-
568
- return `
569
- <fetch>
570
- <entity name="${entityName}">
571
- ${attributeElements}
572
- ${orderElement}
573
- <filter>
574
- <condition attribute="${relationshipField}" operator="eq" value="{{parentId}}" />
575
- </filter>
576
- </entity>
577
- </fetch>`;
578
- }