@khester/create-dynamics-app 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-dynamics-app.js +1 -1
- package/dist/index.js +140 -15
- package/dist/index.js.map +1 -1
- package/dist/utils/consultingHelpers.d.ts +13 -0
- package/dist/utils/consultingHelpers.d.ts.map +1 -0
- package/dist/utils/consultingHelpers.js +569 -0
- package/dist/utils/consultingHelpers.js.map +1 -0
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js.map +1 -1
- package/dist/utils/initGit.d.ts.map +1 -1
- package/dist/utils/initGit.js.map +1 -1
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +3 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/dist/utils/updatePackageJson.d.ts +1 -1
- package/dist/utils/updatePackageJson.d.ts.map +1 -1
- package/dist/utils/updatePackageJson.js +11 -1
- package/dist/utils/updatePackageJson.js.map +1 -1
- package/package.json +1 -1
- package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
- package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
- package/templates/dynamics-365-starter/README.md +566 -137
- package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
- package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
- package/templates/dynamics-365-starter/deployment/README.md +484 -0
- package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
- package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
- package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
- package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
- package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
- package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
- package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
- package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
- package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
- package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
- package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
- package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
- package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
- package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
- package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
- package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
- package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
- package/templates/dynamics-365-starter/package.json +22 -1
- package/templates/dynamics-365-starter/public/index.html +8 -11
- package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
- package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
- package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
- package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
- package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
- package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
- package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
- package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
- package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
- package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
- package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
- package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
- package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
- package/templates/dynamics-365-starter/src/examples/README.md +52 -0
- package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
- package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
- package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
- package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
- package/templates/dynamics-365-starter/src/index.tsx +107 -19
- package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
- package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
- package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
- package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
- package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
- package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
- package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
- package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
- package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
- package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
- package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
- package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
- package/templates/dynamics-365-starter/src/styles/index.css +74 -7
- package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
- package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
- package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
- package/templates/dynamics-365-starter/tsconfig.json +11 -8
- package/templates/dynamics-365-starter/webpack.config.js +8 -9
- package/templates/power-pages-starter/README.md +7 -1
- package/templates/power-pages-starter/public/index.html +8 -11
- package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
- package/templates/power-pages-starter/src/index.tsx +3 -3
- package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
- package/templates/power-pages-starter/tsconfig.json +3 -9
- package/templates/power-pages-starter/webpack.config.js +8 -3
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Stack,
|
|
4
|
+
Text,
|
|
5
|
+
CommandBar,
|
|
6
|
+
ICommandBarItemProps,
|
|
7
|
+
MessageBar,
|
|
8
|
+
MessageBarType,
|
|
9
|
+
Spinner,
|
|
10
|
+
SpinnerSize,
|
|
11
|
+
Breadcrumb,
|
|
12
|
+
IBreadcrumbItem,
|
|
13
|
+
Panel,
|
|
14
|
+
PanelType,
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogType,
|
|
17
|
+
DialogFooter,
|
|
18
|
+
PrimaryButton,
|
|
19
|
+
DefaultButton,
|
|
20
|
+
Pivot,
|
|
21
|
+
PivotItem,
|
|
22
|
+
} from '@fluentui/react';
|
|
23
|
+
import { BaseEntity } from '../models/BaseEntity';
|
|
24
|
+
import { useDynamicsApi } from '../providers/DynamicsProvider';
|
|
25
|
+
import { Logger } from '../components/Logging/logger';
|
|
26
|
+
|
|
27
|
+
interface EntityDetailPageProps<T extends BaseEntity> {
|
|
28
|
+
entityId: string;
|
|
29
|
+
title: string;
|
|
30
|
+
entityLogicalName: string;
|
|
31
|
+
entityClass: any; // Constructor for the entity
|
|
32
|
+
fetchXmlTemplate: string;
|
|
33
|
+
renderForm: (
|
|
34
|
+
entity: T | null,
|
|
35
|
+
onSave: (entity: T) => void,
|
|
36
|
+
readOnly?: boolean
|
|
37
|
+
) => React.ReactNode;
|
|
38
|
+
renderRelatedEntities?: (entity: T) => React.ReactNode;
|
|
39
|
+
renderAuditHistory?: (entity: T) => React.ReactNode;
|
|
40
|
+
onBack?: () => void;
|
|
41
|
+
onEdit?: (entity: T) => void;
|
|
42
|
+
onDelete?: (entity: T) => void;
|
|
43
|
+
customCommands?: ICommandBarItemProps[];
|
|
44
|
+
breadcrumbItems?: IBreadcrumbItem[];
|
|
45
|
+
enableTabs?: boolean;
|
|
46
|
+
readOnly?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Generic entity detail page template for Dynamics 365
|
|
51
|
+
* Supports viewing, editing, related entities, and audit history
|
|
52
|
+
*/
|
|
53
|
+
export function EntityDetailPage<T extends BaseEntity>({
|
|
54
|
+
entityId,
|
|
55
|
+
title,
|
|
56
|
+
entityLogicalName,
|
|
57
|
+
entityClass,
|
|
58
|
+
fetchXmlTemplate,
|
|
59
|
+
renderForm,
|
|
60
|
+
renderRelatedEntities,
|
|
61
|
+
renderAuditHistory,
|
|
62
|
+
onBack,
|
|
63
|
+
onEdit,
|
|
64
|
+
onDelete,
|
|
65
|
+
customCommands = [],
|
|
66
|
+
breadcrumbItems = [],
|
|
67
|
+
enableTabs = true,
|
|
68
|
+
readOnly = false,
|
|
69
|
+
}: EntityDetailPageProps<T>) {
|
|
70
|
+
const { apiService } = useDynamicsApi();
|
|
71
|
+
const [entity, setEntity] = useState<T | null>(null);
|
|
72
|
+
const [loading, setLoading] = useState(true);
|
|
73
|
+
const [error, setError] = useState<string | null>(null);
|
|
74
|
+
const [saving, setSaving] = useState(false);
|
|
75
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
76
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
77
|
+
const [showEditPanel, setShowEditPanel] = useState(false);
|
|
78
|
+
const [selectedTab, setSelectedTab] = useState('details');
|
|
79
|
+
|
|
80
|
+
// Load entity
|
|
81
|
+
const loadEntity = async () => {
|
|
82
|
+
if (!apiService) {
|
|
83
|
+
setError('API service not available');
|
|
84
|
+
setLoading(false);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
setLoading(true);
|
|
90
|
+
setError(null);
|
|
91
|
+
Logger.log(`Loading ${title} with ID: ${entityId}`);
|
|
92
|
+
|
|
93
|
+
const fetchXml = fetchXmlTemplate.replace('{{entityId}}', entityId);
|
|
94
|
+
const result = await apiService.retrieveMultipleRecords(
|
|
95
|
+
entityLogicalName,
|
|
96
|
+
fetchXml
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (result.entities && result.entities.length > 0) {
|
|
100
|
+
const entityData = new entityClass(result.entities[0]);
|
|
101
|
+
setEntity(entityData);
|
|
102
|
+
Logger.log(`${title} loaded successfully`);
|
|
103
|
+
} else {
|
|
104
|
+
setError(`${title} not found`);
|
|
105
|
+
Logger.warn(`${title} not found with ID: ${entityId}`);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const errorMessage =
|
|
109
|
+
err instanceof Error ? err.message : `Failed to load ${title}`;
|
|
110
|
+
setError(errorMessage);
|
|
111
|
+
Logger.error(`Error loading ${title}:`, 'EntityDetailPage', err);
|
|
112
|
+
} finally {
|
|
113
|
+
setLoading(false);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (entityId) {
|
|
119
|
+
loadEntity();
|
|
120
|
+
}
|
|
121
|
+
}, [entityId]);
|
|
122
|
+
|
|
123
|
+
// Handle save
|
|
124
|
+
const handleSave = async (updatedEntity: T) => {
|
|
125
|
+
if (!apiService) {
|
|
126
|
+
setError('API service not available');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
setSaving(true);
|
|
132
|
+
Logger.log(`Saving ${title}...`);
|
|
133
|
+
|
|
134
|
+
// Use the entity class static method or apiService directly
|
|
135
|
+
if (typeof (updatedEntity as any).update === 'function') {
|
|
136
|
+
await (updatedEntity as any).update(apiService);
|
|
137
|
+
} else {
|
|
138
|
+
// Fallback to direct API call
|
|
139
|
+
const entityData = { ...updatedEntity };
|
|
140
|
+
delete (entityData as any).id; // Remove id from update data
|
|
141
|
+
await apiService.updateRecord(entityLogicalName, entityId, entityData);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
setEntity(updatedEntity);
|
|
145
|
+
setIsEditing(false);
|
|
146
|
+
setShowEditPanel(false);
|
|
147
|
+
|
|
148
|
+
Logger.log(`${title} saved successfully`);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const errorMessage =
|
|
151
|
+
err instanceof Error ? err.message : `Failed to save ${title}`;
|
|
152
|
+
setError(errorMessage);
|
|
153
|
+
Logger.error(`Error saving ${title}:`, 'EntityDetailPage', err);
|
|
154
|
+
} finally {
|
|
155
|
+
setSaving(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Handle edit
|
|
160
|
+
const handleEdit = () => {
|
|
161
|
+
if (entity) {
|
|
162
|
+
if (onEdit) {
|
|
163
|
+
onEdit(entity);
|
|
164
|
+
} else {
|
|
165
|
+
setIsEditing(true);
|
|
166
|
+
setShowEditPanel(true);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Handle delete
|
|
172
|
+
const handleDelete = async () => {
|
|
173
|
+
if (!entity || !apiService) {
|
|
174
|
+
setError('Entity or API service not available');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (onDelete) {
|
|
179
|
+
onDelete(entity);
|
|
180
|
+
} else {
|
|
181
|
+
try {
|
|
182
|
+
// Use the entity class static method or apiService directly
|
|
183
|
+
if (typeof (entity as any).delete === 'function') {
|
|
184
|
+
await (entity as any).delete(apiService);
|
|
185
|
+
} else {
|
|
186
|
+
// Fallback to direct API call
|
|
187
|
+
await apiService.deleteRecord(entityLogicalName, entityId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
setShowDeleteDialog(false);
|
|
191
|
+
if (onBack) {
|
|
192
|
+
onBack();
|
|
193
|
+
}
|
|
194
|
+
Logger.log(`${title} deleted successfully`);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const errorMessage =
|
|
197
|
+
err instanceof Error ? err.message : `Failed to delete ${title}`;
|
|
198
|
+
setError(errorMessage);
|
|
199
|
+
Logger.error(`Error deleting ${title}:`, 'EntityDetailPage', err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Handle refresh
|
|
205
|
+
const handleRefresh = () => {
|
|
206
|
+
loadEntity();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Command bar items
|
|
210
|
+
const commandBarItems: ICommandBarItemProps[] = [
|
|
211
|
+
{
|
|
212
|
+
key: 'edit',
|
|
213
|
+
text: 'Edit',
|
|
214
|
+
iconProps: { iconName: 'Edit' },
|
|
215
|
+
disabled: !entity || readOnly,
|
|
216
|
+
onClick: handleEdit,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
key: 'delete',
|
|
220
|
+
text: 'Delete',
|
|
221
|
+
iconProps: { iconName: 'Delete' },
|
|
222
|
+
disabled: !entity || readOnly,
|
|
223
|
+
onClick: () => setShowDeleteDialog(true),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: 'refresh',
|
|
227
|
+
text: 'Refresh',
|
|
228
|
+
iconProps: { iconName: 'Refresh' },
|
|
229
|
+
onClick: handleRefresh,
|
|
230
|
+
},
|
|
231
|
+
...customCommands,
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Far command bar items
|
|
235
|
+
const farCommandBarItems: ICommandBarItemProps[] = onBack
|
|
236
|
+
? [
|
|
237
|
+
{
|
|
238
|
+
key: 'back',
|
|
239
|
+
text: 'Back',
|
|
240
|
+
iconProps: { iconName: 'Back' },
|
|
241
|
+
onClick: onBack,
|
|
242
|
+
},
|
|
243
|
+
]
|
|
244
|
+
: [];
|
|
245
|
+
|
|
246
|
+
// Default breadcrumb
|
|
247
|
+
const defaultBreadcrumbItems: IBreadcrumbItem[] = [
|
|
248
|
+
{ text: title, key: 'list', onClick: onBack },
|
|
249
|
+
{
|
|
250
|
+
text: entity ? (entity as any).name || 'Details' : 'Loading...',
|
|
251
|
+
key: 'details',
|
|
252
|
+
isCurrentItem: true,
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const finalBreadcrumbItems =
|
|
257
|
+
breadcrumbItems.length > 0 ? breadcrumbItems : defaultBreadcrumbItems;
|
|
258
|
+
|
|
259
|
+
if (loading) {
|
|
260
|
+
return (
|
|
261
|
+
<div
|
|
262
|
+
style={{
|
|
263
|
+
display: 'flex',
|
|
264
|
+
justifyContent: 'center',
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
minHeight: 400,
|
|
267
|
+
flexDirection: 'column',
|
|
268
|
+
padding: '20px',
|
|
269
|
+
}}
|
|
270
|
+
>
|
|
271
|
+
<Spinner size={SpinnerSize.large} label={`Loading ${title}...`} />
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (error || !entity) {
|
|
277
|
+
return (
|
|
278
|
+
<div style={{ padding: '20px' }}>
|
|
279
|
+
<Stack tokens={{ childrenGap: 20 }}>
|
|
280
|
+
{onBack && <Breadcrumb items={finalBreadcrumbItems} />}
|
|
281
|
+
|
|
282
|
+
<MessageBar messageBarType={MessageBarType.error}>
|
|
283
|
+
{error || `${title} not found`}
|
|
284
|
+
</MessageBar>
|
|
285
|
+
|
|
286
|
+
<div
|
|
287
|
+
style={{
|
|
288
|
+
textAlign: 'center',
|
|
289
|
+
padding: '40px 20px',
|
|
290
|
+
color: '#605e5c',
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<div style={{ fontSize: 48, marginBottom: 16, color: '#d13438' }}>
|
|
294
|
+
⚠️
|
|
295
|
+
</div>
|
|
296
|
+
<Text
|
|
297
|
+
variant="large"
|
|
298
|
+
style={{ fontWeight: 600, marginBottom: 8, color: '#323130' }}
|
|
299
|
+
>
|
|
300
|
+
{title} Not Found
|
|
301
|
+
</Text>
|
|
302
|
+
<Text variant="medium">
|
|
303
|
+
The requested {title.toLowerCase()} could not be found or you may
|
|
304
|
+
not have permission to view it.
|
|
305
|
+
</Text>
|
|
306
|
+
{onBack && (
|
|
307
|
+
<DefaultButton
|
|
308
|
+
text="Go Back"
|
|
309
|
+
onClick={onBack}
|
|
310
|
+
style={{ marginTop: 20 }}
|
|
311
|
+
/>
|
|
312
|
+
)}
|
|
313
|
+
</div>
|
|
314
|
+
</Stack>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
<div style={{ padding: '20px', maxWidth: '100%' }}>
|
|
321
|
+
<Stack tokens={{ childrenGap: 20 }}>
|
|
322
|
+
{/* Breadcrumb */}
|
|
323
|
+
{onBack && <Breadcrumb items={finalBreadcrumbItems} />}
|
|
324
|
+
|
|
325
|
+
{/* Header */}
|
|
326
|
+
<Stack
|
|
327
|
+
horizontal
|
|
328
|
+
horizontalAlign="space-between"
|
|
329
|
+
verticalAlign="center"
|
|
330
|
+
>
|
|
331
|
+
<Stack>
|
|
332
|
+
<Text
|
|
333
|
+
variant="xxLarge"
|
|
334
|
+
style={{ fontWeight: 600, color: '#323130' }}
|
|
335
|
+
>
|
|
336
|
+
{(entity as any).name || title}
|
|
337
|
+
</Text>
|
|
338
|
+
<Text variant="medium" style={{ color: '#605e5c' }}>
|
|
339
|
+
{title} • {entityId}
|
|
340
|
+
</Text>
|
|
341
|
+
</Stack>
|
|
342
|
+
</Stack>
|
|
343
|
+
|
|
344
|
+
{/* Error Message */}
|
|
345
|
+
{error && (
|
|
346
|
+
<MessageBar
|
|
347
|
+
messageBarType={MessageBarType.error}
|
|
348
|
+
onDismiss={() => setError(null)}
|
|
349
|
+
>
|
|
350
|
+
{error}
|
|
351
|
+
</MessageBar>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Command Bar */}
|
|
355
|
+
<CommandBar items={commandBarItems} farItems={farCommandBarItems} />
|
|
356
|
+
|
|
357
|
+
{/* Content */}
|
|
358
|
+
{enableTabs && (renderRelatedEntities || renderAuditHistory) ? (
|
|
359
|
+
<Pivot
|
|
360
|
+
selectedKey={selectedTab}
|
|
361
|
+
onLinkClick={(item) =>
|
|
362
|
+
setSelectedTab(item?.props.itemKey || 'details')
|
|
363
|
+
}
|
|
364
|
+
>
|
|
365
|
+
<PivotItem headerText="Details" itemKey="details">
|
|
366
|
+
<div style={{ paddingTop: 20 }}>
|
|
367
|
+
{renderForm(entity, handleSave, !isEditing)}
|
|
368
|
+
</div>
|
|
369
|
+
</PivotItem>
|
|
370
|
+
|
|
371
|
+
{renderRelatedEntities && (
|
|
372
|
+
<PivotItem headerText="Related" itemKey="related">
|
|
373
|
+
<div style={{ paddingTop: 20 }}>
|
|
374
|
+
{renderRelatedEntities(entity)}
|
|
375
|
+
</div>
|
|
376
|
+
</PivotItem>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
{renderAuditHistory && (
|
|
380
|
+
<PivotItem headerText="History" itemKey="history">
|
|
381
|
+
<div style={{ paddingTop: 20 }}>
|
|
382
|
+
{renderAuditHistory(entity)}
|
|
383
|
+
</div>
|
|
384
|
+
</PivotItem>
|
|
385
|
+
)}
|
|
386
|
+
</Pivot>
|
|
387
|
+
) : (
|
|
388
|
+
<div style={{ paddingTop: 20 }}>
|
|
389
|
+
{renderForm(entity, handleSave, !isEditing)}
|
|
390
|
+
</div>
|
|
391
|
+
)}
|
|
392
|
+
|
|
393
|
+
{/* Edit Panel */}
|
|
394
|
+
<Panel
|
|
395
|
+
headerText={`Edit ${title}`}
|
|
396
|
+
isOpen={showEditPanel}
|
|
397
|
+
onDismiss={() => {
|
|
398
|
+
setShowEditPanel(false);
|
|
399
|
+
setIsEditing(false);
|
|
400
|
+
}}
|
|
401
|
+
type={PanelType.medium}
|
|
402
|
+
closeButtonAriaLabel="Close"
|
|
403
|
+
>
|
|
404
|
+
<div style={{ paddingTop: 20 }}>
|
|
405
|
+
{renderForm(entity, handleSave, false)}
|
|
406
|
+
</div>
|
|
407
|
+
</Panel>
|
|
408
|
+
|
|
409
|
+
{/* Delete Confirmation Dialog */}
|
|
410
|
+
<Dialog
|
|
411
|
+
hidden={!showDeleteDialog}
|
|
412
|
+
onDismiss={() => setShowDeleteDialog(false)}
|
|
413
|
+
dialogContentProps={{
|
|
414
|
+
type: DialogType.normal,
|
|
415
|
+
title: 'Confirm Delete',
|
|
416
|
+
subText: `Are you sure you want to delete "${(entity as any).name || title}"? This action cannot be undone.`,
|
|
417
|
+
}}
|
|
418
|
+
>
|
|
419
|
+
<DialogFooter>
|
|
420
|
+
<PrimaryButton
|
|
421
|
+
onClick={handleDelete}
|
|
422
|
+
text="Delete"
|
|
423
|
+
disabled={saving}
|
|
424
|
+
/>
|
|
425
|
+
<DefaultButton
|
|
426
|
+
onClick={() => setShowDeleteDialog(false)}
|
|
427
|
+
text="Cancel"
|
|
428
|
+
disabled={saving}
|
|
429
|
+
/>
|
|
430
|
+
</DialogFooter>
|
|
431
|
+
</Dialog>
|
|
432
|
+
</Stack>
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Helper function to create detail FetchXML template
|
|
438
|
+
export function createDetailFetchXml(
|
|
439
|
+
entityName: string,
|
|
440
|
+
attributes: string[],
|
|
441
|
+
primaryIdAttribute: string = `${entityName}id`
|
|
442
|
+
): string {
|
|
443
|
+
const attributeElements = attributes
|
|
444
|
+
.map((attr) => `<attribute name="${attr}" />`)
|
|
445
|
+
.join('\n ');
|
|
446
|
+
|
|
447
|
+
return `
|
|
448
|
+
<fetch top="1">
|
|
449
|
+
<entity name="${entityName}">
|
|
450
|
+
${attributeElements}
|
|
451
|
+
<filter>
|
|
452
|
+
<condition attribute="${primaryIdAttribute}" operator="eq" value="{{entityId}}" />
|
|
453
|
+
</filter>
|
|
454
|
+
</entity>
|
|
455
|
+
</fetch>`;
|
|
456
|
+
}
|