@xrmforge/devkit 0.7.3 → 0.7.5
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/templates/AGENT.md
CHANGED
|
@@ -65,7 +65,7 @@ export const onLoad = wrapHandler('LM.Account.onLoad', logger, (ctx) => {
|
|
|
65
65
|
const form = typedForm<AccountLMFirmaForm>(ctx.getFormContext());
|
|
66
66
|
|
|
67
67
|
// Direct field access - fully typed, IDE autocomplete works
|
|
68
|
-
const name = form.name.getValue(); // string | null
|
|
68
|
+
const name = form.name.getValue(); // string | null (non-nullable, field is on form)
|
|
69
69
|
form.revenue.setValue(150000); // NumberAttribute
|
|
70
70
|
const parent = form.parentaccountid.getValue(); // LookupValue[] | null
|
|
71
71
|
|
|
@@ -85,8 +85,29 @@ export const onLoad = wrapHandler('LM.Account.onLoad', logger, (ctx) => {
|
|
|
85
85
|
|
|
86
86
|
**When to use `form.fieldname` (the typedForm proxy):**
|
|
87
87
|
- `getValue()`, `setValue()` (reading and writing values)
|
|
88
|
+
- `addOnChange()` (event registration directly on the attribute)
|
|
88
89
|
- Any read-only access to field values
|
|
89
90
|
|
|
91
|
+
**When to use `form.$unsafe(EntityFields.X)` (off-form fields):**
|
|
92
|
+
|
|
93
|
+
D365 loads all entity attributes into the FormContext, not just those on the form.
|
|
94
|
+
If a field is NOT in the generated form interface (compile error on `form.fieldname`),
|
|
95
|
+
use `$unsafe()` with the **Entity-level Fields Enum** (never a raw string):
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { OpportunityFields } from '../../generated/fields/opportunity.js';
|
|
99
|
+
|
|
100
|
+
// Off-form field: not in the form interface, but loaded by D365
|
|
101
|
+
form.$unsafe(OpportunityFields.VslBeauftragung)?.setValue(closeDate);
|
|
102
|
+
|
|
103
|
+
// WRONG: raw string in $unsafe
|
|
104
|
+
form.$unsafe('estimatedclosedate')?.setValue(closeDate);
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`$unsafe()` returns `Attribute | null` (nullable, because the field may not exist).
|
|
108
|
+
Always use optional chaining (`?.`). The Entity-level Fields Enum ensures the field
|
|
109
|
+
name is valid even though it's not on the form.
|
|
110
|
+
|
|
90
111
|
### 2. Fields Enum for ALL getAttribute/getControl AND select() calls
|
|
91
112
|
|
|
92
113
|
Two types of Fields enums exist:
|
|
@@ -334,6 +355,8 @@ Xrm.Navigation.openForm({ entityName: EntityNames.Account, entityId: id }); //
|
|
|
334
355
|
- Never access WebApi response properties with `as string` casts (use generated Entity interfaces)
|
|
335
356
|
- Never `.getValue()[0].id` for lookups (use `formLookup`/`formLookupId`)
|
|
336
357
|
- Never raw strings in `parseLookup()` (use NavigationProperties enum)
|
|
358
|
+
- Never raw strings in `$unsafe()` (use Entity-level Fields Enum: `form.$unsafe(AccountFields.X)`)
|
|
359
|
+
- Never manual OData annotation access (`_value`, `@OData.Community.Display.V1.FormattedValue`, `@Microsoft.Dynamics.CRM.lookuplogicalname`). Use `parseLookup()` which extracts all three.
|
|
337
360
|
|
|
338
361
|
**Code quality:**
|
|
339
362
|
- Never `Xrm.Page` (deprecated since D365 v9.0)
|
|
@@ -438,13 +461,32 @@ import { LmBestellungStatusCode } from '../../generated/optionsets/lm_bestellung
|
|
|
438
461
|
</condition>`
|
|
439
462
|
```
|
|
440
463
|
|
|
441
|
-
### Lookup Access
|
|
464
|
+
### Lookup Access (Form)
|
|
442
465
|
```typescript
|
|
443
466
|
// BEFORE: form.getAttribute("customerid").getValue()[0].id.replace("{","").replace("}","")
|
|
444
467
|
// AFTER:
|
|
445
468
|
const customerId = formLookupId(form.customerid);
|
|
446
469
|
```
|
|
447
470
|
|
|
471
|
+
### Lookup from WebApi Response to Form Field
|
|
472
|
+
```typescript
|
|
473
|
+
// BEFORE (verbose, error-prone OData annotations):
|
|
474
|
+
form.customerid.setValue([{
|
|
475
|
+
id: raw._parentcustomerid_value as string,
|
|
476
|
+
entityType: EntityNames.Account,
|
|
477
|
+
name: (raw['_parentcustomerid_value@OData.Community.Display.V1.FormattedValue'] as string) ?? '',
|
|
478
|
+
}]);
|
|
479
|
+
|
|
480
|
+
// AFTER (parseLookup extracts id, name, entityType automatically):
|
|
481
|
+
import { parseLookup } from '@xrmforge/helpers';
|
|
482
|
+
import { ContactNavigationProperties as ContactNav } from '../../generated/entities/contact.js';
|
|
483
|
+
|
|
484
|
+
const customer = parseLookup(raw, ContactNav.ParentCustomerId);
|
|
485
|
+
if (customer) {
|
|
486
|
+
form.customerid.setValue([customer]);
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
448
490
|
### Custom API Call
|
|
449
491
|
```typescript
|
|
450
492
|
// BEFORE: ExecuteFunctionCall("CancelInvoice", { InvoiceId: id })
|
|
@@ -218,6 +218,34 @@ checkPattern(
|
|
|
218
218
|
['logger.ts'],
|
|
219
219
|
);
|
|
220
220
|
|
|
221
|
+
// ── Type Safety Bypass ───────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
// 3l2. Cast to Xrm.FormContext (bypasses typed form interface)
|
|
224
|
+
checkPattern(
|
|
225
|
+
'Cast to Xrm.FormContext (use typedForm $unsafe() for off-form fields)',
|
|
226
|
+
formFiles,
|
|
227
|
+
/as\s+(?:unknown\s+as\s+)?Xrm\.FormContext/,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// 3l3. Raw strings in $filter (field names must use Fields Enum interpolation)
|
|
231
|
+
checkPattern(
|
|
232
|
+
'Raw field names in $filter (use Fields Enum interpolation)',
|
|
233
|
+
allSrcFiles,
|
|
234
|
+
/\$filter=[^$]*\b(?:eq|ne|gt|lt|ge|le|contains|startswith)\b/,
|
|
235
|
+
['generated/', 'validate-form'],
|
|
236
|
+
[
|
|
237
|
+
// Allow if the line contains template literal interpolation (${...})
|
|
238
|
+
/\$\{/,
|
|
239
|
+
],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// 3l4. Raw strings in $unsafe() (must use Entity-level Fields Enum)
|
|
243
|
+
checkPattern(
|
|
244
|
+
'Raw field strings in $unsafe() (use Entity-level Fields Enum)',
|
|
245
|
+
formFiles,
|
|
246
|
+
/\$unsafe\s*\(\s*['"][a-z]/,
|
|
247
|
+
);
|
|
248
|
+
|
|
221
249
|
// ── Handler Pattern ──────────────────────────────────────────────────────────
|
|
222
250
|
|
|
223
251
|
// 3l. Exported handlers without wrapHandler or wrapCommand
|
|
@@ -260,6 +288,60 @@ checkPattern(
|
|
|
260
288
|
['generated/'],
|
|
261
289
|
);
|
|
262
290
|
|
|
291
|
+
// ── WebApi Response Typing ────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
// 3p. Untyped WebApi responses (must use generated Entity interfaces)
|
|
294
|
+
checkPattern(
|
|
295
|
+
'Untyped WebApi response cast (use generated Entity interface instead of Record<string, unknown>)',
|
|
296
|
+
allSrcFiles,
|
|
297
|
+
/as\s+Record\s*<\s*string\s*,\s*unknown\s*>/,
|
|
298
|
+
['generated/'],
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// 3q. Manual OData annotation access (use parseLookup instead)
|
|
302
|
+
checkPattern(
|
|
303
|
+
'Manual OData annotation access (use parseLookup() from @xrmforge/helpers)',
|
|
304
|
+
allSrcFiles,
|
|
305
|
+
/@OData\.Community\.Display|@Microsoft\.Dynamics\.CRM\.lookuplogicalname|_value(?:@|\s*as\s)/,
|
|
306
|
+
['generated/', 'node_modules'],
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// ── Legacy Helper Wrappers ───────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
// 3r. Forbidden legacy helper functions (must use typedForm + @xrmforge/helpers)
|
|
312
|
+
checkPattern(
|
|
313
|
+
'Forbidden helper: getLookupId (use formLookupId from @xrmforge/helpers)',
|
|
314
|
+
allSrcFiles,
|
|
315
|
+
/\bgetLookupId\s*\(/,
|
|
316
|
+
['generated/'],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
checkPattern(
|
|
320
|
+
'Forbidden helper: setLookupValue (use form.field.setValue([{...}]))',
|
|
321
|
+
allSrcFiles,
|
|
322
|
+
/\bsetLookupValue\s*\(/,
|
|
323
|
+
['generated/'],
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// ── UI Localization ──────────────────────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
// 3s. Hardcoded UI strings in dialogs/progress (must use pickLang from constants.ts)
|
|
329
|
+
checkPattern(
|
|
330
|
+
'Hardcoded UI string in dialog/progress (use pickLang(MESSAGES) from constants.ts)',
|
|
331
|
+
allSrcFiles,
|
|
332
|
+
/(?:openAlertDialog|openConfirmDialog|openErrorDialog|showProgressIndicator)\s*\(\s*(?:\{\s*text\s*:\s*)?['"]/,
|
|
333
|
+
['generated/', 'constants.ts'],
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// ── Duplicate Framework Functions ────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
// 3t. Own normalizeGuid/compareGuid (use normalizeGuid from @xrmforge/helpers)
|
|
339
|
+
checkPattern(
|
|
340
|
+
'Own normalizeGuid/compareGuid definition (use normalizeGuid from @xrmforge/helpers)',
|
|
341
|
+
allSrcFiles,
|
|
342
|
+
/(?:export\s+)?function\s+(?:normalizeGuid|compareGuid)\s*\(/,
|
|
343
|
+
);
|
|
344
|
+
|
|
263
345
|
// ============================================================
|
|
264
346
|
// Result
|
|
265
347
|
// ============================================================
|