@khester/create-dynamics-app 2.0.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 (122) hide show
  1. package/README.md +28 -0
  2. package/dist/artifacts/registry.d.ts +4 -3
  3. package/dist/artifacts/registry.d.ts.map +1 -1
  4. package/dist/artifacts/registry.js +145 -11
  5. package/dist/artifacts/registry.js.map +1 -1
  6. package/dist/artifacts/types.d.ts +10 -1
  7. package/dist/artifacts/types.d.ts.map +1 -1
  8. package/dist/index.js +19 -2
  9. package/dist/index.js.map +1 -1
  10. package/dist/injectDevTools.d.ts.map +1 -1
  11. package/dist/injectDevTools.js +4 -2
  12. package/dist/injectDevTools.js.map +1 -1
  13. package/dist/scaffold.d.ts +23 -1
  14. package/dist/scaffold.d.ts.map +1 -1
  15. package/dist/scaffold.js +27 -1
  16. package/dist/scaffold.js.map +1 -1
  17. package/package.json +3 -2
  18. package/templates/grid-starter/ARCHITECTURE.md +66 -0
  19. package/templates/grid-starter/README.md +122 -0
  20. package/templates/grid-starter/env.example +16 -0
  21. package/templates/grid-starter/gitignore +6 -0
  22. package/templates/grid-starter/index.html +16 -0
  23. package/templates/grid-starter/package.json +39 -0
  24. package/templates/grid-starter/src/App.tsx +23 -0
  25. package/templates/grid-starter/src/core/services/FetchApiService.ts +117 -0
  26. package/templates/grid-starter/src/core/services/IApiService.ts +37 -0
  27. package/templates/grid-starter/src/core/services/MockApiService.ts +72 -0
  28. package/templates/grid-starter/src/core/services/ServiceFactory.ts +58 -0
  29. package/templates/grid-starter/src/core/services/XrmApiService.ts +135 -0
  30. package/templates/grid-starter/src/core/services/crudLogging.ts +52 -0
  31. package/templates/grid-starter/src/dev-tools/DevPanel.tsx +239 -0
  32. package/templates/grid-starter/src/grid/GridPage.tsx +119 -0
  33. package/templates/grid-starter/src/index.tsx +18 -0
  34. package/templates/grid-starter/src/vite-env.d.ts +15 -0
  35. package/templates/grid-starter/tools/deploy/deploy-webresource.cjs +117 -0
  36. package/templates/grid-starter/tsconfig.json +19 -0
  37. package/templates/grid-starter/vite.config.ts +76 -0
  38. package/templates/pcf-field/_variants/ValueInput.boolean.tsx +2 -0
  39. package/templates/pcf-field/_variants/ValueInput.date.tsx +2 -0
  40. package/templates/pcf-field/_variants/ValueInput.number.tsx +2 -0
  41. package/templates/pcf-field/_variants/ValueInput.optionset.tsx +77 -0
  42. package/templates/pcf-field/_variants/ValueInput.text.tsx +2 -0
  43. package/templates/pcf-field/index.ts +1 -1
  44. package/templates/pcf-field/package.json +3 -1
  45. package/templates/pcf-field/{{componentName}}Component.tsx +2 -0
  46. package/templates/react-custom-page/ARCHITECTURE.md +75 -0
  47. package/templates/react-custom-page/README.md +74 -568
  48. package/templates/react-custom-page/env.example +16 -0
  49. package/templates/react-custom-page/gitignore +1 -0
  50. package/templates/react-custom-page/index.html +16 -0
  51. package/templates/react-custom-page/package.json +21 -49
  52. package/templates/react-custom-page/src/App.tsx +26 -0
  53. package/templates/react-custom-page/src/core/recordContext.test.ts +30 -0
  54. package/templates/react-custom-page/src/core/recordContext.ts +51 -0
  55. package/templates/react-custom-page/src/core/services/FetchApiService.ts +117 -0
  56. package/templates/react-custom-page/src/core/services/IApiService.ts +37 -0
  57. package/templates/react-custom-page/src/core/services/MockApiService.ts +73 -0
  58. package/templates/react-custom-page/src/core/services/ServiceFactory.ts +58 -0
  59. package/templates/react-custom-page/src/core/services/XrmApiService.ts +135 -0
  60. package/templates/react-custom-page/src/core/services/crudLogging.ts +52 -0
  61. package/templates/react-custom-page/src/dev-tools/DevPanel.tsx +238 -0
  62. package/templates/react-custom-page/src/domain/diff.test.ts +87 -0
  63. package/templates/react-custom-page/src/domain/diff.ts +38 -0
  64. package/templates/react-custom-page/src/example/ExamplePage.tsx +140 -0
  65. package/templates/react-custom-page/src/example/exampleError.ts +36 -0
  66. package/templates/react-custom-page/src/example/hooks/useExampleData.ts +40 -0
  67. package/templates/react-custom-page/src/example/hooks/useExampleForm.ts +99 -0
  68. package/templates/react-custom-page/src/example/mappers/accountMapper.test.ts +38 -0
  69. package/templates/react-custom-page/src/example/mappers/accountMapper.ts +55 -0
  70. package/templates/react-custom-page/src/example/models/Account.ts +74 -0
  71. package/templates/react-custom-page/src/index.tsx +18 -128
  72. package/templates/react-custom-page/src/vite-env.d.ts +15 -0
  73. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +117 -0
  74. package/templates/react-custom-page/tsconfig.json +12 -22
  75. package/templates/react-custom-page/vite.config.ts +76 -0
  76. package/templates/starter-page/README.md +38 -0
  77. package/templates/starter-page/_variants/App.dashboard.v8.tsx +46 -0
  78. package/templates/starter-page/_variants/App.form.v8.tsx +59 -0
  79. package/templates/starter-page/_variants/App.master-detail.v8.tsx +61 -0
  80. package/templates/starter-page/_variants/App.panel.v8.tsx +99 -0
  81. package/templates/starter-page/gitignore +5 -0
  82. package/templates/starter-page/package.json +27 -0
  83. package/templates/starter-page/public/index.html +11 -0
  84. package/templates/starter-page/src/index.tsx +10 -0
  85. package/templates/starter-page/src/services/dataverse.ts +30 -0
  86. package/templates/starter-page/tsconfig.json +15 -0
  87. package/templates/starter-page/webpack.config.js +17 -0
  88. package/templates/react-custom-page/deployment/README.md +0 -484
  89. package/templates/react-custom-page/docs/ARCHITECTURE_OVERVIEW.md +0 -506
  90. package/templates/react-custom-page/docs/BEST_PRACTICES.md +0 -723
  91. package/templates/react-custom-page/docs/MIGRATION_GUIDE.md +0 -447
  92. package/templates/react-custom-page/public/index.html +0 -15
  93. package/templates/react-custom-page/scripts/custom-build.js +0 -255
  94. package/templates/react-custom-page/src/components/AccountForm.css +0 -71
  95. package/templates/react-custom-page/src/components/AccountForm.tsx +0 -541
  96. package/templates/react-custom-page/src/components/AccountManagement.css +0 -86
  97. package/templates/react-custom-page/src/components/AccountManagement.tsx +0 -370
  98. package/templates/react-custom-page/src/components/ContactForm.css +0 -48
  99. package/templates/react-custom-page/src/components/ContactForm.tsx +0 -327
  100. package/templates/react-custom-page/src/components/ContactManagement.css +0 -86
  101. package/templates/react-custom-page/src/components/ContactManagement.tsx +0 -357
  102. package/templates/react-custom-page/src/components/Logging/LogDialog.tsx +0 -291
  103. package/templates/react-custom-page/src/components/Logging/LoggingContext.tsx +0 -166
  104. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.css +0 -192
  105. package/templates/react-custom-page/src/components/Logging/LoggingDebugPanel.tsx +0 -177
  106. package/templates/react-custom-page/src/components/Logging/LoggingProvider.tsx +0 -3
  107. package/templates/react-custom-page/src/components/Logging/logger.ts +0 -193
  108. package/templates/react-custom-page/src/constants/account.ts +0 -410
  109. package/templates/react-custom-page/src/constants/contact.ts +0 -362
  110. package/templates/react-custom-page/src/models/Account.ts +0 -480
  111. package/templates/react-custom-page/src/models/BaseEntity.ts +0 -204
  112. package/templates/react-custom-page/src/models/Contact.ts +0 -580
  113. package/templates/react-custom-page/src/pcf/ContactControlWrapper.tsx +0 -107
  114. package/templates/react-custom-page/src/pcf/MultiEntityControlWrapper.tsx +0 -205
  115. package/templates/react-custom-page/src/providers/DynamicsProvider.tsx +0 -353
  116. package/templates/react-custom-page/src/services/MockApiService.ts +0 -260
  117. package/templates/react-custom-page/src/services/ServiceFactory.ts +0 -65
  118. package/templates/react-custom-page/src/services/XrmApiService.ts +0 -213
  119. package/templates/react-custom-page/src/styles/index.css +0 -171
  120. package/templates/react-custom-page/tools/metadata-sync/index.js +0 -152
  121. package/templates/react-custom-page/webpack.config.js +0 -57
  122. /package/templates/_shared/dev-tools/auth/{get-token.js → get-token.cjs} +0 -0
package/dist/scaffold.js CHANGED
@@ -21,6 +21,25 @@ export function resolveLibraryFromFlags(options) {
21
21
  }
22
22
  return undefined;
23
23
  }
24
+ const COMPONENT_HOSTS = 'power-pages, web-resource, dialog-form';
25
+ export function resolveWithComponents(descriptor, library, withComponents) {
26
+ if (!withComponents)
27
+ return { kind: 'skip' };
28
+ if (!descriptor.supportsComponents) {
29
+ return {
30
+ kind: 'warn',
31
+ message: `--with-components is ignored for "${descriptor.id}". Supported: ${COMPONENT_HOSTS}.`,
32
+ };
33
+ }
34
+ if (library === 'fluent-v9') {
35
+ return {
36
+ kind: 'error',
37
+ message: `--with-components ships Fluent v8 components and can't combine with Fluent v9 on "${descriptor.id}". ` +
38
+ 'Re-run with --v8 (or drop --with-components).',
39
+ };
40
+ }
41
+ return { kind: 'apply' };
42
+ }
24
43
  /** Map known CLI flags to prompt answers so we don't re-ask for them. */
25
44
  export function prefilledAnswers(options) {
26
45
  const pre = {};
@@ -32,6 +51,8 @@ export function prefilledAnswers(options) {
32
51
  pre.propertyType = options.propertyType;
33
52
  if (options.graph)
34
53
  pre.graph = 'true';
54
+ if (options.surface)
55
+ pre.surface = options.surface;
35
56
  return pre;
36
57
  }
37
58
  /** Run an artifact's prompts, skipping any already provided via flags. */
@@ -86,7 +107,7 @@ export async function runArtifactPrompts(descriptor, projectName, options) {
86
107
  return answers;
87
108
  }
88
109
  /** Build the `{{var}}` substitution bag from answers + the artifact's derive() hook. */
89
- export function buildTemplateVariables(descriptor, projectName, answers, library) {
110
+ export function buildTemplateVariables(descriptor, projectName, answers, library, withComponents) {
90
111
  let vars = { projectName, ...answers };
91
112
  // Derived vars are data-driven per artifact (replaces hard-coded per-id logic).
92
113
  if (descriptor.derive) {
@@ -95,6 +116,11 @@ export function buildTemplateVariables(descriptor, projectName, answers, library
95
116
  if (descriptor.supportsLibraryChoice && library) {
96
117
  vars.componentLibrary = library;
97
118
  }
119
+ // Opt-in @khester/reusable-components (Fluent v8). Only honored on a host that
120
+ // can consume them; the driver guards `--with-components --v9` before this point.
121
+ if (descriptor.supportsComponents && withComponents) {
122
+ vars.withComponents = 'true';
123
+ }
98
124
  // Always return the bag. Substitution is keyed on `{{name}}`, so it is a safe
99
125
  // no-op on templates without placeholders — and this guarantees `{{projectName}}`
100
126
  // is honored if a template adds it later (the old length-based guard silently skipped it).
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khester/create-dynamics-app",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Unified CLI to scaffold Dynamics 365 / Power Platform artifacts",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -19,12 +19,13 @@
19
19
  "prepublishOnly": "npm run build",
20
20
  "test": "vitest run",
21
21
  "test:watch": "vitest",
22
+ "test:e2e": "tsc --noEmit -p tsconfig.e2e.json && vitest run --config vitest.e2e.config.ts",
22
23
  "typecheck": "tsc --noEmit",
23
24
  "lint": "eslint src --ext .ts,.tsx",
24
25
  "clean": "rimraf dist tsconfig.tsbuildinfo"
25
26
  },
26
27
  "dependencies": {
27
- "@dataverse-kit/export-engine": "^1.0.0",
28
+ "@dataverse-kit/export-engine": "^1.19.0",
28
29
  "@khester/dynamics-ui-components": "^1.0.0",
29
30
  "chalk": "^5.3.0",
30
31
  "commander": "^11.1.0",
@@ -0,0 +1,66 @@
1
+ # Architecture
2
+
3
+ A small, layered custom page centered on a reusable **grid**. The grid is
4
+ declarative (columns are data), and data access is isolated behind one seam.
5
+
6
+ ## Layers
7
+
8
+ ```
9
+ @dataverse-kit/grid-kit <DataGrid> + cell-renderer registry (npm dependency)
10
+ @khester/dynamics-cell-renderers the cell components grid-kit wraps
11
+ @khester/reusable-components logging + dev-panel UI primitives
12
+ │ (imported)
13
+
14
+ src/grid/GridPage.tsx view — builds ColumnDef[] + createCellRegistry,
15
+ maps API rows, renders <DataGrid>
16
+
17
+
18
+ src/core/services/ IApiService the data-access contract
19
+ ServiceFactory picks one impl per environment (the seam)
20
+ Mock / Fetch / Xrm three implementations
21
+ ```
22
+
23
+ **Dependency rule (one direction):** view → `IApiService`. `core/` imports nothing
24
+ app-specific. UI/grid/logging are consumed from packages, never reached into.
25
+
26
+ ## The grid pattern
27
+
28
+ | Concern | Lives in | Why |
29
+ |---------|----------|-----|
30
+ | Columns | the `columns: ColumnDef[]` array in `GridPage.tsx` | declarative; each column is a `rendererType` + `rendererConfig` (+ `editorType`) |
31
+ | Cell rendering | `@dataverse-kit/grid-kit` registry | shared renderers; override or add types via `registry.register(...)` |
32
+ | Row mapping | `mapAccount` in `GridPage.tsx` | Dataverse attribute → display row, one place |
33
+ | Filtering | local `search` state | client-side; the toolbar `onSearch` updates it |
34
+ | Load | `api.retrieveMultipleRecords('accounts', …)` | data fetch isolated behind `IApiService` |
35
+
36
+ The same registry that drives this `<DataGrid>` also drives a Grid Customizer via
37
+ `toGridCustomizerOverrides` — so a cell type you add here works in both hosts.
38
+
39
+ ## The ServiceFactory seam
40
+
41
+ `ServiceFactory.createApiService(Xrm)` is the single decision point:
42
+
43
+ - **Mock** (`localhost`, no `DYNAMICS_URL`) → `MockApiService`, in-memory (seeds a
44
+ few accounts).
45
+ - **Token** (`localhost` + `DYNAMICS_URL`) → `FetchApiService`. `vite.config.ts`
46
+ proxies `/api/data/*` to the org and injects the bearer **server-side**, so the
47
+ token never reaches the client bundle.
48
+ - **Production** (deployed) → `XrmApiService`, hitting the Web API on the same
49
+ origin (session auth). `Xrm` is resolved from `window.parent` in `App.tsx`.
50
+
51
+ All three implement the same `IApiService` and address entities by their Web API
52
+ **set name** (e.g. `accounts`).
53
+
54
+ ## Logging
55
+
56
+ `logger` / `withCrudLog` come from `@khester/reusable-components`.
57
+ `core/services/crudLogging.ts` binds `withCrudLog` to the Dataverse error shapes so
58
+ every service method emits one structured `[CRUD] …` line. Console output is
59
+ **localhost-gated** (errors always surface); on localhost the buffer is exposed as
60
+ `window.__APP_LOGS__` + `window.dumpAppLogs(filter?)`.
61
+
62
+ ## Testing
63
+
64
+ The view and `IApiService` implementations are best covered by running the app
65
+ against the mock seam (`npm run dev`). Add unit tests for any pure mappers you
66
+ introduce (Vitest is configured via `npm run test`).
@@ -0,0 +1,122 @@
1
+ # Grid Starter (Dataverse custom page)
2
+
3
+ A production-shaped starter for a React app mounted as a **Dynamics 365 custom
4
+ page**, centered on a reusable **grid**. `src/grid/GridPage.tsx` renders an
5
+ [`@dataverse-kit/grid-kit`](../../../grid-kit) `<DataGrid>` over the
6
+ `ServiceFactory` data seam: mock data on localhost, live Dataverse in production.
7
+
8
+ The grid is **registry-driven** — every column is a `ColumnDef` with a
9
+ `rendererType` (text, currency, `coloredCell` status badge, rating, …). The same
10
+ registry works in a PCF grid and a Grid Customizer. To add or change a cell:
11
+ edit the `columns` array in `GridPage.tsx`; to add a bespoke cell type, call
12
+ `registry.register('myType', { read: MyCell })`.
13
+
14
+ The data-access seam (`src/core/services`) is owned by this app.
15
+
16
+ ## Commands
17
+
18
+ | Command | What it does |
19
+ |---------|--------------|
20
+ | `npm install` | Install dependencies |
21
+ | `npm run dev` | Run in **mock** mode — seeded data, no org needed |
22
+ | `npm run dev:token` | Run against a **live org** — refreshes the token + starts Vite |
23
+ | `npm run auth:token -- --url https://<org>.crm.dynamics.com` | Acquire/refresh a Dataverse token into `.env` (needs `az login`) |
24
+ | `npm run build` | Production build → `dist/` (multi-file) |
25
+ | `npm run build:d365` | Single self-contained `dist/index.html` (the web resource) |
26
+ | `npm run deploy` | Build + upload + **publish** the web resource to your org |
27
+ | `npm test` | Run Vitest (no tests ship by default — add your own; passes with none) |
28
+ | `npm run typecheck` | Type-check (`tsc --noEmit`) |
29
+ | `npm install @khester/reusable-components@latest` | Update the shared component library |
30
+
31
+ > Requires **Node 18+**. For `dev:token` / `deploy` you also need `az login` and
32
+ > `DYNAMICS_URL` in `.env` (set once via `npm run auth:token -- --url …`).
33
+
34
+ ## Quick start
35
+
36
+ ```bash
37
+ npm install
38
+ npm run dev # mock mode — a seeded account, no org needed
39
+ ```
40
+
41
+ Open http://localhost:3000. The grid lists the seeded accounts; type in the
42
+ toolbar search to filter, click **Refresh** to reload. Each load logs a
43
+ `[CRUD] READ accounts ok` line; on localhost `window.dumpAppLogs()` in the
44
+ browser console prints the structured log buffer.
45
+
46
+ A floating **🔧 Dev Tools** panel (bottom-right, localhost only) shows the active
47
+ mode (Mock / Token-proxy / Production) and the `[CRUD]` log buffer. It renders
48
+ nothing once deployed.
49
+
50
+ ## Three runtime modes
51
+
52
+ `src/core/services/ServiceFactory.ts` picks the data service per environment:
53
+
54
+ | Mode | When | Service |
55
+ |------|------|---------|
56
+ | **Mock** | `localhost`, no `DYNAMICS_URL` | `MockApiService` (in-memory, seeds a few accounts) |
57
+ | **Token** | `localhost` + `DYNAMICS_URL` set | `FetchApiService` → Vite proxy → real Dataverse |
58
+ | **Production** | deployed in a model-driven app | `XrmApiService` (Web API, session auth) |
59
+
60
+ ### Token mode — test against a real org locally
61
+
62
+ ```bash
63
+ npm run auth:token -- --url https://<org>.crm.dynamics.com # writes .env via Azure CLI
64
+ npm run dev
65
+ ```
66
+
67
+ The Vite dev server proxies `/api/data/*` to the org and injects the bearer token
68
+ **server-side** — the token never enters the browser bundle. Tokens last ~60 min;
69
+ re-run `npm run auth:token` to refresh (no dev-server restart needed). Requires
70
+ `az login` first. See `env.example` for the variables.
71
+
72
+ Once `DYNAMICS_URL` is in `.env`, **`npm run dev:token`** refreshes the token and
73
+ starts the dev server in one step:
74
+
75
+ ```bash
76
+ npm run dev:token
77
+ ```
78
+
79
+ ## Build & deploy
80
+
81
+ ```bash
82
+ npm run build # standard multi-file build → dist/
83
+ npm run build:d365 # single self-contained dist/index.html (vite-plugin-singlefile)
84
+ npm run deploy # build:d365 + upload as an HTML web resource + publish
85
+ ```
86
+
87
+ **`npm run deploy`** refreshes the token, builds the single-file bundle, and
88
+ upserts + publishes it as an HTML web resource via the Dataverse Web API (no PAC
89
+ CLI needed). Set the web-resource name in `.env` first — the prefix must match a
90
+ publisher that exists in your org:
91
+
92
+ ```bash
93
+ # .env
94
+ WEBRESOURCE_NAME=cr1a2_/myapp/index.html # use YOUR publisher prefix
95
+ # WEBRESOURCE_SOLUTION=MySolution # optional: add it to an unmanaged solution
96
+ ```
97
+
98
+ Then host it on a model-driven **custom page**, or open it via
99
+ `Xrm.Navigation.navigateTo({ pageType: "webresource", webresourceName: "<name>" })`.
100
+ The page reads an optional record id from the URL (`?id=<guid>`); in production it
101
+ resolves `Xrm` from `window.parent`.
102
+
103
+ > Some orgs enforce a Content-Security-Policy that blocks inline `<script>`. The
104
+ > single-file bundle is one inline script — if your environment blocks it, use the
105
+ > multi-file `npm run build` output instead.
106
+
107
+ ## Make it your own
108
+
109
+ The grid lives in `src/grid/GridPage.tsx`:
110
+
111
+ 1. **Change columns** — edit the `columns: ColumnDef[]` array. Each column has a
112
+ `rendererType` (text, currency, coloredCell, rating, optionset, progress, date,
113
+ link, toggle, …) plus optional `rendererConfig` and `editorType`.
114
+ 2. **Custom cell** — `registry.register('myType', { read: MyCell })`, then set a
115
+ column's `rendererType` to `'myType'`.
116
+ 3. **Map your entity** — adjust `mapAccount` and the column `fieldName`s to your
117
+ Dataverse attributes, and seed rows in `src/core/services/MockApiService.ts`
118
+ so `npm run dev` works offline.
119
+
120
+ ```bash
121
+ npm run typecheck # tsc --noEmit
122
+ ```
@@ -0,0 +1,16 @@
1
+ # Token-proxy dev mode — run `npm run dev` against a real Dataverse org.
2
+ #
3
+ # Preferred: run `npm run auth:token -- --url https://<org>.crm.dynamics.com`,
4
+ # which acquires a token via the Azure CLI and writes both values to .env.
5
+ # Or copy this file to .env and fill them in manually.
6
+ #
7
+ # When DYNAMICS_URL is set, the Vite dev server proxies /api/data/* to the org
8
+ # and injects DYNAMICS_TOKEN as the bearer (server-side — never in the bundle).
9
+ DYNAMICS_URL=https://your-org.crm.dynamics.com
10
+ DYNAMICS_TOKEN=
11
+
12
+ # `npm run deploy` — the HTML web-resource unique name. The prefix (before `_`)
13
+ # MUST be a publisher prefix that exists in your org (e.g. cr1a2_ / new_).
14
+ WEBRESOURCE_NAME=new_/your-app/index.html
15
+ # Optional: add the web resource to this unmanaged solution (unique name).
16
+ WEBRESOURCE_SOLUTION=
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .env
5
+ .env.local
6
+ .env.*.local
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Dynamics Custom Page</title>
7
+ <style>
8
+ /* App canvas behind the form — matches the Dynamics model-driven background. */
9
+ body { margin: 0; min-height: 100vh; background: #fafafa; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/index.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "grid-starter-app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Dataverse React custom page with a grid-kit <DataGrid> (Vite + @dataverse-kit/grid-kit)",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "scripts": {
11
+ "dev": "vite",
12
+ "dev:token": "npm run auth:token && npm run dev",
13
+ "build": "tsc && vite build",
14
+ "build:d365": "tsc && vite build --mode d365",
15
+ "deploy": "npm run auth:token && npm run build:d365 && node tools/deploy/deploy-webresource.cjs",
16
+ "preview": "vite preview",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "vitest run --passWithNoTests",
19
+ "clean": "rimraf dist"
20
+ },
21
+ "dependencies": {
22
+ "@dataverse-kit/grid-kit": "^0.1.0",
23
+ "@khester/dynamics-cell-renderers": "^1.1.0",
24
+ "@khester/reusable-components": "^0.1.4",
25
+ "@fluentui/react": "^8.110.10",
26
+ "react": "^18.2.0",
27
+ "react-dom": "^18.2.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^18.2.0",
31
+ "@types/react-dom": "^18.2.0",
32
+ "@vitejs/plugin-react": "^4.2.1",
33
+ "rimraf": "^5.0.5",
34
+ "typescript": "^5.3.3",
35
+ "vite": "^5.4.21",
36
+ "vite-plugin-singlefile": "^2.0.3",
37
+ "vitest": "^1.6.0"
38
+ }
39
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { ServiceFactory } from "./core/services/ServiceFactory";
3
+ import { GridPage } from "./grid/GridPage";
4
+ import { DevPanel } from "./dev-tools/DevPanel";
5
+
6
+ // Domain-free shell. The Xrm context exists only when this bundle is hosted
7
+ // inside a model-driven app (the custom page runs in an iframe, so Xrm lives on
8
+ // window.parent). ServiceFactory uses it for production mode; on localhost it is
9
+ // undefined and the factory returns the mock or token-proxy service instead.
10
+ const Xrm: any = (window as any).parent?.Xrm ?? (window as any).Xrm;
11
+
12
+ export const App: React.FC = () => {
13
+ const api = React.useMemo(() => ServiceFactory.createApiService(Xrm), []);
14
+ return (
15
+ <>
16
+ <GridPage api={api} />
17
+ {/* Localhost-only floating dev tools (renders nothing once deployed). */}
18
+ <DevPanel />
19
+ </>
20
+ );
21
+ };
22
+
23
+ export default App;
@@ -0,0 +1,117 @@
1
+ import { IApiService } from "./IApiService";
2
+ import { logCrud } from "./crudLogging";
3
+ import { logger } from "@khester/reusable-components";
4
+
5
+ /**
6
+ * Dev API service for token-proxy mode (`npm run dev` with DYNAMICS_URL set).
7
+ * Requests go to the SAME-ORIGIN path /api/data/v9.2/... and the Vite dev proxy
8
+ * (vite.config.ts) injects the `Authorization: Bearer` header server-side — the
9
+ * token is never read by, or bundled into, the client.
10
+ */
11
+ export class FetchApiService implements IApiService {
12
+ constructor(private baseUrl = "") {}
13
+
14
+ private headers(): HeadersInit {
15
+ return {
16
+ "Content-Type": "application/json; charset=utf-8",
17
+ Accept: "application/json",
18
+ "OData-Version": "4.0",
19
+ "OData-MaxVersion": "4.0",
20
+ Prefer: 'odata.include-annotations="*"',
21
+ };
22
+ }
23
+
24
+ retrieveMultipleRecords(
25
+ entity: string,
26
+ fetchXml: string,
27
+ ): Promise<{ entities: any[] }> {
28
+ return logCrud(
29
+ { op: "READ", entity, resultCount: (r) => r?.entities?.length },
30
+ async () => {
31
+ const url = `${this.baseUrl}/api/data/v9.2/${entity}?fetchXml=${encodeURIComponent(fetchXml)}`;
32
+ const response = await fetch(url, { method: "GET", headers: this.headers() });
33
+ if (!response.ok) {
34
+ const body = await response.text().catch(() => "");
35
+ throw new Error(`API error ${response.status}: ${response.statusText}${body ? ` — ${body}` : ""}`);
36
+ }
37
+ const data = await response.json();
38
+ return { entities: data.value ?? [] };
39
+ },
40
+ );
41
+ }
42
+
43
+ createRecord(entity: string, record: any): Promise<any> {
44
+ return logCrud({ op: "CREATE", entity, resultId: (r) => r?.id }, async () => {
45
+ const url = `${this.baseUrl}/api/data/v9.2/${entity}`;
46
+ const response = await fetch(url, {
47
+ method: "POST",
48
+ headers: this.headers(),
49
+ body: JSON.stringify(record),
50
+ });
51
+ if (!response.ok) {
52
+ const body = await response.text().catch(() => "");
53
+ throw new Error(`Create failed ${response.status}: ${body}`);
54
+ }
55
+ const entityId = response.headers.get("OData-EntityId");
56
+ const match = entityId ? /\(([^)]+)\)/.exec(entityId) : null;
57
+ return match ? { id: match[1] } : response.json().catch(() => ({}));
58
+ });
59
+ }
60
+
61
+ updateRecord(entity: string, id: string, record: any): Promise<any> {
62
+ return logCrud({ op: "UPDATE", entity, id }, async () => {
63
+ const cleanId = id.replace(/[{}]/g, "");
64
+ const url = `${this.baseUrl}/api/data/v9.2/${entity}(${cleanId})`;
65
+ const response = await fetch(url, {
66
+ method: "PATCH",
67
+ headers: this.headers(),
68
+ body: JSON.stringify(record),
69
+ });
70
+ if (!response.ok) {
71
+ const body = await response.text().catch(() => "");
72
+ throw new Error(`Update failed ${response.status}: ${body}`);
73
+ }
74
+ return { success: true };
75
+ });
76
+ }
77
+
78
+ deleteRecord(entity: string, id: string): Promise<void> {
79
+ return logCrud({ op: "DELETE", entity, id }, async () => {
80
+ const cleanId = id.replace(/[{}]/g, "");
81
+ const url = `${this.baseUrl}/api/data/v9.2/${entity}(${cleanId})`;
82
+ const response = await fetch(url, { method: "DELETE", headers: this.headers() });
83
+ if (!response.ok) {
84
+ const body = await response.text().catch(() => "");
85
+ throw new Error(`Delete failed ${response.status}: ${body}`);
86
+ }
87
+ });
88
+ }
89
+
90
+ executeRequest(requestName: string, requestData: any): Promise<any> {
91
+ return logCrud({ op: "EXECUTE", entity: requestName }, async () => {
92
+ const url = `${this.baseUrl}/api/data/v9.2/${requestName}`;
93
+ const response = await fetch(url, {
94
+ method: "POST",
95
+ headers: this.headers(),
96
+ body: JSON.stringify(requestData),
97
+ });
98
+ if (!response.ok) {
99
+ const body = await response.text().catch(() => "");
100
+ throw new Error(`Execute failed ${response.status}: ${body}`);
101
+ }
102
+ return response.json().catch(() => ({}));
103
+ });
104
+ }
105
+
106
+ async uploadFile(file: File): Promise<string> {
107
+ logger.warn("uploadFile not implemented", {
108
+ source: "FetchApiService",
109
+ data: { file: file.name },
110
+ });
111
+ return "";
112
+ }
113
+
114
+ async associateRecord(): Promise<void> {
115
+ logger.warn("associateRecord not implemented", { source: "FetchApiService" });
116
+ }
117
+ }
@@ -0,0 +1,37 @@
1
+ // The single data-access contract for the page. Three implementations satisfy
2
+ // it — MockApiService (offline), FetchApiService (token-proxy dev), and
3
+ // XrmApiService (production) — and ServiceFactory picks one per environment.
4
+ //
5
+ // Entities are addressed by their Web API ENTITY SET name (plural, e.g.
6
+ // "accounts"), used consistently for reads and writes across all three services.
7
+ export interface IApiService {
8
+ /** Update a record. `record` contains only the changed attributes. */
9
+ updateRecord(entity: string, id: string, record: any): Promise<any>;
10
+
11
+ /** Create a record; resolves to `{ id }`. */
12
+ createRecord(entity: string, record: any): Promise<any>;
13
+
14
+ /** Delete a record. */
15
+ deleteRecord(entity: string, id: string): Promise<void>;
16
+
17
+ /** Retrieve records via a FetchXML query. */
18
+ retrieveMultipleRecords(
19
+ entity: string,
20
+ fetchXml: string,
21
+ ): Promise<{ entities: any[] }>;
22
+
23
+ /** Execute a custom API / unbound function or action. */
24
+ executeRequest(requestName: string, requestData: any): Promise<any>;
25
+
26
+ /** Upload a file and return its URL. */
27
+ uploadFile(file: File): Promise<string>;
28
+
29
+ /** Associate two records via a relationship. */
30
+ associateRecord(
31
+ entityName: string,
32
+ entityId: string,
33
+ relationshipName: string,
34
+ relatedEntityName: string,
35
+ relatedEntityId: string,
36
+ ): Promise<void>;
37
+ }
@@ -0,0 +1,72 @@
1
+ import { IApiService } from "./IApiService";
2
+ import { logCrud } from "./crudLogging";
3
+
4
+ // A few seeded in-memory accounts so `npm run dev` renders the grid with no org
5
+ // or token. Each carries the extra demo fields the grid columns show (revenue,
6
+ // statuscode, rating). Replace this seed as your page grows.
7
+ const SEED_ACCOUNTS = [
8
+ { accountid: "00000000-0000-0000-0000-000000000001", name: "Contoso Ltd", accountnumber: "ACC-1001", telephone1: "+1 (425) 555-0100", emailaddress1: "info@contoso.example", websiteurl: "https://contoso.example", revenue: 1500000, statuscode: "Active", rating: 5 },
9
+ { accountid: "00000000-0000-0000-0000-000000000002", name: "Fabrikam Inc", accountnumber: "ACC-1002", telephone1: "+1 (206) 555-0140", emailaddress1: "hello@fabrikam.example", websiteurl: "https://fabrikam.example", revenue: 220000, statuscode: "On hold", rating: 3 },
10
+ { accountid: "00000000-0000-0000-0000-000000000003", name: "Adventure Works", accountnumber: "ACC-1003", telephone1: "+1 (312) 555-0190", emailaddress1: "sales@adventure.example", websiteurl: "https://adventure.example", revenue: 85000, statuscode: "Inactive", rating: 2 },
11
+ { accountid: "00000000-0000-0000-0000-000000000004", name: "Wingtip Toys", accountnumber: "ACC-1004", telephone1: "+1 (646) 555-0177", emailaddress1: "contact@wingtip.example", websiteurl: "https://wingtip.example", revenue: 640000, statuscode: "Active", rating: 4 },
12
+ ];
13
+
14
+ /**
15
+ * Default mock used on localhost when no token URL is configured. Operations are
16
+ * routed through `logCrud`, so a starter page shows the same `[CRUD] …` console
17
+ * lines (and populates `window.dumpAppLogs()`) as a real service.
18
+ */
19
+ export class MockApiService implements IApiService {
20
+ private accounts: Record<string, any> = Object.fromEntries(
21
+ SEED_ACCOUNTS.map((a) => [a.accountid, { ...a }]),
22
+ );
23
+ private nextId = SEED_ACCOUNTS.length + 1;
24
+
25
+ retrieveMultipleRecords(entity: string): Promise<{ entities: any[] }> {
26
+ return logCrud(
27
+ { op: "READ", entity, resultCount: (r) => r?.entities?.length },
28
+ async () => ({
29
+ entities: entity === "accounts" ? Object.values(this.accounts) : [],
30
+ }),
31
+ );
32
+ }
33
+
34
+ updateRecord(entity: string, id: string, record: any): Promise<any> {
35
+ return logCrud({ op: "UPDATE", entity, id }, async () => {
36
+ const key = id.replace(/[{}]/g, "");
37
+ if (this.accounts[key]) {
38
+ this.accounts[key] = { ...this.accounts[key], ...record };
39
+ }
40
+ return { success: true };
41
+ });
42
+ }
43
+
44
+ createRecord(entity: string, record: any): Promise<any> {
45
+ return logCrud({ op: "CREATE", entity, resultId: (r) => r?.id }, async () => {
46
+ const id = `00000000-0000-0000-0000-${String(this.nextId++).padStart(12, "0")}`;
47
+ if (entity === "accounts") this.accounts[id] = { accountid: id, ...record };
48
+ return { id };
49
+ });
50
+ }
51
+
52
+ deleteRecord(entity: string, id: string): Promise<void> {
53
+ return logCrud({ op: "DELETE", entity, id }, async () => {
54
+ delete this.accounts[id.replace(/[{}]/g, "")];
55
+ });
56
+ }
57
+
58
+ executeRequest(requestName: string): Promise<any> {
59
+ return logCrud({ op: "EXECUTE", entity: requestName }, async () => ({}));
60
+ }
61
+
62
+ async uploadFile(): Promise<string> {
63
+ return "";
64
+ }
65
+
66
+ associateRecord(entityName: string, entityId: string): Promise<void> {
67
+ return logCrud(
68
+ { op: "ASSOCIATE", entity: entityName, id: entityId },
69
+ async () => undefined,
70
+ );
71
+ }
72
+ }