@khester/create-dynamics-app 2.1.0 → 2.2.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 (121) hide show
  1. package/dist/artifacts/registry.d.ts +4 -3
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +121 -11
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/artifacts/types.d.ts +1 -1
  6. package/dist/artifacts/types.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/injectDevTools.d.ts.map +1 -1
  10. package/dist/injectDevTools.js +4 -2
  11. package/dist/injectDevTools.js.map +1 -1
  12. package/dist/scaffold.d.ts +1 -0
  13. package/dist/scaffold.d.ts.map +1 -1
  14. package/dist/scaffold.js +3 -1
  15. package/dist/scaffold.js.map +1 -1
  16. package/package.json +3 -2
  17. package/templates/grid-starter/ARCHITECTURE.md +66 -0
  18. package/templates/grid-starter/README.md +122 -0
  19. package/templates/grid-starter/env.example +16 -0
  20. package/templates/grid-starter/gitignore +6 -0
  21. package/templates/grid-starter/index.html +16 -0
  22. package/templates/grid-starter/package.json +39 -0
  23. package/templates/grid-starter/src/App.tsx +23 -0
  24. package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
  25. package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
  26. package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
  27. package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
  28. package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
  29. package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
  30. package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
  31. package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
  32. package/templates/grid-starter/src/index.tsx +18 -0
  33. package/templates/grid-starter/src/vite-env.d.ts +15 -0
  34. package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
  35. package/templates/grid-starter/tsconfig.json +19 -0
  36. package/templates/grid-starter/vite.config.ts +76 -0
  37. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
  38. package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
  39. package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
  40. package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
  41. package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
  42. package/templates/pcf-field/index.ts +1 -1
  43. package/templates/pcf-field/package.json +3 -1
  44. package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
  45. package/templates/react-custom-page/ARCHITECTURE.md +75 -0
  46. package/templates/react-custom-page/README.md +74 -568
  47. package/templates/react-custom-page/env.example +16 -0
  48. package/templates/react-custom-page/gitignore +1 -0
  49. package/templates/react-custom-page/index.html +16 -0
  50. package/templates/react-custom-page/package.json +21 -49
  51. package/templates/react-custom-page/src/App.tsx +26 -0
  52. package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
  53. package/templates/react-custom-page/src/core/recordContext.ts +51 -0
  54. package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
  55. package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
  56. package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
  57. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
  58. package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
  59. package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
  60. package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
  61. package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
  62. package/templates/react-custom-page/src/domain/diff.ts +38 -0
  63. package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
  64. package/templates/react-custom-page/src/example/exampleError.ts +36 -0
  65. package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
  66. package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
  67. package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
  68. package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
  69. package/templates/react-custom-page/src/example/models/Account.ts +74 -0
  70. package/templates/react-custom-page/src/index.tsx +18 -128
  71. package/templates/react-custom-page/src/vite-env.d.ts +15 -0
  72. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
  73. package/templates/react-custom-page/tsconfig.json +12 -22
  74. package/templates/react-custom-page/vite.config.ts +76 -0
  75. package/templates/starter-page/README.md +38 -0
  76. package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
  77. package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
  78. package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
  79. package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
  80. package/templates/starter-page/gitignore +5 -0
  81. package/templates/starter-page/package.json +27 -0
  82. package/templates/starter-page/public/index.html +11 -0
  83. package/templates/starter-page/src/index.tsx +10 -0
  84. package/templates/starter-page/src/services/dataverse.ts +30 -0
  85. package/templates/starter-page/tsconfig.json +15 -0
  86. package/templates/starter-page/webpack.config.js +17 -0
  87. package/templates/react-custom-page/deployment/README.md +0 -484
  88. package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
  89. package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
  90. package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
  91. package/templates/react-custom-page/public/index.html +0 -15
  92. package/templates/react-custom-page/scripts/custom-build.js +0 -255
  93. package/templates/react-custom-page/src/components/AccountForm.css +0 -71
  94. package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
  95. package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
  96. package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
  97. package/templates/react-custom-page/src/components/ContactForm.css +0 -48
  98. package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
  99. package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
  100. package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
  101. package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
  102. package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
  103. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
  104. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
  105. package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
  106. package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
  107. package/templates/react-custom-page/src/constants/account.ts +0 -410
  108. package/templates/react-custom-page/src/constants/contact.ts +0 -362
  109. package/templates/react-custom-page/src/models/Account.ts +0 -480
  110. package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
  111. package/templates/react-custom-page/src/models/Contact.ts +0 -580
  112. package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
  113. package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
  114. package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
  115. package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
  116. package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
  117. package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
  118. package/templates/react-custom-page/src/styles/index.css +0 -171
  119. package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
  120. package/templates/react-custom-page/webpack.config.js +0 -57
  121. /package/templates/_shared/dev-tools/auth/{get-token.js → get-token.cjs} +0 -0
@@ -1,327 +0,0 @@
1
- import React, { useState, useCallback, useEffect } from 'react';
2
- import {
3
- Button,
4
- TextField,
5
- Dropdown,
6
- DatePicker,
7
- } from '@khester/dynamics-ui-components';
8
- import { useDynamicsApi } from '../providers/DynamicsProvider';
9
- import { Contact, IContact } from '../models/Contact';
10
- import {
11
- ContactConstants,
12
- PreferredContactMethodCode_OptionSet,
13
- } from '../constants/contact';
14
- import { Logger } from './Logging/logger';
15
- import './ContactForm.css';
16
-
17
- interface ContactFormData {
18
- firstname: string;
19
- lastname: string;
20
- emailaddress1: string;
21
- telephone1: string;
22
- preferredcontactmethodcode: number;
23
- birthdate: Date | null;
24
- }
25
-
26
- interface ContactFormProps {
27
- contactId?: string;
28
- initialData?: Partial<ContactFormData>;
29
- onSave?: () => void;
30
- onCancel?: () => void;
31
- }
32
-
33
- const preferredContactOptions = [
34
- { key: PreferredContactMethodCode_OptionSet.Any, text: 'Any' },
35
- { key: PreferredContactMethodCode_OptionSet.Email, text: 'Email' },
36
- { key: PreferredContactMethodCode_OptionSet.Phone, text: 'Phone' },
37
- { key: PreferredContactMethodCode_OptionSet.Fax, text: 'Fax' },
38
- { key: PreferredContactMethodCode_OptionSet.Mail, text: 'Mail' },
39
- ];
40
-
41
- export const ContactForm: React.FC<ContactFormProps> = ({
42
- contactId,
43
- initialData,
44
- onSave,
45
- onCancel,
46
- }) => {
47
- const { apiService } = useDynamicsApi();
48
-
49
- const [formData, setFormData] = useState<ContactFormData>({
50
- firstname: '',
51
- lastname: '',
52
- emailaddress1: '',
53
- telephone1: '',
54
- preferredcontactmethodcode: PreferredContactMethodCode_OptionSet.Any,
55
- birthdate: null,
56
- });
57
-
58
- useEffect(() => {
59
- Logger.userAction(
60
- contactId
61
- ? 'Contact form opened for edit'
62
- : 'Contact form opened for new contact',
63
- { contactId },
64
- 'ContactForm'
65
- );
66
- }, [contactId]);
67
-
68
- const [errors, setErrors] = useState<Partial<ContactFormData>>({});
69
- const [isSubmitting, setIsSubmitting] = useState(false);
70
- const [submitError, setSubmitError] = useState<string>('');
71
-
72
- useEffect(() => {
73
- if (initialData) {
74
- Logger.debug(
75
- 'Populating form with initial data',
76
- 'ContactForm.useEffect',
77
- initialData
78
- );
79
-
80
- setFormData({
81
- firstname: initialData.firstname || '',
82
- lastname: initialData.lastname || '',
83
- emailaddress1: initialData.emailaddress1 || '',
84
- telephone1: initialData.telephone1 || '',
85
- preferredcontactmethodcode:
86
- initialData.preferredcontactmethodcode ||
87
- PreferredContactMethodCode_OptionSet.Any,
88
- birthdate: initialData.birthdate || null,
89
- });
90
- }
91
- }, [initialData]);
92
-
93
- const validateForm = useCallback((): boolean => {
94
- const newErrors: Partial<ContactFormData> = {};
95
- const validationErrors: string[] = [];
96
-
97
- // At least one name is required (following Contact model validation)
98
- if (!formData.firstname.trim() && !formData.lastname.trim()) {
99
- const error = 'Either first name or last name is required';
100
- newErrors.firstname = error;
101
- newErrors.lastname = error;
102
- validationErrors.push(error);
103
- }
104
-
105
- if (
106
- formData.emailaddress1 &&
107
- !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.emailaddress1)
108
- ) {
109
- const error = 'Please enter a valid email address';
110
- newErrors.emailaddress1 = error;
111
- validationErrors.push(error);
112
- }
113
-
114
- if (formData.telephone1 && !/^\+?[\d\s\-()]+$/.test(formData.telephone1)) {
115
- const error = 'Please enter a valid phone number';
116
- newErrors.telephone1 = error;
117
- validationErrors.push(error);
118
- }
119
-
120
- if (validationErrors.length > 0) {
121
- Logger.validation(
122
- 'Contact',
123
- validationErrors,
124
- 'ContactForm.validateForm'
125
- );
126
- }
127
-
128
- setErrors(newErrors);
129
- return Object.keys(newErrors).length === 0;
130
- }, [formData]);
131
-
132
- const handleInputChange = useCallback(
133
- (field: keyof ContactFormData, value: any) => {
134
- setFormData((prev) => ({ ...prev, [field]: value }));
135
-
136
- // Clear error for this field when user starts typing
137
- if (errors[field]) {
138
- setErrors((prev) => ({ ...prev, [field]: undefined }));
139
- }
140
- setSubmitError('');
141
- },
142
- [errors]
143
- );
144
-
145
- const handleSubmit = useCallback(async () => {
146
- if (!validateForm()) {
147
- return;
148
- }
149
-
150
- if (!apiService) {
151
- setSubmitError('API service not available');
152
- Logger.error('API service not available', 'ContactForm.handleSubmit');
153
- return;
154
- }
155
-
156
- setIsSubmitting(true);
157
- setSubmitError('');
158
-
159
- try {
160
- Logger.userAction(
161
- contactId ? 'Contact update submitted' : 'Contact creation submitted',
162
- { contactId, formData },
163
- 'ContactForm.handleSubmit'
164
- );
165
-
166
- const contactData: IContact = {
167
- [ContactConstants.FirstName]: formData.firstname.trim() || undefined,
168
- [ContactConstants.LastName]: formData.lastname.trim() || undefined,
169
- [ContactConstants.EMailAddress1]:
170
- formData.emailaddress1.trim() || undefined,
171
- [ContactConstants.BusinessPhone]:
172
- formData.telephone1.trim() || undefined,
173
- [ContactConstants.PreferredContactMethodCode]:
174
- formData.preferredcontactmethodcode,
175
- [ContactConstants.BirthDate]: formData.birthdate
176
- ? formData.birthdate.toISOString().split('T')[0]
177
- : undefined,
178
- };
179
-
180
- // Remove undefined values
181
- Object.keys(contactData).forEach((key) => {
182
- if (contactData[key as keyof IContact] === undefined) {
183
- delete contactData[key as keyof IContact];
184
- }
185
- });
186
-
187
- const contact = new Contact(contactData);
188
-
189
- if (contactId) {
190
- contact.contactid = contactId;
191
- await Contact.update(apiService, contact);
192
- Logger.log(
193
- `Successfully updated contact: ${contact.firstname} ${contact.lastname}`,
194
- 'ContactForm.handleSubmit'
195
- );
196
- } else {
197
- await Contact.create(apiService, contact);
198
- Logger.log(
199
- `Successfully created contact: ${contact.firstname} ${contact.lastname}`,
200
- 'ContactForm.handleSubmit'
201
- );
202
- }
203
-
204
- if (onSave) {
205
- onSave();
206
- }
207
- } catch (error) {
208
- const errorMessage =
209
- error instanceof Error
210
- ? error.message
211
- : 'An error occurred while saving';
212
- setSubmitError(errorMessage);
213
- Logger.error(
214
- `Failed to ${contactId ? 'update' : 'create'} contact`,
215
- 'ContactForm.handleSubmit',
216
- error
217
- );
218
- } finally {
219
- setIsSubmitting(false);
220
- }
221
- }, [formData, contactId, validateForm, apiService, onSave]);
222
-
223
- const handleCancel = useCallback(() => {
224
- if (onCancel) {
225
- onCancel();
226
- }
227
- }, [onCancel]);
228
-
229
- return (
230
- <div className="contact-form">
231
- {submitError && (
232
- <div className="contact-form__error" role="alert">
233
- {submitError}
234
- </div>
235
- )}
236
-
237
- <div className="contact-form__content">
238
- <div className="contact-form__row">
239
- <div className="contact-form__field">
240
- <TextField
241
- label="First Name"
242
- required
243
- value={formData.firstname}
244
- onChange={(_, value) =>
245
- handleInputChange('firstname', value || '')
246
- }
247
- errorMessage={errors.firstname}
248
- disabled={isSubmitting}
249
- />
250
- </div>
251
- <div className="contact-form__field">
252
- <TextField
253
- label="Last Name"
254
- required
255
- value={formData.lastname}
256
- onChange={(_, value) =>
257
- handleInputChange('lastname', value || '')
258
- }
259
- errorMessage={errors.lastname}
260
- disabled={isSubmitting}
261
- />
262
- </div>
263
- </div>
264
-
265
- <div className="contact-form__row">
266
- <div className="contact-form__field">
267
- <TextField
268
- label="Email Address"
269
- type="email"
270
- required
271
- value={formData.emailaddress1}
272
- onChange={(_, value) =>
273
- handleInputChange('emailaddress1', value || '')
274
- }
275
- errorMessage={errors.emailaddress1}
276
- disabled={isSubmitting}
277
- />
278
- </div>
279
- <div className="contact-form__field">
280
- <TextField
281
- label="Phone Number"
282
- type="tel"
283
- value={formData.telephone1}
284
- onChange={(_, value) =>
285
- handleInputChange('telephone1', value || '')
286
- }
287
- errorMessage={errors.telephone1}
288
- disabled={isSubmitting}
289
- />
290
- </div>
291
- </div>
292
-
293
- <div className="contact-form__row">
294
- <div className="contact-form__field">
295
- <Dropdown
296
- label="Preferred Contact Method"
297
- options={preferredContactOptions}
298
- selectedKey={formData.preferredcontactmethodcode}
299
- onChange={(_, option) =>
300
- handleInputChange('preferredcontactmethodcode', option?.key)
301
- }
302
- disabled={isSubmitting}
303
- />
304
- </div>
305
- <div className="contact-form__field">
306
- <DatePicker
307
- label="Birth Date"
308
- value={formData.birthdate ?? undefined}
309
- onSelectDate={(date) => handleInputChange('birthdate', date)}
310
- disabled={isSubmitting}
311
- placeholder="Select date..."
312
- />
313
- </div>
314
- </div>
315
- </div>
316
-
317
- <div className="contact-form__actions">
318
- <Button
319
- text={contactId ? 'Update' : 'Save'}
320
- onClick={handleSubmit}
321
- disabled={isSubmitting}
322
- />
323
- <Button text="Cancel" onClick={handleCancel} disabled={isSubmitting} />
324
- </div>
325
- </div>
326
- );
327
- };
@@ -1,86 +0,0 @@
1
- .contact-management {
2
- padding: 16px;
3
- }
4
-
5
- .contact-management__header {
6
- display: flex;
7
- justify-content: space-between;
8
- align-items: center;
9
- margin-bottom: 16px;
10
- flex-wrap: wrap;
11
- gap: 16px;
12
- }
13
-
14
- .contact-management__header h2 {
15
- margin: 0;
16
- font-size: 20px;
17
- font-weight: 600;
18
- color: #323130;
19
- }
20
-
21
- .contact-management__actions {
22
- display: flex;
23
- gap: 12px;
24
- align-items: center;
25
- flex-wrap: wrap;
26
- }
27
-
28
- .contact-management__toolbar {
29
- display: flex;
30
- gap: 8px;
31
- margin-bottom: 16px;
32
- padding: 8px 0;
33
- border-bottom: 1px solid #edebe9;
34
- }
35
-
36
- .contact-management__list {
37
- min-height: 400px;
38
- border: 1px solid #edebe9;
39
- border-radius: 4px;
40
- }
41
-
42
- .contact-management__loading {
43
- display: flex;
44
- justify-content: center;
45
- align-items: center;
46
- height: 200px;
47
- color: #605e5c;
48
- font-style: italic;
49
- }
50
-
51
- .contact-management__empty {
52
- display: flex;
53
- justify-content: center;
54
- align-items: center;
55
- height: 200px;
56
- color: #605e5c;
57
- font-style: italic;
58
- text-align: center;
59
- padding: 16px;
60
- }
61
-
62
- .contact-management__dialog-actions {
63
- display: flex;
64
- gap: 12px;
65
- justify-content: flex-end;
66
- padding-top: 16px;
67
- }
68
-
69
- @media (max-width: 768px) {
70
- .contact-management {
71
- padding: 8px;
72
- }
73
-
74
- .contact-management__header {
75
- flex-direction: column;
76
- align-items: stretch;
77
- }
78
-
79
- .contact-management__actions {
80
- justify-content: stretch;
81
- }
82
-
83
- .contact-management__toolbar {
84
- flex-wrap: wrap;
85
- }
86
- }