@khester/create-dynamics-app 2.1.0 → 2.3.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/dist/artifacts/registry.d.ts +4 -3
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +122 -12
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/artifacts/types.d.ts +1 -1
- package/dist/artifacts/types.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/injectDevTools.d.ts.map +1 -1
- package/dist/injectDevTools.js +4 -2
- package/dist/injectDevTools.js.map +1 -1
- package/dist/scaffold.d.ts +1 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +3 -1
- package/dist/scaffold.js.map +1 -1
- package/package.json +3 -2
- package/templates/grid-starter/ARCHITECTURE.md +66 -0
- package/templates/grid-starter/README.md +122 -0
- package/templates/grid-starter/env.example +16 -0
- package/templates/grid-starter/gitignore +6 -0
- package/templates/grid-starter/index.html +16 -0
- package/templates/grid-starter/package.json +39 -0
- package/templates/grid-starter/src/App.tsx +23 -0
- package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
- package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
- package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
- package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
- package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
- package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
- package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
- package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
- package/templates/grid-starter/src/index.tsx +18 -0
- package/templates/grid-starter/src/vite-env.d.ts +15 -0
- package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
- package/templates/grid-starter/tsconfig.json +19 -0
- package/templates/grid-starter/vite.config.ts +76 -0
- package/templates/pcf-dataset/package.json +3 -1
- package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
- package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
- package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
- package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
- package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
- package/templates/pcf-field/index.ts +1 -1
- package/templates/pcf-field/package.json +3 -1
- package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
- package/templates/react-custom-page/ARCHITECTURE.md +75 -0
- package/templates/react-custom-page/README.md +74 -568
- package/templates/react-custom-page/env.example +16 -0
- package/templates/react-custom-page/gitignore +1 -0
- package/templates/react-custom-page/index.html +16 -0
- package/templates/react-custom-page/package.json +21 -49
- package/templates/react-custom-page/src/App.tsx +26 -0
- package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
- package/templates/react-custom-page/src/core/recordContext.ts +51 -0
- package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
- package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
- package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
- package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
- package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
- package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
- package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
- package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
- package/templates/react-custom-page/src/domain/diff.ts +38 -0
- package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
- package/templates/react-custom-page/src/example/exampleError.ts +36 -0
- package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
- package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
- package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
- package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
- package/templates/react-custom-page/src/example/models/Account.ts +74 -0
- package/templates/react-custom-page/src/index.tsx +18 -128
- package/templates/react-custom-page/src/vite-env.d.ts +15 -0
- package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
- package/templates/react-custom-page/tsconfig.json +12 -22
- package/templates/react-custom-page/vite.config.ts +76 -0
- package/templates/starter-page/README.md +38 -0
- package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
- package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
- package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
- package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
- package/templates/starter-page/gitignore +5 -0
- package/templates/starter-page/package.json +27 -0
- package/templates/starter-page/public/index.html +11 -0
- package/templates/starter-page/src/index.tsx +10 -0
- package/templates/starter-page/src/services/dataverse.ts +30 -0
- package/templates/starter-page/tsconfig.json +15 -0
- package/templates/starter-page/webpack.config.js +17 -0
- package/templates/react-custom-page/deployment/README.md +0 -484
- package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
- package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
- package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
- package/templates/react-custom-page/public/index.html +0 -15
- package/templates/react-custom-page/scripts/custom-build.js +0 -255
- package/templates/react-custom-page/src/components/AccountForm.css +0 -71
- package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
- package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
- package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
- package/templates/react-custom-page/src/components/ContactForm.css +0 -48
- package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
- package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
- package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
- package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
- package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
- package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
- package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
- package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
- package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
- package/templates/react-custom-page/src/constants/account.ts +0 -410
- package/templates/react-custom-page/src/constants/contact.ts +0 -362
- package/templates/react-custom-page/src/models/Account.ts +0 -480
- package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
- package/templates/react-custom-page/src/models/Contact.ts +0 -580
- package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
- package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
- package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
- package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
- package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
- package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
- package/templates/react-custom-page/src/styles/index.css +0 -171
- package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
- package/templates/react-custom-page/webpack.config.js +0 -57
- /package/templates/_shared/dev-tools/auth/{get-token.js → get-token.cjs} +0 -0
|
@@ -1,723 +0,0 @@
|
|
|
1
|
-
# Best Practices - Dynamics 365 Template
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This document outlines best practices for developing robust Dynamics 365 applications using the
|
|
6
|
-
enhanced template patterns.
|
|
7
|
-
|
|
8
|
-
## Architecture Best Practices
|
|
9
|
-
|
|
10
|
-
### 1. Entity Model Design
|
|
11
|
-
|
|
12
|
-
#### ✅ DO: Use Static Methods for CRUD Operations
|
|
13
|
-
|
|
14
|
-
```typescript
|
|
15
|
-
export class Account extends BaseEntity {
|
|
16
|
-
public static async create(apiService: IApiService, account: Account): Promise<Account> {
|
|
17
|
-
return await this.createEntity<Account>(
|
|
18
|
-
apiService,
|
|
19
|
-
account,
|
|
20
|
-
AccountConstants.EntityCollectionName,
|
|
21
|
-
'Account.create'
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public static async retrieveByName(apiService: IApiService, name: string): Promise<Account[]> {
|
|
26
|
-
const fetchXml = this.buildFetchXml(
|
|
27
|
-
AccountConstants.EntityLogicalName,
|
|
28
|
-
['accountid', 'name', 'emailaddress1'],
|
|
29
|
-
`<filter type="and">
|
|
30
|
-
<condition attribute="name" operator="like" value="%${this.escapeXml(name)}%" />
|
|
31
|
-
</filter>`
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
return await this.retrieveEntitiesByFilter<Account>(
|
|
35
|
-
apiService,
|
|
36
|
-
AccountConstants.EntityCollectionName,
|
|
37
|
-
fetchXml,
|
|
38
|
-
Account,
|
|
39
|
-
'Account.retrieveByName'
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
#### ❌ DON'T: Mix Instance and Static Methods
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// Avoid this pattern
|
|
49
|
-
export class Account {
|
|
50
|
-
public async save() {
|
|
51
|
-
/* instance method */
|
|
52
|
-
}
|
|
53
|
-
public static async create() {
|
|
54
|
-
/* static method */
|
|
55
|
-
}
|
|
56
|
-
public async delete() {
|
|
57
|
-
/* instance method */
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
#### ✅ DO: Implement Comprehensive Validation
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
export class Account extends BaseEntity {
|
|
66
|
-
public validate(): boolean {
|
|
67
|
-
const errors: string[] = [];
|
|
68
|
-
|
|
69
|
-
// Required field validation
|
|
70
|
-
if (!this.name?.trim()) {
|
|
71
|
-
errors.push('Account name is required');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Length validation
|
|
75
|
-
if (this.name && this.name.length > 160) {
|
|
76
|
-
errors.push('Account name cannot exceed 160 characters');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Format validation
|
|
80
|
-
if (this.emailaddress1 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.emailaddress1)) {
|
|
81
|
-
errors.push('Invalid email format');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Business rule validation
|
|
85
|
-
if (this.revenue && this.revenue < 0) {
|
|
86
|
-
errors.push('Revenue cannot be negative');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (errors.length > 0) {
|
|
90
|
-
Logger.validation('Account', errors, 'Account.validate');
|
|
91
|
-
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### 2. Service Layer Architecture
|
|
100
|
-
|
|
101
|
-
#### ✅ DO: Use the Service Factory Pattern
|
|
102
|
-
|
|
103
|
-
```typescript
|
|
104
|
-
// Let the factory determine the appropriate service
|
|
105
|
-
const apiService = ServiceFactory.createApiService();
|
|
106
|
-
|
|
107
|
-
// Environment-aware service creation
|
|
108
|
-
const apiService = ServiceFactory.createApiService(
|
|
109
|
-
ServiceFactory.isDynamics365Context() ? (window as any).Xrm : undefined
|
|
110
|
-
);
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
#### ❌ DON'T: Hard-code Service Selection
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
// Avoid hard-coding environment detection
|
|
117
|
-
const apiService =
|
|
118
|
-
window.location.hostname === 'localhost'
|
|
119
|
-
? new MockApiService()
|
|
120
|
-
: new XrmApiService((window as any).Xrm);
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
#### ✅ DO: Implement Proper Error Handling in Services
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
export class XrmApiService implements IApiService {
|
|
127
|
-
async createRecord(entityName: string, record: any): Promise<any> {
|
|
128
|
-
try {
|
|
129
|
-
if (!this.xrm?.WebApi) {
|
|
130
|
-
throw new Error('Xrm.WebApi is not available');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const result = await this.xrm.WebApi.createRecord(entityName, record);
|
|
134
|
-
Logger.apiOperation('CREATE', entityName, record, 'XrmApiService.createRecord');
|
|
135
|
-
return result;
|
|
136
|
-
} catch (error) {
|
|
137
|
-
Logger.error(`Failed to create ${entityName} record`, 'XrmApiService.createRecord', error);
|
|
138
|
-
throw new Error(`Unable to create ${entityName}: ${error}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### 3. Component Architecture
|
|
145
|
-
|
|
146
|
-
#### ✅ DO: Use the Provider Pattern for API Services
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
function App() {
|
|
150
|
-
return (
|
|
151
|
-
<DynamicsProvider>
|
|
152
|
-
<LoggingProvider>
|
|
153
|
-
<Router>
|
|
154
|
-
<Routes>
|
|
155
|
-
<Route path="/accounts" element={<AccountManagement />} />
|
|
156
|
-
<Route path="/contacts" element={<ContactManagement />} />
|
|
157
|
-
</Routes>
|
|
158
|
-
</Router>
|
|
159
|
-
</LoggingProvider>
|
|
160
|
-
</DynamicsProvider>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function AccountManagement() {
|
|
165
|
-
const { apiService } = useDynamicsApi(); // Gets service from context
|
|
166
|
-
// Component implementation
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
#### ✅ DO: Implement Proper Loading and Error States
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
export const AccountManagement: React.FC = () => {
|
|
174
|
-
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
175
|
-
const [loading, setLoading] = useState(false);
|
|
176
|
-
const [error, setError] = useState<string | null>(null);
|
|
177
|
-
|
|
178
|
-
const loadAccounts = useCallback(async () => {
|
|
179
|
-
setLoading(true);
|
|
180
|
-
setError(null);
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const accountsData = await Account.retrieveActiveAccounts(apiService);
|
|
184
|
-
setAccounts(accountsData);
|
|
185
|
-
Logger.log(`Loaded ${accountsData.length} accounts`, 'AccountManagement.loadAccounts');
|
|
186
|
-
} catch (error) {
|
|
187
|
-
const errorMessage = 'Failed to load accounts';
|
|
188
|
-
setError(errorMessage);
|
|
189
|
-
Logger.error(errorMessage, 'AccountManagement.loadAccounts', error);
|
|
190
|
-
} finally {
|
|
191
|
-
setLoading(false);
|
|
192
|
-
}
|
|
193
|
-
}, [apiService]);
|
|
194
|
-
|
|
195
|
-
if (loading) return <div>Loading accounts...</div>;
|
|
196
|
-
if (error) return <div>Error: {error}</div>;
|
|
197
|
-
if (accounts.length === 0) return <div>No accounts found</div>;
|
|
198
|
-
|
|
199
|
-
return (
|
|
200
|
-
<DetailsList
|
|
201
|
-
items={accounts}
|
|
202
|
-
columns={columns}
|
|
203
|
-
onItemInvoked={handleItemInvoked}
|
|
204
|
-
/>
|
|
205
|
-
);
|
|
206
|
-
};
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
#### ❌ DON'T: Ignore Error States
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
// Avoid this - no error handling
|
|
213
|
-
export const AccountManagement: React.FC = () => {
|
|
214
|
-
const [accounts, setAccounts] = useState<Account[]>([]);
|
|
215
|
-
|
|
216
|
-
useEffect(() => {
|
|
217
|
-
Account.retrieveActiveAccounts(apiService)
|
|
218
|
-
.then(setAccounts); // No error handling
|
|
219
|
-
}, []);
|
|
220
|
-
|
|
221
|
-
return <DetailsList items={accounts} columns={columns} />;
|
|
222
|
-
};
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
## Development Best Practices
|
|
226
|
-
|
|
227
|
-
### 1. Logging Strategy
|
|
228
|
-
|
|
229
|
-
#### ✅ DO: Use Structured Logging
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
// User actions
|
|
233
|
-
Logger.userAction(
|
|
234
|
-
'Account created',
|
|
235
|
-
{ accountId: result.accountid, accountName: result.name },
|
|
236
|
-
'AccountForm.handleSubmit'
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// API operations
|
|
240
|
-
Logger.apiOperation('CREATE', 'account', accountData, 'Account.create');
|
|
241
|
-
|
|
242
|
-
// Business logic
|
|
243
|
-
Logger.log('Processing high-value opportunity', 'SalesWorkflow.applyBusinessRules');
|
|
244
|
-
|
|
245
|
-
// Errors with context
|
|
246
|
-
Logger.error('Validation failed', 'AccountForm.validateForm', {
|
|
247
|
-
error,
|
|
248
|
-
formData,
|
|
249
|
-
validationErrors,
|
|
250
|
-
});
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
#### ❌ DON'T: Use Console Directly
|
|
254
|
-
|
|
255
|
-
```typescript
|
|
256
|
-
// Avoid direct console usage
|
|
257
|
-
console.log('Account created'); // No context
|
|
258
|
-
console.error(error); // No structured data
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
#### ✅ DO: Use Appropriate Log Levels
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
// Development debugging
|
|
265
|
-
Logger.debug('Form state updated', 'AccountForm.handleInputChange', formData);
|
|
266
|
-
|
|
267
|
-
// User feedback
|
|
268
|
-
Logger.userAction('Search performed', { searchTerm, resultCount }, 'AccountManagement');
|
|
269
|
-
|
|
270
|
-
// Performance monitoring
|
|
271
|
-
Logger.timing('Account list load', startTime, 'AccountManagement.loadAccounts');
|
|
272
|
-
|
|
273
|
-
// Validation feedback
|
|
274
|
-
Logger.validation('Account', errors, 'Account.validate');
|
|
275
|
-
|
|
276
|
-
// Critical errors
|
|
277
|
-
Logger.error('API service unavailable', 'ServiceFactory.createApiService', error);
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### 2. Error Handling Patterns
|
|
281
|
-
|
|
282
|
-
#### ✅ DO: Implement Layered Error Handling
|
|
283
|
-
|
|
284
|
-
```typescript
|
|
285
|
-
// Entity Layer - Business logic errors
|
|
286
|
-
export class Account extends BaseEntity {
|
|
287
|
-
public static async create(apiService: IApiService, account: Account): Promise<Account> {
|
|
288
|
-
try {
|
|
289
|
-
account.validate(); // Throws validation errors
|
|
290
|
-
return await this.createEntity<Account>(apiService, account, AccountConstants.EntityCollectionName);
|
|
291
|
-
} catch (error) {
|
|
292
|
-
Logger.error('Account creation failed', 'Account.create', error);
|
|
293
|
-
throw error; // Re-throw for component handling
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Component Layer - User feedback
|
|
299
|
-
export const AccountForm: React.FC = () => {
|
|
300
|
-
const [submitError, setSubmitError] = useState<string>('');
|
|
301
|
-
|
|
302
|
-
const handleSubmit = async () => {
|
|
303
|
-
try {
|
|
304
|
-
setSubmitError('');
|
|
305
|
-
const account = new Account(formData);
|
|
306
|
-
await Account.create(apiService, account);
|
|
307
|
-
onSave?.(); // Success callback
|
|
308
|
-
} catch (error) {
|
|
309
|
-
if (error instanceof Error) {
|
|
310
|
-
setSubmitError(error.message); // Show user-friendly message
|
|
311
|
-
} else {
|
|
312
|
-
setSubmitError('An unexpected error occurred');
|
|
313
|
-
}
|
|
314
|
-
Logger.error('Form submission failed', 'AccountForm.handleSubmit', error);
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
return (
|
|
319
|
-
<form onSubmit={handleSubmit}>
|
|
320
|
-
{submitError && <div className="error-message">{submitError}</div>}
|
|
321
|
-
{/* Form fields */}
|
|
322
|
-
</form>
|
|
323
|
-
);
|
|
324
|
-
};
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
#### ✅ DO: Provide User-Friendly Error Messages
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
const getErrorMessage = (error: unknown): string => {
|
|
331
|
-
if (error instanceof Error) {
|
|
332
|
-
// Handle specific error types
|
|
333
|
-
if (error.message.includes('validation failed')) {
|
|
334
|
-
return 'Please check your input and try again.';
|
|
335
|
-
}
|
|
336
|
-
if (error.message.includes('network')) {
|
|
337
|
-
return 'Unable to connect to server. Please check your connection.';
|
|
338
|
-
}
|
|
339
|
-
if (error.message.includes('permission')) {
|
|
340
|
-
return 'You do not have permission to perform this action.';
|
|
341
|
-
}
|
|
342
|
-
return error.message;
|
|
343
|
-
}
|
|
344
|
-
return 'An unexpected error occurred. Please try again.';
|
|
345
|
-
};
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
### 3. Performance Optimization
|
|
349
|
-
|
|
350
|
-
#### ✅ DO: Use React Performance Optimizations
|
|
351
|
-
|
|
352
|
-
```typescript
|
|
353
|
-
// Memoize expensive calculations
|
|
354
|
-
const filteredAccounts = useMemo(() => {
|
|
355
|
-
return accounts.filter(account =>
|
|
356
|
-
account.name?.toLowerCase().includes(searchText.toLowerCase())
|
|
357
|
-
);
|
|
358
|
-
}, [accounts, searchText]);
|
|
359
|
-
|
|
360
|
-
// Memoize callbacks to prevent unnecessary re-renders
|
|
361
|
-
const handleItemInvoked = useCallback((item: Account) => {
|
|
362
|
-
Logger.userAction('Account selected', { accountId: item.accountid });
|
|
363
|
-
setSelectedAccount(item);
|
|
364
|
-
}, []);
|
|
365
|
-
|
|
366
|
-
// Use React.memo for stable components
|
|
367
|
-
export const AccountListItem = React.memo<{ account: Account }>((props) => {
|
|
368
|
-
return <div>{props.account.name}</div>;
|
|
369
|
-
});
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
#### ✅ DO: Implement Efficient Data Loading
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
// Load only required fields
|
|
376
|
-
const fetchXml = Account.buildFetchXml(
|
|
377
|
-
AccountConstants.EntityLogicalName,
|
|
378
|
-
['accountid', 'name', 'emailaddress1'], // Only fields needed for list
|
|
379
|
-
filter,
|
|
380
|
-
{ attribute: 'createdon', descending: true }
|
|
381
|
-
);
|
|
382
|
-
|
|
383
|
-
// Implement pagination for large datasets
|
|
384
|
-
const loadAccountsPage = async (pageNumber: number, pageSize: number = 50) => {
|
|
385
|
-
const fetchXml = `
|
|
386
|
-
<fetch mapping="logical" count="${pageSize}" page="${pageNumber}">
|
|
387
|
-
<entity name="account">
|
|
388
|
-
<attribute name="accountid" />
|
|
389
|
-
<attribute name="name" />
|
|
390
|
-
<attribute name="emailaddress1" />
|
|
391
|
-
<order attribute="createdon" descending="true" />
|
|
392
|
-
</entity>
|
|
393
|
-
</fetch>`;
|
|
394
|
-
|
|
395
|
-
return await Account.retrieveEntitiesByFilter(apiService, 'accounts', fetchXml, Account);
|
|
396
|
-
};
|
|
397
|
-
```
|
|
398
|
-
|
|
399
|
-
### 4. Type Safety
|
|
400
|
-
|
|
401
|
-
#### ✅ DO: Use Strong Typing Throughout
|
|
402
|
-
|
|
403
|
-
```typescript
|
|
404
|
-
// Define proper interfaces
|
|
405
|
-
interface AccountFormData {
|
|
406
|
-
name: string;
|
|
407
|
-
emailaddress1: string;
|
|
408
|
-
telephone1: string;
|
|
409
|
-
revenue: number | null;
|
|
410
|
-
industrycode: number;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Use generic types with constraints
|
|
414
|
-
export class BaseEntity {
|
|
415
|
-
protected static async createEntity<T extends BaseEntity>(
|
|
416
|
-
apiService: IApiService,
|
|
417
|
-
entity: T,
|
|
418
|
-
entityName: string,
|
|
419
|
-
loggerContext?: string
|
|
420
|
-
): Promise<T> {
|
|
421
|
-
// Implementation
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Type component props properly
|
|
426
|
-
interface AccountFormProps {
|
|
427
|
-
accountId?: string;
|
|
428
|
-
initialData?: Partial<AccountFormData>;
|
|
429
|
-
onSave?: (account: Account) => void;
|
|
430
|
-
onCancel?: () => void;
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
#### ❌ DON'T: Use 'any' Unless Necessary
|
|
435
|
-
|
|
436
|
-
```typescript
|
|
437
|
-
// Avoid this - too permissive
|
|
438
|
-
const handleSubmit = (data: any) => {
|
|
439
|
-
// No type safety
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
// Better - use proper types
|
|
443
|
-
const handleSubmit = (data: AccountFormData) => {
|
|
444
|
-
// Full type safety
|
|
445
|
-
};
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## Deployment Best Practices
|
|
449
|
-
|
|
450
|
-
### 1. Build Optimization
|
|
451
|
-
|
|
452
|
-
#### ✅ DO: Use Environment-Specific Builds
|
|
453
|
-
|
|
454
|
-
```typescript
|
|
455
|
-
// Development build - unminified, with source maps
|
|
456
|
-
"build:dev": "node scripts/custom-build.js --dev"
|
|
457
|
-
|
|
458
|
-
// Production build - minified, optimized
|
|
459
|
-
"build:prod": "node scripts/custom-build.js"
|
|
460
|
-
|
|
461
|
-
// D365-specific build - single bundle, web resource ready
|
|
462
|
-
"build:d365": "node scripts/custom-build.js"
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
#### ✅ DO: Monitor Bundle Size
|
|
466
|
-
|
|
467
|
-
```typescript
|
|
468
|
-
// Check deployment info after build
|
|
469
|
-
const deploymentInfo = require('./dist/deployment-info.json');
|
|
470
|
-
console.log(`Bundle size: ${deploymentInfo.performance.totalSize} bytes`);
|
|
471
|
-
|
|
472
|
-
// Ensure size is under D365 limits
|
|
473
|
-
if (deploymentInfo.performance.totalSize > 2000000) {
|
|
474
|
-
// 2MB limit
|
|
475
|
-
console.warn('Bundle size exceeds recommended limit for web resources');
|
|
476
|
-
}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
### 2. Web Resource Management
|
|
480
|
-
|
|
481
|
-
#### ✅ DO: Use Consistent Naming Convention
|
|
482
|
-
|
|
483
|
-
```
|
|
484
|
-
// Web Resource Names
|
|
485
|
-
new_/scripts/accountmanagement_main.js
|
|
486
|
-
new_/scripts/contactmanagement_main.js
|
|
487
|
-
new_/styles/dynamics_ui_styles.css
|
|
488
|
-
new_/pages/accountmanagement.html
|
|
489
|
-
```
|
|
490
|
-
|
|
491
|
-
#### ✅ DO: Implement Proper Caching Strategy
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
// Add version numbers to web resources
|
|
495
|
-
const CACHE_VERSION = '1.0.0';
|
|
496
|
-
|
|
497
|
-
// Check for cached API services
|
|
498
|
-
const getCachedApiService = () => {
|
|
499
|
-
const cached = sessionStorage.getItem(`apiService_${CACHE_VERSION}`);
|
|
500
|
-
return cached ? JSON.parse(cached) : null;
|
|
501
|
-
};
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### 3. Security Considerations
|
|
505
|
-
|
|
506
|
-
#### ✅ DO: Validate User Permissions
|
|
507
|
-
|
|
508
|
-
```typescript
|
|
509
|
-
export class AccountService {
|
|
510
|
-
public static async create(apiService: IApiService, account: Account): Promise<Account> {
|
|
511
|
-
// Check user permissions before operations
|
|
512
|
-
if (!(await this.checkCreatePermission(apiService))) {
|
|
513
|
-
throw new Error('Insufficient permissions to create accounts');
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return await Account.create(apiService, account);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
private static async checkCreatePermission(apiService: IApiService): Promise<boolean> {
|
|
520
|
-
try {
|
|
521
|
-
// Use D365 security context
|
|
522
|
-
const userPrivileges = await apiService.executeRequest('RetrieveUserPrivileges', {});
|
|
523
|
-
return userPrivileges.some((p) => p.Name === 'prvCreateAccount');
|
|
524
|
-
} catch {
|
|
525
|
-
return false; // Fail secure
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
#### ✅ DO: Sanitize User Input
|
|
532
|
-
|
|
533
|
-
```typescript
|
|
534
|
-
export class BaseEntity {
|
|
535
|
-
protected static escapeXml(value: string): string {
|
|
536
|
-
if (!value) return value;
|
|
537
|
-
|
|
538
|
-
return value
|
|
539
|
-
.replace(/&/g, '&')
|
|
540
|
-
.replace(/</g, '<')
|
|
541
|
-
.replace(/>/g, '>')
|
|
542
|
-
.replace(/"/g, '"')
|
|
543
|
-
.replace(/'/g, ''');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
protected static validateInput(input: string, maxLength: number): string {
|
|
547
|
-
if (!input) return '';
|
|
548
|
-
|
|
549
|
-
// Remove potentially dangerous characters
|
|
550
|
-
const cleaned = input.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
|
551
|
-
|
|
552
|
-
// Enforce length limits
|
|
553
|
-
return cleaned.substring(0, maxLength);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### 4. Testing Strategy
|
|
559
|
-
|
|
560
|
-
#### ✅ DO: Test in Multiple Environments
|
|
561
|
-
|
|
562
|
-
```typescript
|
|
563
|
-
// Environment-specific testing
|
|
564
|
-
describe('Account Management', () => {
|
|
565
|
-
beforeEach(() => {
|
|
566
|
-
// Test with mock service
|
|
567
|
-
const mockService = new MockApiService();
|
|
568
|
-
render(
|
|
569
|
-
<DynamicsProvider customApiService={mockService}>
|
|
570
|
-
<AccountManagement />
|
|
571
|
-
</DynamicsProvider>
|
|
572
|
-
);
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
test('should create account successfully', async () => {
|
|
576
|
-
// Test implementation
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
test('should handle validation errors', async () => {
|
|
580
|
-
// Test error scenarios
|
|
581
|
-
});
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
// Integration testing with real D365
|
|
585
|
-
describe('Account Management - Integration', () => {
|
|
586
|
-
beforeEach(() => {
|
|
587
|
-
// Test with real Xrm service (in development environment)
|
|
588
|
-
const xrmService = new XrmApiService((window as any).Xrm);
|
|
589
|
-
render(
|
|
590
|
-
<DynamicsProvider customApiService={xrmService}>
|
|
591
|
-
<AccountManagement />
|
|
592
|
-
</DynamicsProvider>
|
|
593
|
-
);
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
#### ✅ DO: Implement Quality Gates
|
|
599
|
-
|
|
600
|
-
```typescript
|
|
601
|
-
// Pre-commit quality checks
|
|
602
|
-
"precommit": "npm run quality"
|
|
603
|
-
"quality": "npm run lint && npm run typecheck"
|
|
604
|
-
|
|
605
|
-
// Pre-deployment validation
|
|
606
|
-
"validate": "npm run quality && npm run build:prod"
|
|
607
|
-
"prepublishOnly": "npm run validate"
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
## Maintenance Best Practices
|
|
611
|
-
|
|
612
|
-
### 1. Documentation
|
|
613
|
-
|
|
614
|
-
#### ✅ DO: Maintain Comprehensive Documentation
|
|
615
|
-
|
|
616
|
-
````typescript
|
|
617
|
-
/**
|
|
618
|
-
* Creates a new account with full validation and business rule enforcement
|
|
619
|
-
*
|
|
620
|
-
* @param apiService - The API service instance for Dynamics 365 operations
|
|
621
|
-
* @param account - The account instance to create
|
|
622
|
-
* @returns Promise resolving to the created account with server-generated fields
|
|
623
|
-
*
|
|
624
|
-
* @throws {Error} When validation fails or API operation fails
|
|
625
|
-
*
|
|
626
|
-
* @example
|
|
627
|
-
* ```typescript
|
|
628
|
-
* const account = new Account({ name: 'ACME Corp', emailaddress1: 'info@acme.com' });
|
|
629
|
-
* const created = await Account.create(apiService, account);
|
|
630
|
-
* console.log('Created account:', created.accountid);
|
|
631
|
-
* ```
|
|
632
|
-
*/
|
|
633
|
-
public static async create(apiService: IApiService, account: Account): Promise<Account> {
|
|
634
|
-
// Implementation
|
|
635
|
-
}
|
|
636
|
-
````
|
|
637
|
-
|
|
638
|
-
### 2. Monitoring and Logging
|
|
639
|
-
|
|
640
|
-
#### ✅ DO: Implement Production Monitoring
|
|
641
|
-
|
|
642
|
-
```typescript
|
|
643
|
-
// Performance monitoring
|
|
644
|
-
const performanceObserver = new PerformanceObserver((list) => {
|
|
645
|
-
for (const entry of list.getEntries()) {
|
|
646
|
-
if (entry.duration > 1000) {
|
|
647
|
-
// Log slow operations
|
|
648
|
-
Logger.performance('Slow operation detected', {
|
|
649
|
-
name: entry.name,
|
|
650
|
-
duration: entry.duration,
|
|
651
|
-
context: 'ProductionMonitoring',
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
// Error tracking with context
|
|
658
|
-
window.addEventListener('error', (event) => {
|
|
659
|
-
Logger.error('Unhandled error in production', 'GlobalErrorHandler', {
|
|
660
|
-
message: event.error?.message,
|
|
661
|
-
filename: event.filename,
|
|
662
|
-
lineno: event.lineno,
|
|
663
|
-
colno: event.colno,
|
|
664
|
-
stack: event.error?.stack,
|
|
665
|
-
});
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
// API failure tracking
|
|
669
|
-
const trackApiFailures = (operation: string, entityName: string, error: Error) => {
|
|
670
|
-
Logger.error(`API operation failed: ${operation}`, 'ApiFailureTracker', {
|
|
671
|
-
operation,
|
|
672
|
-
entityName,
|
|
673
|
-
error: error.message,
|
|
674
|
-
timestamp: new Date().toISOString(),
|
|
675
|
-
userAgent: navigator.userAgent,
|
|
676
|
-
});
|
|
677
|
-
};
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
### 3. Version Management
|
|
681
|
-
|
|
682
|
-
#### ✅ DO: Implement Semantic Versioning
|
|
683
|
-
|
|
684
|
-
```json
|
|
685
|
-
{
|
|
686
|
-
"name": "dynamics-365-app",
|
|
687
|
-
"version": "1.2.3", // MAJOR.MINOR.PATCH
|
|
688
|
-
"description": "Version changes tracked in CHANGELOG.md"
|
|
689
|
-
}
|
|
690
|
-
```
|
|
691
|
-
|
|
692
|
-
#### ✅ DO: Maintain Backward Compatibility
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
// Support both old and new API patterns
|
|
696
|
-
export class Account extends BaseEntity {
|
|
697
|
-
// New method (preferred)
|
|
698
|
-
public static async retrieveByName(apiService: IApiService, name: string): Promise<Account[]> {
|
|
699
|
-
// Implementation
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Legacy method (deprecated but supported)
|
|
703
|
-
/** @deprecated Use retrieveByName instead */
|
|
704
|
-
public static async findByName(apiService: IApiService, name: string): Promise<Account[]> {
|
|
705
|
-
Logger.warn('findByName is deprecated, use retrieveByName', 'Account.findByName');
|
|
706
|
-
return this.retrieveByName(apiService, name);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
## Conclusion
|
|
712
|
-
|
|
713
|
-
Following these best practices ensures that your Dynamics 365 applications are:
|
|
714
|
-
|
|
715
|
-
- **Robust**: Comprehensive error handling and validation
|
|
716
|
-
- **Maintainable**: Clean architecture and proper documentation
|
|
717
|
-
- **Performant**: Optimized builds and efficient data loading
|
|
718
|
-
- **Secure**: Input validation and permission checking
|
|
719
|
-
- **Scalable**: Modular architecture that grows with your needs
|
|
720
|
-
- **Type-Safe**: Full TypeScript support throughout
|
|
721
|
-
|
|
722
|
-
Regular review and adherence to these practices will result in high-quality, enterprise-grade
|
|
723
|
-
Dynamics 365 applications.
|