@xrmforge/devkit 0.7.0 → 0.7.2
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 +79 -6
- package/package.json +1 -1
package/dist/templates/AGENT.md
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
# XrmForge - AI Agent Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Quality Philosophy
|
|
4
|
+
|
|
5
|
+
The goal is not "code that compiles" or "code that passes a linter". The goal is
|
|
6
|
+
code that reads like a description of the business logic. A developer opening a
|
|
7
|
+
file should immediately understand what happens, without Xrm API docs, without
|
|
8
|
+
OData knowledge, without deciphering GUIDs or magic numbers.
|
|
9
|
+
|
|
10
|
+
Every string that references a Dataverse resource (field name, entity name,
|
|
11
|
+
OptionSet value, tab name, section name, notification ID, navigation property)
|
|
12
|
+
MUST come from a generated constant or a named constant from constants.ts.
|
|
13
|
+
No exceptions. No workarounds. No helper wrappers that accept raw strings.
|
|
14
|
+
|
|
15
|
+
Abstraction layers that merely wrap single API calls with string parameters
|
|
16
|
+
(getValue, setValue, setDisabled, addOnChange) destroy type safety and must not
|
|
17
|
+
exist. The correct abstraction is `typedForm()` (language-level proxy), not
|
|
18
|
+
string wrappers (API-level indirection). Business logic belongs in named
|
|
19
|
+
functions with domain-specific names, not in anonymous chains of API calls.
|
|
4
20
|
|
|
5
21
|
## Packages
|
|
6
22
|
|
|
@@ -484,22 +500,79 @@ each attribute to its control. `mock.getControl(Fields.Name)` works out of the b
|
|
|
484
500
|
|
|
485
501
|
## Pattern Recognition: Legacy to XrmForge
|
|
486
502
|
|
|
503
|
+
### Xrm API Patterns
|
|
487
504
|
| Legacy Pattern | XrmForge Replacement |
|
|
488
505
|
|---|---|
|
|
489
|
-
| `getAttribute("name")` | `form.name` (via typedForm)
|
|
506
|
+
| `getAttribute("name")` | `form.name` (via typedForm) |
|
|
490
507
|
| `getControl("name")` | `form.$control(Fields.Name)` |
|
|
491
|
-
| `getValue() === 595300000` | `form.statuscode.getValue() === StatusCode.Active` |
|
|
492
|
-
| `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
|
|
493
|
-
| `"?$select=name,revenue"` | `select(AccountFields.Name, AccountFields.Revenue)` |
|
|
494
|
-
| `value[0].id.replace("{","")` | `formLookupId(form.customerid)` |
|
|
495
508
|
| `Xrm.Page.getAttribute(...)` | `form.fieldname` (via typedForm) |
|
|
496
509
|
| `var formContext` (global) | `const form = typedForm<MyForm>(ctx.getFormContext())` |
|
|
497
510
|
| `function form_OnLoad(ctx)` | `export const onLoad = wrapHandler(...)` |
|
|
511
|
+
| `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
|
|
512
|
+
| `"?$select=name,revenue"` | `select(AccountFields.Name, AccountFields.Revenue)` |
|
|
513
|
+
| `value[0].id.replace("{","")` | `formLookupId(form.customerid)` |
|
|
498
514
|
| `ExecuteFunctionCall("name", ...)` | `import { Name } from '../../generated/actions/global.js'` |
|
|
499
515
|
| `setFormNotification(msg, 'ERROR', id)` | `setFormNotification(msg, FormNotificationLevel.Error, id)` |
|
|
516
|
+
| `getValue() === 595300000` | `form.statuscode.getValue() === StatusCode.Active` |
|
|
500
517
|
| `86400000` | `const MS_PER_DAY = 24 * 60 * 60 * 1000` |
|
|
501
518
|
| `'[Kurzbeschreibung]'` | `pickLang(languageId, MESSAGES).placeholder` |
|
|
502
519
|
|
|
520
|
+
### Legacy Helper Functions (DO NOT recreate, use typedForm instead)
|
|
521
|
+
|
|
522
|
+
These helper wrappers are common in legacy code. They destroy type safety.
|
|
523
|
+
Never recreate them. Use the typed API directly.
|
|
524
|
+
|
|
525
|
+
| Legacy Helper | XrmForge Replacement |
|
|
526
|
+
|---|---|
|
|
527
|
+
| `GetValue(fieldName)` | `form.fieldname.getValue()` (typed via typedForm) |
|
|
528
|
+
| `SetValue(fieldName, value)` | `form.fieldname.setValue(value)` (typed via typedForm) |
|
|
529
|
+
| `SetDisabled(attributeName, disabled)` | `form.$control(Fields.X).setDisabled(disabled)` |
|
|
530
|
+
| `SetVisible(attributeName, visible)` | `form.$control(Fields.X).setVisible(visible)` |
|
|
531
|
+
| `SetRequiredLevel(attributeName, level)` | `form.$context.getAttribute(Fields.X).setRequiredLevel(RequiredLevel.Required)` |
|
|
532
|
+
| `AddOnChange(attributeName, callback)` | `form.$context.getAttribute(Fields.X).addOnChange(cb)` |
|
|
533
|
+
| `AddPreSearch(controlName, callback)` | `(form.$control(Fields.X) as Xrm.Controls.LookupControl).addPreSearch(cb)` |
|
|
534
|
+
| `GetLookupValueId(fieldName)` | `formLookupId(form.fieldname)` |
|
|
535
|
+
| `SetLookupValue(field, id, type, name)` | `form.fieldname.setValue([{ id, entityType, name }])` |
|
|
536
|
+
| `GetId()` | `form.$context.data.entity.getId()` |
|
|
537
|
+
| `GetEntityName()` | `form.$context.data.entity.getEntityName()` |
|
|
538
|
+
| `GetFormType()` | `form.$context.ui.getFormType()` |
|
|
539
|
+
| `GetIsDirty()` | `form.$context.data.entity.getIsDirty()` |
|
|
540
|
+
| `IsNullOrEmpty(value)` | `value == null \|\| value === ''` (inline) |
|
|
541
|
+
| `IsAttributeNullOrEmpty(field)` | `form.fieldname.getValue() == null` |
|
|
542
|
+
| `GetUserId()` | `Xrm.Utility.getGlobalContext().userSettings.userId` |
|
|
543
|
+
| `GetUserLanguageId()` | `Xrm.Utility.getGlobalContext().userSettings.languageId` |
|
|
544
|
+
| `OpenForm(entityName, id)` | `Xrm.Navigation.openForm({ entityName: EntityNames.X, entityId: id })` |
|
|
545
|
+
| `OpenAlertDialog(text)` | `Xrm.Navigation.openAlertDialog({ text })` |
|
|
546
|
+
| `OpenConfirmDialog(text, ...)` | `Xrm.Navigation.openConfirmDialog({ text, title, ... })` |
|
|
547
|
+
| `ShowProgressIndicator(msg)` | `Xrm.Utility.showProgressIndicator(msg)` |
|
|
548
|
+
| `CloseProgressIndicator()` | `Xrm.Utility.closeProgressIndicator()` |
|
|
549
|
+
| `SetNotification(attr, msg)` | `form.$context.getControl(Fields.X).setNotification(msg, NOTIFICATION_IDS.x)` |
|
|
550
|
+
| `SetSectionDisabled(tab, sec, off)` | `form.$context.ui.tabs.get(Tabs.X).sections.get(Sections.Y).setVisible(!off)` |
|
|
551
|
+
|
|
552
|
+
### GUID Handling (common CRM anti-pattern)
|
|
553
|
+
|
|
554
|
+
D365 returns GUIDs in various formats: `{A1B2C3D4-...}`, `a1b2c3d4-...`, `A1B2C3D4-...`.
|
|
555
|
+
Legacy code commonly has helpers like `CompareGuid()`, `GetCompatibleGuid()`,
|
|
556
|
+
`NormalizeGuid()`, `StripBraces()`. **Do NOT recreate these.**
|
|
557
|
+
|
|
558
|
+
`formLookupId()` from @xrmforge/helpers already normalizes GUIDs (removes braces).
|
|
559
|
+
GUID comparison is then a simple `===`:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// WRONG: legacy GUID helpers
|
|
563
|
+
function CompareGuid(a, b) { return a.replace(/[{}]/g,'').toLowerCase() === b.replace(/[{}]/g,'').toLowerCase(); }
|
|
564
|
+
const id = GetCompatibleGuid(form.getAttribute("customerid").getValue()[0].id);
|
|
565
|
+
|
|
566
|
+
// CORRECT: formLookupId normalizes automatically
|
|
567
|
+
const customerId = formLookupId(form.customerid); // already clean: "a1b2c3d4-..."
|
|
568
|
+
if (customerId === otherNormalizedId) { ... } // simple ===
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Rule of thumb:** If a helper function just wraps a single Xrm API call with a
|
|
572
|
+
string parameter, it MUST NOT exist. The typed API is shorter, safer, and provides
|
|
573
|
+
IDE autocomplete. Only keep shared helpers that contain actual domain logic
|
|
574
|
+
(calculations, WebApi queries, multi-step workflows).
|
|
575
|
+
|
|
503
576
|
## @types/xrm Pitfalls (known issues)
|
|
504
577
|
|
|
505
578
|
1. **Form Interface:** Do NOT use `interface extends Xrm.FormContext`. Use `Omit` pattern.
|