@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.
Files changed (107) hide show
  1. package/bin/create-dynamics-app.js +1 -1
  2. package/dist/index.js +140 -15
  3. package/dist/index.js.map +1 -1
  4. package/dist/utils/consultingHelpers.d.ts +13 -0
  5. package/dist/utils/consultingHelpers.d.ts.map +1 -0
  6. package/dist/utils/consultingHelpers.js +569 -0
  7. package/dist/utils/consultingHelpers.js.map +1 -0
  8. package/dist/utils/copyTemplate.d.ts.map +1 -1
  9. package/dist/utils/copyTemplate.js.map +1 -1
  10. package/dist/utils/initGit.d.ts.map +1 -1
  11. package/dist/utils/initGit.js.map +1 -1
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +3 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/dist/utils/updatePackageJson.d.ts +1 -1
  16. package/dist/utils/updatePackageJson.d.ts.map +1 -1
  17. package/dist/utils/updatePackageJson.js +11 -1
  18. package/dist/utils/updatePackageJson.js.map +1 -1
  19. package/package.json +1 -1
  20. package/templates/dynamics-365-starter/INTEGRATION_TEST_RESULTS.md +302 -0
  21. package/templates/dynamics-365-starter/PHASE_4_COMPLETION_SUMMARY.md +305 -0
  22. package/templates/dynamics-365-starter/README.md +566 -137
  23. package/templates/dynamics-365-starter/deployment/QUICKSTART-MAC.md +507 -0
  24. package/templates/dynamics-365-starter/deployment/QUICKSTART-WINDOWS.md +372 -0
  25. package/templates/dynamics-365-starter/deployment/README.md +484 -0
  26. package/templates/dynamics-365-starter/deployment/pipelines/README.md +375 -0
  27. package/templates/dynamics-365-starter/deployment/pipelines/azure-pipelines.yml +330 -0
  28. package/templates/dynamics-365-starter/deployment/pipelines/github-actions.yml +422 -0
  29. package/templates/dynamics-365-starter/deployment/pipelines/jenkins.groovy +636 -0
  30. package/templates/dynamics-365-starter/deployment/scripts/deploy.ps1 +417 -0
  31. package/templates/dynamics-365-starter/deployment/scripts/deploy.sh +582 -0
  32. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.ps1 +486 -0
  33. package/templates/dynamics-365-starter/deployment/scripts/team-onboarding.sh +567 -0
  34. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.ps1 +703 -0
  35. package/templates/dynamics-365-starter/deployment/scripts/validate-setup.sh +671 -0
  36. package/templates/dynamics-365-starter/docs/ARCHITECTURE_OVERVIEW.md +506 -0
  37. package/templates/dynamics-365-starter/docs/BEST_PRACTICES.md +723 -0
  38. package/templates/dynamics-365-starter/docs/MIGRATION_GUIDE.md +447 -0
  39. package/templates/dynamics-365-starter/docs/team-standards/README.md +273 -0
  40. package/templates/dynamics-365-starter/docs/team-standards/client-onboarding.md +577 -0
  41. package/templates/dynamics-365-starter/docs/team-standards/code-review-checklist.md +359 -0
  42. package/templates/dynamics-365-starter/docs/team-standards/coding-standards.md +700 -0
  43. package/templates/dynamics-365-starter/docs/team-standards/cross-platform-team-guide.md +736 -0
  44. package/templates/dynamics-365-starter/docs/team-standards/development-workflows.md +727 -0
  45. package/templates/dynamics-365-starter/docs/troubleshooting/common-errors.md +758 -0
  46. package/templates/dynamics-365-starter/docs/troubleshooting/platform-specific-issues.md +878 -0
  47. package/templates/dynamics-365-starter/package.json +22 -1
  48. package/templates/dynamics-365-starter/public/index.html +8 -11
  49. package/templates/dynamics-365-starter/scripts/custom-build.js +255 -0
  50. package/templates/dynamics-365-starter/src/client-project-template/README.md +234 -0
  51. package/templates/dynamics-365-starter/src/client-project-template/config/client.template.json +114 -0
  52. package/templates/dynamics-365-starter/src/client-project-template/config/environments/template.json +186 -0
  53. package/templates/dynamics-365-starter/src/client-project-template/scripts/client-setup.js +667 -0
  54. package/templates/dynamics-365-starter/src/components/AccountForm.css +71 -0
  55. package/templates/dynamics-365-starter/src/components/AccountForm.tsx +541 -0
  56. package/templates/dynamics-365-starter/src/components/AccountManagement.css +86 -0
  57. package/templates/dynamics-365-starter/src/components/AccountManagement.tsx +370 -0
  58. package/templates/dynamics-365-starter/src/components/ContactForm.tsx +149 -63
  59. package/templates/dynamics-365-starter/src/components/ContactManagement.tsx +153 -63
  60. package/templates/dynamics-365-starter/src/components/Logging/LogDialog.tsx +291 -0
  61. package/templates/dynamics-365-starter/src/components/Logging/LoggingContext.tsx +166 -0
  62. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.css +192 -0
  63. package/templates/dynamics-365-starter/src/components/Logging/LoggingDebugPanel.tsx +177 -0
  64. package/templates/dynamics-365-starter/src/components/Logging/LoggingProvider.tsx +3 -0
  65. package/templates/dynamics-365-starter/src/components/Logging/logger.ts +193 -0
  66. package/templates/dynamics-365-starter/src/constants/account.ts +410 -0
  67. package/templates/dynamics-365-starter/src/constants/contact.ts +362 -0
  68. package/templates/dynamics-365-starter/src/examples/README.md +52 -0
  69. package/templates/dynamics-365-starter/src/examples/component-examples/opportunity-management.tsx +625 -0
  70. package/templates/dynamics-365-starter/src/examples/entity-examples/opportunity-model.ts +545 -0
  71. package/templates/dynamics-365-starter/src/examples/integration-examples/custom-pcf-wrapper.tsx +722 -0
  72. package/templates/dynamics-365-starter/src/examples/workflow-examples/sales-workflow.ts +662 -0
  73. package/templates/dynamics-365-starter/src/index.tsx +107 -19
  74. package/templates/dynamics-365-starter/src/models/Account.ts +480 -0
  75. package/templates/dynamics-365-starter/src/models/BaseEntity.ts +204 -0
  76. package/templates/dynamics-365-starter/src/models/Contact.ts +580 -0
  77. package/templates/dynamics-365-starter/src/page-templates/EntityDashboard.tsx +519 -0
  78. package/templates/dynamics-365-starter/src/page-templates/EntityDetailPage.tsx +456 -0
  79. package/templates/dynamics-365-starter/src/page-templates/EntityListPage.tsx +406 -0
  80. package/templates/dynamics-365-starter/src/page-templates/RelatedEntitiesPage.tsx +578 -0
  81. package/templates/dynamics-365-starter/src/page-templates/SearchPage.tsx +629 -0
  82. package/templates/dynamics-365-starter/src/pcf/ContactControlWrapper.tsx +75 -22
  83. package/templates/dynamics-365-starter/src/pcf/MultiEntityControlWrapper.tsx +205 -0
  84. package/templates/dynamics-365-starter/src/providers/DynamicsProvider.tsx +297 -80
  85. package/templates/dynamics-365-starter/src/services/MockApiService.ts +260 -0
  86. package/templates/dynamics-365-starter/src/services/ServiceFactory.ts +65 -0
  87. package/templates/dynamics-365-starter/src/services/XrmApiService.ts +213 -0
  88. package/templates/dynamics-365-starter/src/styles/index.css +74 -7
  89. package/templates/dynamics-365-starter/tools/entity-generator/index.js +168 -0
  90. package/templates/dynamics-365-starter/tools/entity-generator/templates/constants.template.ts +124 -0
  91. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.css +283 -0
  92. package/templates/dynamics-365-starter/tools/entity-generator/templates/form.template.tsx +275 -0
  93. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.css +204 -0
  94. package/templates/dynamics-365-starter/tools/entity-generator/templates/management.template.tsx +413 -0
  95. package/templates/dynamics-365-starter/tools/entity-generator/templates/model.template.ts +250 -0
  96. package/templates/dynamics-365-starter/tools/metadata-sync/d365-client.js +410 -0
  97. package/templates/dynamics-365-starter/tools/metadata-sync/index.js +512 -0
  98. package/templates/dynamics-365-starter/tools/metadata-sync/type-generator.js +675 -0
  99. package/templates/dynamics-365-starter/tsconfig.json +11 -8
  100. package/templates/dynamics-365-starter/webpack.config.js +8 -9
  101. package/templates/power-pages-starter/README.md +7 -1
  102. package/templates/power-pages-starter/public/index.html +8 -11
  103. package/templates/power-pages-starter/src/components/ContactForm.tsx +60 -41
  104. package/templates/power-pages-starter/src/index.tsx +3 -3
  105. package/templates/power-pages-starter/src/providers/PowerPagesProvider.tsx +46 -23
  106. package/templates/power-pages-starter/tsconfig.json +3 -9
  107. package/templates/power-pages-starter/webpack.config.js +8 -3
@@ -0,0 +1,283 @@
1
+ /* {{displayName}} Form Component Styles */
2
+ /* Generated by Entity Generator */
3
+ /* Replace 'sample-entity' with your actual entity name during template processing */
4
+
5
+ .sample-entity-form {
6
+ padding: 24px 0;
7
+ max-width: 600px;
8
+ }
9
+
10
+ .sample-entity-form-section {
11
+ margin-bottom: 24px;
12
+ }
13
+
14
+ .sample-entity-form-section-title {
15
+ font-size: 16px;
16
+ font-weight: 600;
17
+ color: #323130;
18
+ margin: 0 0 16px 0;
19
+ border-bottom: 1px solid #edebe9;
20
+ padding-bottom: 8px;
21
+ }
22
+
23
+ .sample-entity-form-field {
24
+ margin-bottom: 16px;
25
+ }
26
+
27
+ .sample-entity-form-errors {
28
+ margin: 0;
29
+ padding: 0;
30
+ list-style: none;
31
+ }
32
+
33
+ .sample-entity-form-errors li {
34
+ margin: 4px 0;
35
+ }
36
+
37
+ /* Field-specific styling */
38
+ .sample-entity-form .ms-TextField {
39
+ width: 100%;
40
+ }
41
+
42
+ .sample-entity-form .ms-TextField-fieldGroup {
43
+ border-radius: 2px;
44
+ }
45
+
46
+ .sample-entity-form .ms-TextField-field {
47
+ padding: 8px 12px;
48
+ font-size: 14px;
49
+ }
50
+
51
+ .sample-entity-form .ms-TextField-field:focus {
52
+ border-color: #0078d4;
53
+ box-shadow: 0 0 0 1px #0078d4;
54
+ }
55
+
56
+ /* Dropdown styling */
57
+ .sample-entity-form .ms-Dropdown {
58
+ width: 100%;
59
+ }
60
+
61
+ .sample-entity-form .ms-Dropdown-title {
62
+ padding: 8px 12px;
63
+ font-size: 14px;
64
+ border-radius: 2px;
65
+ }
66
+
67
+ .sample-entity-form .ms-Dropdown-title:focus {
68
+ border-color: #0078d4;
69
+ box-shadow: 0 0 0 1px #0078d4;
70
+ }
71
+
72
+ /* DatePicker styling */
73
+ .sample-entity-form .ms-DatePicker {
74
+ width: 100%;
75
+ }
76
+
77
+ .sample-entity-form .ms-DatePicker-input {
78
+ width: 100%;
79
+ }
80
+
81
+ .sample-entity-form .ms-DatePicker-input .ms-TextField-field {
82
+ padding: 8px 12px;
83
+ font-size: 14px;
84
+ }
85
+
86
+ /* Checkbox styling */
87
+ .sample-entity-form .ms-Checkbox {
88
+ margin-top: 8px;
89
+ }
90
+
91
+ .sample-entity-form .ms-Checkbox-label {
92
+ font-size: 14px;
93
+ color: #323130;
94
+ }
95
+
96
+ .sample-entity-form .ms-Checkbox-checkbox {
97
+ border-radius: 2px;
98
+ }
99
+
100
+ .sample-entity-form .ms-Checkbox-checkbox:checked {
101
+ background-color: #0078d4;
102
+ border-color: #0078d4;
103
+ }
104
+
105
+ /* Button styling */
106
+ .sample-entity-form .ms-Button {
107
+ min-width: 80px;
108
+ height: 32px;
109
+ border-radius: 2px;
110
+ font-size: 14px;
111
+ }
112
+
113
+ .sample-entity-form .ms-Button--primary {
114
+ background-color: #0078d4;
115
+ border-color: #0078d4;
116
+ }
117
+
118
+ .sample-entity-form .ms-Button--primary:hover {
119
+ background-color: #106ebe;
120
+ border-color: #106ebe;
121
+ }
122
+
123
+ .sample-entity-form .ms-Button--default {
124
+ background-color: #ffffff;
125
+ border-color: #8a8886;
126
+ color: #323130;
127
+ }
128
+
129
+ .sample-entity-form .ms-Button--default:hover {
130
+ background-color: #f3f2f1;
131
+ border-color: #8a8886;
132
+ }
133
+
134
+ /* Required field indicators */
135
+ .sample-entity-form .ms-Label .ms-Label-required {
136
+ color: #d13438;
137
+ }
138
+
139
+ /* Error states */
140
+ .sample-entity-form .ms-TextField--hasError .ms-TextField-fieldGroup {
141
+ border-color: #d13438;
142
+ }
143
+
144
+ .sample-entity-form .ms-TextField--hasError .ms-TextField-field:focus {
145
+ border-color: #d13438;
146
+ box-shadow: 0 0 0 1px #d13438;
147
+ }
148
+
149
+ .sample-entity-form .ms-Dropdown--hasError .ms-Dropdown-title {
150
+ border-color: #d13438;
151
+ }
152
+
153
+ /* Disabled states */
154
+ .sample-entity-form .ms-TextField--disabled .ms-TextField-field {
155
+ background-color: #f3f2f1;
156
+ color: #a19f9d;
157
+ cursor: not-allowed;
158
+ }
159
+
160
+ .sample-entity-form .ms-Dropdown--disabled .ms-Dropdown-title {
161
+ background-color: #f3f2f1;
162
+ color: #a19f9d;
163
+ cursor: not-allowed;
164
+ }
165
+
166
+ .sample-entity-form .ms-Checkbox--disabled .ms-Checkbox-label {
167
+ color: #a19f9d;
168
+ cursor: not-allowed;
169
+ }
170
+
171
+ .sample-entity-form .ms-Button:disabled {
172
+ background-color: #f3f2f1;
173
+ border-color: #c8c6c4;
174
+ color: #a19f9d;
175
+ cursor: not-allowed;
176
+ }
177
+
178
+ /* Responsive design */
179
+ @media (max-width: 768px) {
180
+ .sample-entity-form {
181
+ padding: 16px 0;
182
+ max-width: 100%;
183
+ }
184
+
185
+ .sample-entity-form-section-title {
186
+ font-size: 14px;
187
+ margin-bottom: 12px;
188
+ }
189
+
190
+ .sample-entity-form-field {
191
+ margin-bottom: 12px;
192
+ }
193
+
194
+ .sample-entity-form .ms-TextField-field,
195
+ .sample-entity-form .ms-Dropdown-title,
196
+ .sample-entity-form .ms-DatePicker-input .ms-TextField-field {
197
+ padding: 6px 10px;
198
+ font-size: 13px;
199
+ }
200
+ }
201
+
202
+ /* Focus management */
203
+ .sample-entity-form .ms-TextField-field:focus,
204
+ .sample-entity-form .ms-Dropdown-title:focus,
205
+ .sample-entity-form .ms-DatePicker-input .ms-TextField-field:focus {
206
+ outline: none;
207
+ }
208
+
209
+ /* High contrast mode support */
210
+ @media (prefers-contrast: high) {
211
+ .sample-entity-form .ms-TextField-fieldGroup,
212
+ .sample-entity-form .ms-Dropdown-title {
213
+ border-width: 2px;
214
+ }
215
+
216
+ .sample-entity-form .ms-Button {
217
+ border-width: 2px;
218
+ }
219
+ }
220
+
221
+ /* Print styles */
222
+ @media print {
223
+ .sample-entity-form .ms-Button {
224
+ display: none;
225
+ }
226
+
227
+ .sample-entity-form {
228
+ padding: 0;
229
+ }
230
+
231
+ .sample-entity-form-section-title {
232
+ font-size: 14px;
233
+ font-weight: bold;
234
+ }
235
+ }
236
+
237
+ /* Animation for section transitions */
238
+ .sample-entity-form-section {
239
+ transition: opacity 0.2s ease-in-out;
240
+ }
241
+
242
+ .sample-entity-form-section.loading {
243
+ opacity: 0.6;
244
+ }
245
+
246
+ /* Custom styling for money fields */
247
+ .sample-entity-form .money-field .ms-TextField-prefix {
248
+ background-color: #f3f2f1;
249
+ border-right: 1px solid #edebe9;
250
+ color: #605e5c;
251
+ font-weight: 600;
252
+ }
253
+
254
+ /* Custom styling for multiline text fields */
255
+ .sample-entity-form .ms-TextField--multiline .ms-TextField-field {
256
+ min-height: 80px;
257
+ resize: vertical;
258
+ }
259
+
260
+ /* Loading state for buttons */
261
+ .sample-entity-form .ms-Button.loading {
262
+ position: relative;
263
+ color: transparent;
264
+ }
265
+
266
+ .sample-entity-form .ms-Button.loading::after {
267
+ content: '';
268
+ position: absolute;
269
+ top: 50%;
270
+ left: 50%;
271
+ width: 16px;
272
+ height: 16px;
273
+ margin: -8px 0 0 -8px;
274
+ border: 2px solid #ffffff;
275
+ border-radius: 50%;
276
+ border-top-color: transparent;
277
+ animation: button-loading-spin 1s linear infinite;
278
+ }
279
+
280
+ @keyframes button-loading-spin {
281
+ 0% { transform: rotate(0deg); }
282
+ 100% { transform: rotate(360deg); }
283
+ }
@@ -0,0 +1,275 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ TextField,
4
+ DatePicker,
5
+ Dropdown,
6
+ IDropdownOption,
7
+ Checkbox,
8
+ PrimaryButton,
9
+ DefaultButton,
10
+ MessageBar,
11
+ MessageBarType,
12
+ Stack,
13
+ Separator
14
+ } from '@fluentui/react';
15
+ // Replace 'SampleEntity' with your actual entity PascalCase name during template processing
16
+ // import { BaseEntity } from '../../models/BaseEntity';
17
+ // import { Logger } from '../../components/Logging/logger';
18
+ import './SampleEntityForm.css';
19
+
20
+ // Template base entity interface - replace with actual BaseEntity during generation
21
+ interface BaseEntity {
22
+ id?: string;
23
+ [key: string]: any;
24
+ }
25
+
26
+ // Template Logger - replace with actual Logger during generation
27
+ const Logger = {
28
+ log: (message: string, source?: string) => console.log(message, source),
29
+ warn: (message: string, source?: string) => console.warn(message, source),
30
+ error: (message: string, source?: string, error?: any) => console.error(message, source, error)
31
+ };
32
+
33
+ // Template interfaces - replace with actual entity interface during generation
34
+ interface ISampleEntity {
35
+ id?: string;
36
+ name?: string;
37
+ description?: string;
38
+ priority?: number;
39
+ dueDate?: Date;
40
+ amount?: number;
41
+ isActive?: boolean;
42
+ }
43
+
44
+ interface SampleEntity extends BaseEntity, ISampleEntity {}
45
+
46
+ // Template constants - replace with actual constants during generation
47
+ const SampleEntityConstants = {
48
+ PrimaryNameAttribute: 'name',
49
+ Description: 'description',
50
+ Priority: 'priority',
51
+ DueDate: 'dueDate',
52
+ Amount: 'amount',
53
+ IsActive: 'isActive',
54
+ FieldDisplayNames: {
55
+ name: 'Name',
56
+ description: 'Description',
57
+ priority: 'Priority',
58
+ dueDate: 'Due Date',
59
+ amount: 'Amount',
60
+ isActive: 'Is Active'
61
+ },
62
+ PriorityOptions: {
63
+ Low: 1,
64
+ Medium: 2,
65
+ High: 3,
66
+ Critical: 4
67
+ }
68
+ };
69
+
70
+ interface SampleEntityFormProps {
71
+ sampleEntity?: SampleEntity | null;
72
+ onSave: (sampleEntityData: Partial<ISampleEntity>) => Promise<void>;
73
+ onCancel: () => void;
74
+ readOnly?: boolean;
75
+ }
76
+
77
+ export const SampleEntityForm: React.FC<SampleEntityFormProps> = ({
78
+ sampleEntity,
79
+ onSave,
80
+ onCancel,
81
+ readOnly = false
82
+ }) => {
83
+ const [formData, setFormData] = useState<Partial<ISampleEntity>>({});
84
+ const [errors, setErrors] = useState<string[]>([]);
85
+ const [saving, setSaving] = useState(false);
86
+
87
+ // Initialize form data
88
+ useEffect(() => {
89
+ if (sampleEntity) {
90
+ setFormData({
91
+ [SampleEntityConstants.PrimaryNameAttribute]: sampleEntity[SampleEntityConstants.PrimaryNameAttribute] || '',
92
+ [SampleEntityConstants.Description]: sampleEntity[SampleEntityConstants.Description] || '',
93
+ [SampleEntityConstants.Priority]: sampleEntity[SampleEntityConstants.Priority] || SampleEntityConstants.PriorityOptions.Medium,
94
+ [SampleEntityConstants.DueDate]: sampleEntity[SampleEntityConstants.DueDate] || null,
95
+ [SampleEntityConstants.Amount]: sampleEntity[SampleEntityConstants.Amount] || 0,
96
+ [SampleEntityConstants.IsActive]: sampleEntity[SampleEntityConstants.IsActive] ?? true
97
+ });
98
+ } else {
99
+ // Default values for new entity
100
+ setFormData({
101
+ [SampleEntityConstants.PrimaryNameAttribute]: '',
102
+ [SampleEntityConstants.Description]: '',
103
+ [SampleEntityConstants.Priority]: SampleEntityConstants.PriorityOptions.Medium,
104
+ [SampleEntityConstants.DueDate]: null,
105
+ [SampleEntityConstants.Amount]: 0,
106
+ [SampleEntityConstants.IsActive]: true
107
+ });
108
+ }
109
+ }, [sampleEntity]);
110
+
111
+ // Handle field changes
112
+ const handleFieldChange = (fieldName: string, value: any) => {
113
+ setFormData(prev => ({
114
+ ...prev,
115
+ [fieldName]: value
116
+ }));
117
+
118
+ // Clear errors when user starts typing
119
+ if (errors.length > 0) {
120
+ setErrors([]);
121
+ }
122
+ };
123
+
124
+ // Validate form
125
+ const validateForm = (): boolean => {
126
+ const validationErrors: string[] = [];
127
+
128
+ // Required field validation
129
+ if (!formData[SampleEntityConstants.PrimaryNameAttribute]?.trim()) {
130
+ validationErrors.push(`${SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.PrimaryNameAttribute]} is required`);
131
+ }
132
+
133
+ // Additional validation rules
134
+ if (formData[SampleEntityConstants.PrimaryNameAttribute] &&
135
+ formData[SampleEntityConstants.PrimaryNameAttribute].length > 100) {
136
+ validationErrors.push(`${SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.PrimaryNameAttribute]} cannot exceed 100 characters`);
137
+ }
138
+
139
+ if (formData[SampleEntityConstants.Amount] &&
140
+ formData[SampleEntityConstants.Amount] < 0) {
141
+ validationErrors.push(`${SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.Amount]} cannot be negative`);
142
+ }
143
+
144
+ setErrors(validationErrors);
145
+ return validationErrors.length === 0;
146
+ };
147
+
148
+ // Handle save
149
+ const handleSave = async () => {
150
+ if (!validateForm()) {
151
+ Logger.warn('Sample Entity form validation failed');
152
+ return;
153
+ }
154
+
155
+ try {
156
+ setSaving(true);
157
+ Logger.log('Saving Sample Entity form data');
158
+ await onSave(formData);
159
+ } catch (error) {
160
+ Logger.error('Error saving Sample Entity:', error);
161
+ setErrors([error instanceof Error ? error.message : 'Failed to save Sample Entity']);
162
+ } finally {
163
+ setSaving(false);
164
+ }
165
+ };
166
+
167
+ // Priority options
168
+ const priorityOptions: IDropdownOption[] = Object.entries(SampleEntityConstants.PriorityOptions).map(([key, value]) => ({
169
+ key: value as string | number,
170
+ text: key
171
+ }));
172
+
173
+ return (
174
+ <div className="sample-entity-form">
175
+ {errors.length > 0 && (
176
+ <MessageBar messageBarType={MessageBarType.error}>
177
+ <ul className="sample-entity-form-errors">
178
+ {errors.map((error, index) => (
179
+ <li key={index}>{error}</li>
180
+ ))}
181
+ </ul>
182
+ </MessageBar>
183
+ )}
184
+
185
+ <Stack tokens={{ childrenGap: 20 }}>
186
+ {/* Basic Information Section */}
187
+ <div className="sample-entity-form-section">
188
+ <h3 className="sample-entity-form-section-title">Basic Information</h3>
189
+
190
+ <TextField
191
+ label={SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.PrimaryNameAttribute]}
192
+ required
193
+ value={formData[SampleEntityConstants.PrimaryNameAttribute] || ''}
194
+ onChange={(_, newValue) => handleFieldChange(SampleEntityConstants.PrimaryNameAttribute, newValue)}
195
+ disabled={readOnly}
196
+ placeholder="Enter Sample Entity name"
197
+ maxLength={100}
198
+ className="sample-entity-form-field"
199
+ />
200
+
201
+ <TextField
202
+ label={SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.Description]}
203
+ multiline
204
+ rows={3}
205
+ value={formData[SampleEntityConstants.Description] || ''}
206
+ onChange={(_, newValue) => handleFieldChange(SampleEntityConstants.Description, newValue)}
207
+ disabled={readOnly}
208
+ placeholder="Enter description"
209
+ className="sample-entity-form-field"
210
+ />
211
+
212
+ <Dropdown
213
+ label={SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.Priority]}
214
+ options={priorityOptions}
215
+ selectedKey={formData[SampleEntityConstants.Priority]}
216
+ onChange={(_, option) => handleFieldChange(SampleEntityConstants.Priority, option?.key)}
217
+ disabled={readOnly}
218
+ className="sample-entity-form-field"
219
+ />
220
+ </div>
221
+
222
+ <Separator />
223
+
224
+ {/* Additional Details Section */}
225
+ <div className="sample-entity-form-section">
226
+ <h3 className="sample-entity-form-section-title">Additional Details</h3>
227
+
228
+ <DatePicker
229
+ label={SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.DueDate]}
230
+ value={formData[SampleEntityConstants.DueDate] ? new Date(formData[SampleEntityConstants.DueDate]) : undefined}
231
+ onSelectDate={(date) => handleFieldChange(SampleEntityConstants.DueDate, date)}
232
+ disabled={readOnly}
233
+ placeholder="Select due date"
234
+ ariaLabel="Due date"
235
+ className="sample-entity-form-field"
236
+ />
237
+
238
+ <TextField
239
+ label={SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.Amount]}
240
+ type="number"
241
+ value={formData[SampleEntityConstants.Amount]?.toString() || '0'}
242
+ onChange={(_, newValue) => handleFieldChange(SampleEntityConstants.Amount, parseFloat(newValue || '0'))}
243
+ disabled={readOnly}
244
+ prefix="$"
245
+ className="sample-entity-form-field"
246
+ />
247
+
248
+ <Checkbox
249
+ label={SampleEntityConstants.FieldDisplayNames[SampleEntityConstants.IsActive]}
250
+ checked={formData[SampleEntityConstants.IsActive] ?? true}
251
+ onChange={(_, checked) => handleFieldChange(SampleEntityConstants.IsActive, checked)}
252
+ disabled={readOnly}
253
+ className="sample-entity-form-field"
254
+ />
255
+ </div>
256
+
257
+ {/* Action Buttons */}
258
+ {!readOnly && (
259
+ <Stack horizontal horizontalAlign="end" tokens={{ childrenGap: 10 }}>
260
+ <DefaultButton
261
+ text="Cancel"
262
+ onClick={onCancel}
263
+ disabled={saving}
264
+ />
265
+ <PrimaryButton
266
+ text={sampleEntity ? "Update" : "Create"}
267
+ onClick={handleSave}
268
+ disabled={saving}
269
+ />
270
+ </Stack>
271
+ )}
272
+ </Stack>
273
+ </div>
274
+ );
275
+ };