@khester/create-dynamics-app 3.1.0 → 3.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.
Files changed (64) hide show
  1. package/README.md +2 -2
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +23 -5
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/index.js +31 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/injectDevTools.d.ts +2 -0
  8. package/dist/injectDevTools.d.ts.map +1 -1
  9. package/dist/injectDevTools.js +27 -1
  10. package/dist/injectDevTools.js.map +1 -1
  11. package/dist/utils/installDependencies.d.ts +15 -0
  12. package/dist/utils/installDependencies.d.ts.map +1 -1
  13. package/dist/utils/installDependencies.js +32 -2
  14. package/dist/utils/installDependencies.js.map +1 -1
  15. package/package.json +1 -1
  16. package/templates/_shared/dev-tools/auth/get-token.cjs +1 -1
  17. package/templates/_shared/dev-tools/metadata-sync/index.js +23 -11
  18. package/templates/_shared/dev-tools/smoke/test-retrieve.js +45 -16
  19. package/templates/grid-customizer/README.md +105 -15
  20. package/templates/grid-customizer/dev/harness/MockGrid.tsx +165 -0
  21. package/templates/grid-customizer/dev/harness/dataverse.ts +168 -0
  22. package/templates/grid-customizer/dev/index.html +20 -0
  23. package/templates/grid-customizer/dev/main.tsx +260 -0
  24. package/templates/grid-customizer/dev/vite-env.d.ts +1 -0
  25. package/templates/grid-customizer/env.local.example +24 -0
  26. package/templates/grid-customizer/eslintrc.json +11 -0
  27. package/templates/grid-customizer/featureconfig.json +3 -0
  28. package/templates/grid-customizer/gitignore +3 -0
  29. package/templates/grid-customizer/package.json +30 -16
  30. package/templates/grid-customizer/pcfconfig.json +3 -0
  31. package/templates/grid-customizer/tsconfig.dev.json +16 -0
  32. package/templates/grid-customizer/tsconfig.json +17 -10
  33. package/templates/grid-customizer/vite.config.ts +64 -0
  34. package/templates/grid-customizer/{{componentName}}/ControlManifest.Input.xml +63 -0
  35. package/templates/grid-customizer/{{componentName}}/customizers/CellEditorOverrides.tsx +38 -0
  36. package/templates/grid-customizer/{{componentName}}/customizers/CellRendererOverrides.tsx +109 -0
  37. package/templates/grid-customizer/{{componentName}}/customizers/cells.tsx +137 -0
  38. package/templates/grid-customizer/{{componentName}}/customizers/config.ts +84 -0
  39. package/templates/grid-customizer/{{componentName}}/index.ts +81 -0
  40. package/templates/grid-customizer/{{componentName}}/strings/{{componentName}}.1033.resx +56 -0
  41. package/templates/grid-customizer/{{componentName}}/types.ts +168 -0
  42. package/templates/pcf-dataset/dev/main.tsx +5 -1
  43. package/templates/pcf-dataset/{{componentName}}/ControlManifest.Input.xml +8 -1
  44. package/templates/pcf-field/dev/harness/dataverse.ts +71 -0
  45. package/templates/pcf-field/dev/index.html +20 -0
  46. package/templates/pcf-field/dev/main.tsx +212 -0
  47. package/templates/pcf-field/dev/vite-env.d.ts +1 -0
  48. package/templates/pcf-field/env.local.example +24 -0
  49. package/templates/pcf-field/gitignore +4 -0
  50. package/templates/pcf-field/package.json +10 -2
  51. package/templates/pcf-field/tsconfig.dev.json +15 -0
  52. package/templates/pcf-field/tsconfig.json +20 -4
  53. package/templates/pcf-field/vite.config.ts +60 -0
  54. package/templates/pcf-field/{{componentName}}/{{componentName}}Component.tsx +6 -1
  55. package/templates/static-web-app/api/local.settings.json +7 -0
  56. package/templates/static-web-app/frontend/vite.config.ts +9 -1
  57. package/templates/static-web-app/package.json +3 -1
  58. package/templates/static-web-app/staticwebapp.config.json +4 -1
  59. package/templates/grid-customizer/src/GridCustomizer.ts +0 -28
  60. package/templates/grid-customizer/src/cell-renderers.tsx +0 -35
  61. package/templates/grid-customizer/src/index.ts +0 -4
  62. package/templates/grid-customizer/src/types/grid-types.ts +0 -30
  63. package/templates/grid-customizer/src/utils/color-utils.ts +0 -24
  64. package/templates/grid-customizer/webpack.config.js +0 -17
@@ -0,0 +1,260 @@
1
+ import * as React from 'react';
2
+ // ReactDOM.render, NOT createRoot — this control is pinned to React 16.14.0 to match the
3
+ // `Reactv16` platform global it externalises, and `react-dom/client` does not exist there.
4
+ import * as ReactDOM from 'react-dom';
5
+ import {
6
+ HarnessShell,
7
+ usePersistedState,
8
+ type ThemeChoice,
9
+ type DataSource,
10
+ type HarnessField,
11
+ } from '@dataverse-kit/pcf-dev-harness/shell';
12
+ import { createCellRendererOverrides } from '../{{componentName}}/customizers/CellRendererOverrides';
13
+ import { MockGrid, type HarnessColumn, type HarnessRow } from './harness/MockGrid';
14
+ import { fetchGridData, type HttpError } from './harness/dataverse';
15
+
16
+ /**
17
+ * Dev harness for {{componentName}}.
18
+ *
19
+ * ★ It renders the customizer's REAL override map (`createCellRendererOverrides`) through a
20
+ * stand-in grid that follows the same call contract as PowerAppsOneGrid — see
21
+ * `harness/MockGrid.tsx`. `npm start` cannot do this: the stock PCF harness has no grid, so
22
+ * the overrides are never invoked and the canvas is simply blank.
23
+ *
24
+ * Two data sources:
25
+ * - Mock — the recordset below, one column per overridden data type. Needs no org.
26
+ * - Live — real rows and, critically, real column DATA TYPES read from entity metadata.
27
+ * Enabled only when VITE_DYNAMICS_URL is set in `.env.local`.
28
+ */
29
+
30
+ /** Live-org binding: entity set + columns + page size. */
31
+ interface GridBinding {
32
+ entitySetName: string;
33
+ select: string;
34
+ top: string;
35
+ }
36
+
37
+ /** One column per data type this template overrides, plus a Text column to show fall-through. */
38
+ const MOCK_COLUMNS: HarnessColumn[] = [
39
+ { name: 'name', displayName: 'Name', dataType: 'Text', width: 200 },
40
+ { name: 'statuscode', displayName: 'Status', dataType: 'OptionSet', width: 150 },
41
+ { name: 'revenue', displayName: 'Revenue', dataType: 'Currency', width: 150 },
42
+ { name: 'percentcomplete', displayName: 'Complete', dataType: 'Decimal', width: 200 },
43
+ { name: 'donotemail', displayName: 'Email OK', dataType: 'TwoOptions', width: 130 },
44
+ ];
45
+
46
+ function cell(raw: unknown, formatted: string): { raw: unknown; formatted: string } {
47
+ return { raw, formatted };
48
+ }
49
+
50
+ const MOCK_ROWS: HarnessRow[] = [
51
+ {
52
+ name: cell('Contoso Ltd', 'Contoso Ltd'),
53
+ statuscode: cell(1, 'Active'),
54
+ revenue: cell(5200000, '$5,200,000.00'),
55
+ percentcomplete: cell(82, '82'),
56
+ donotemail: cell(true, 'Yes'),
57
+ },
58
+ {
59
+ name: cell('Fabrikam Inc', 'Fabrikam Inc'),
60
+ statuscode: cell(2, 'Pending'),
61
+ revenue: cell(-18500, '-$18,500.00'),
62
+ percentcomplete: cell(35, '35'),
63
+ donotemail: cell(false, 'No'),
64
+ },
65
+ {
66
+ name: cell('Adventure Works', 'Adventure Works'),
67
+ statuscode: cell(3, 'Blocked'),
68
+ revenue: cell(920000, '$920,000.00'),
69
+ percentcomplete: cell(0, '0'),
70
+ donotemail: cell(false, 'No'),
71
+ },
72
+ {
73
+ name: cell('Northwind Traders', 'Northwind Traders'),
74
+ statuscode: cell(4, 'Completed'),
75
+ revenue: cell(3400000, '$3,400,000.00'),
76
+ percentcomplete: cell(100, '100'),
77
+ donotemail: cell(true, 'Yes'),
78
+ },
79
+ {
80
+ name: cell('Tailspin Toys', 'Tailspin Toys'),
81
+ // An option label absent from config.STATUS_COLORS — proves the derived-colour fallback.
82
+ statuscode: cell(5, 'Awaiting Review'),
83
+ revenue: cell(undefined, ''),
84
+ percentcomplete: cell(58, '58'),
85
+ donotemail: cell(false, 'No'),
86
+ },
87
+ {
88
+ // ★ The null row. Dataverse columns are nullable — a Boolean added to a table with
89
+ // existing rows is null on all of them — and a renderer that coerces null would paint
90
+ // this as a definite "No". Every cell here must fall through to the DEFAULT renderer
91
+ // (blank), not to a custom one. If a badge, tick or bar appears on this row, a handler
92
+ // is missing its empty check.
93
+ name: cell('Wingtip Toys (empty row)', 'Wingtip Toys (empty row)'),
94
+ statuscode: cell(null, ''),
95
+ revenue: cell(null, ''),
96
+ percentcomplete: cell(null, ''),
97
+ donotemail: cell(null, ''),
98
+ },
99
+ ];
100
+
101
+ // VITE_-prefixed vars reach client code; this one only GATES Live mode. The token is
102
+ // deliberately not read here — it would be inlined into the bundle. The proxy injects it.
103
+ const LIVE_ENABLED = !!import.meta.env.VITE_DYNAMICS_URL;
104
+
105
+ /** Turn a failed live fetch into a red status line, calling out auth failures specifically. */
106
+ function describeLoadError(e: unknown): React.ReactNode {
107
+ const err = e as HttpError;
108
+ // The dev server refreshes the token in the background, so a 401/403 that survives the
109
+ // client retry means the `az login` session itself lapsed — not just the short-lived token.
110
+ const msg =
111
+ err.status === 401 || err.status === 403
112
+ ? 'Not authorized — your `az login` session has expired. Run `az login`, then Load again.'
113
+ : `Failed to load: ${err.message}`;
114
+ return <span style={{ color: '#a4262c' }}>{msg}</span>;
115
+ }
116
+
117
+ const Harness: React.FC = () => {
118
+ // Panel CONFIG persists across reloads (localStorage); fetched DATA does not.
119
+ const [width, setWidth] = usePersistedState('width', 0); // 0 => fill the canvas
120
+ const [height, setHeight] = usePersistedState('height', 0); // 0 => auto
121
+ const [theme, setTheme] = usePersistedState<ThemeChoice>('theme', 'light');
122
+ const [dataSource, setDataSource] = usePersistedState<DataSource>('dataSource', 'mock');
123
+ const [binding, setBinding] = usePersistedState<GridBinding>('binding', {
124
+ entitySetName: 'accounts',
125
+ select: 'name,statuscode,revenue,donotemail',
126
+ top: '25',
127
+ });
128
+ // Seeded with the mock decimal column so the progress renderer is visible on first run.
129
+ // The SHIPPED default (config.PROGRESS_COLUMNS) is deliberately empty — see that file.
130
+ const [progressColumns, setProgressColumns] = usePersistedState(
131
+ 'progressColumns',
132
+ 'percentcomplete',
133
+ );
134
+
135
+ const [columns, setColumns] = React.useState<HarnessColumn[]>(MOCK_COLUMNS);
136
+ const [rows, setRows] = React.useState<HarnessRow[]>(MOCK_ROWS);
137
+ const [liveStatus, setLiveStatus] = React.useState<React.ReactNode>(null);
138
+
139
+ // The real override map, rebuilt only when the progress-column list changes. This is the
140
+ // same factory index.ts calls — the harness never forks the renderers.
141
+ const overrides = React.useMemo(
142
+ () =>
143
+ createCellRendererOverrides({
144
+ progressColumns: progressColumns
145
+ .split(',')
146
+ .map((s) => s.trim())
147
+ .filter(Boolean),
148
+ }),
149
+ [progressColumns],
150
+ );
151
+
152
+ // On reload with a restored Live selection: clear and prompt for Load — or fall back to
153
+ // Mock when this session has no org configured. Runs once on mount.
154
+ React.useEffect(() => {
155
+ if (dataSource !== 'live') return;
156
+ if (!LIVE_ENABLED) {
157
+ setDataSource('mock');
158
+ return;
159
+ }
160
+ setColumns([]);
161
+ setRows([]);
162
+ setLiveStatus(<span style={{ color: '#605e5c' }}>Binding restored — click Load.</span>);
163
+ // Mount-only by design: `[]` deliberately omits dataSource/LIVE_ENABLED, because this
164
+ // reconciles the RESTORED selection once and must not re-run when it later changes.
165
+ // (No eslint-disable here: this template's .eslintrc loads no react-hooks plugin, and a
166
+ // disable naming an unresolvable rule is an ESLint ERROR the moment anything lints it —
167
+ // the failure mode gated by "PCF templates never disable an unresolvable ESLint rule".)
168
+ }, []);
169
+
170
+ const onDataSourceChange = (next: DataSource): void => {
171
+ setDataSource(next);
172
+ if (next === 'mock') {
173
+ setColumns(MOCK_COLUMNS);
174
+ setRows(MOCK_ROWS);
175
+ setLiveStatus(null);
176
+ } else {
177
+ setColumns([]);
178
+ setRows([]);
179
+ setLiveStatus(<span style={{ color: '#605e5c' }}>Enter a binding and click Load.</span>);
180
+ }
181
+ };
182
+
183
+ const onLoadLive = (): void => {
184
+ void (async () => {
185
+ setLiveStatus(<span style={{ color: '#605e5c' }}>Loading…</span>);
186
+ try {
187
+ const result = await fetchGridData(binding.entitySetName, binding.select, binding.top);
188
+ setColumns(result.columns);
189
+ setRows(result.rows);
190
+ const typed = result.columns.map((c) => `${c.name}:${c.dataType}`).join(', ');
191
+ setLiveStatus(
192
+ <span style={{ color: '#107c10' }}>
193
+ Loaded {result.rows.length} row{result.rows.length === 1 ? '' : 's'} — {typed}
194
+ </span>,
195
+ );
196
+ } catch (e) {
197
+ setLiveStatus(describeLoadError(e));
198
+ }
199
+ })();
200
+ };
201
+
202
+ const bindingFields: HarnessField[] = [
203
+ {
204
+ kind: 'text',
205
+ label: 'Entity set (plural)',
206
+ placeholder: 'accounts',
207
+ value: binding.entitySetName,
208
+ onChange: (v) => setBinding((b) => ({ ...b, entitySetName: v })),
209
+ },
210
+ {
211
+ kind: 'text',
212
+ label: 'Columns ($select, comma-separated)',
213
+ placeholder: 'name,statuscode,revenue',
214
+ value: binding.select,
215
+ onChange: (v) => setBinding((b) => ({ ...b, select: v })),
216
+ },
217
+ {
218
+ kind: 'text',
219
+ label: 'Top (max rows)',
220
+ placeholder: '25',
221
+ value: binding.top,
222
+ onChange: (v) => setBinding((b) => ({ ...b, top: v })),
223
+ },
224
+ ];
225
+
226
+ return (
227
+ <HarnessShell
228
+ title="{{componentName}} — dev harness"
229
+ subtitle="Grid customizer · cell renderer overrides against a stand-in OneGrid"
230
+ width={width}
231
+ onWidthChange={setWidth}
232
+ height={height}
233
+ onHeightChange={setHeight}
234
+ theme={theme}
235
+ onThemeChange={setTheme}
236
+ dataSource={dataSource}
237
+ onDataSourceChange={onDataSourceChange}
238
+ liveEnabled={LIVE_ENABLED}
239
+ bindingFields={bindingFields}
240
+ onLoadLive={onLoadLive}
241
+ liveStatus={liveStatus}
242
+ // ★ NOT manifest input properties — a grid customizer cannot have any (see the
243
+ // manifest). This is the compile-time config from customizers/config.ts, exposed here
244
+ // so you can find the right column list before hard-coding it and redeploying.
245
+ fields={[
246
+ {
247
+ kind: 'text',
248
+ label: 'Progress columns (config.PROGRESS_COLUMNS)',
249
+ placeholder: 'dvk_percentcomplete',
250
+ value: progressColumns,
251
+ onChange: setProgressColumns,
252
+ },
253
+ ]}
254
+ >
255
+ <MockGrid columns={columns} rows={rows} overrides={overrides} />
256
+ </HarnessShell>
257
+ );
258
+ };
259
+
260
+ ReactDOM.render(<Harness />, document.getElementById('root'));
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,24 @@
1
+ # Live-org dev harness config. Copy to `.env.local` (gitignored) and fill in your org.
2
+ #
3
+ # cp env.local.example .env.local
4
+ # az login
5
+ # npm run dev # http://localhost:8182
6
+ #
7
+ # `az login` alone is enough: the Vite dev server acquires the bearer token in-process and
8
+ # refreshes it before expiry. DYNAMICS_TOKEN below is only an optional static fallback.
9
+
10
+ # The ONLY var exposed to the browser bundle. Setting it enables the harness "Live" toggle;
11
+ # leave it blank to run against mock data only.
12
+ VITE_DYNAMICS_URL=
13
+
14
+ # Optional. Set for a CDX / cross-tenant org — passed to `az --tenant`.
15
+ DYNAMICS_TENANT=
16
+
17
+ # Optional static token fallback, used only if the in-process `az` acquisition fails.
18
+ # ★ NO `VITE_` PREFIX, deliberately: a VITE_-prefixed var is inlined into the client bundle,
19
+ # which would ship your bearer token to the browser. Server-side only.
20
+ #
21
+ # ★ Do NOT populate this with the workspace's `msft/04-dynamics/scripts/get-token.sh` — its
22
+ # detect_env_prefix writes REACT_APP_/VITE_-prefixed names, and this harness looks up a bare
23
+ # `DYNAMICS_TOKEN`. Use `npm run auth:token` (the pcf-dev-token bin) instead.
24
+ DYNAMICS_TOKEN=
@@ -0,0 +1,11 @@
1
+ {
2
+ "root": true,
3
+ "parser": "@typescript-eslint/parser",
4
+ "parserOptions": {
5
+ "ecmaVersion": 2021,
6
+ "sourceType": "module",
7
+ "ecmaFeatures": { "jsx": true }
8
+ },
9
+ "env": { "browser": true, "es2021": true },
10
+ "ignorePatterns": ["out/**", "node_modules/**", "**/generated/**"]
11
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "pcfReactPlatformLibraries": "on"
3
+ }
@@ -1,4 +1,7 @@
1
1
  node_modules/
2
+ out/
2
3
  dist/
4
+ generated/
3
5
  *.log
4
6
  .env
7
+ .env.local
@@ -1,25 +1,39 @@
1
1
  {
2
2
  "name": "{{projectName}}",
3
3
  "version": "1.0.0",
4
- "description": "{{componentName}} — Dynamics 365 editable-grid customizer (Fluent v9)",
5
- "private": true,
4
+ "description": "{{componentName}} — Power Apps OneGrid cell-renderer customizer (PCF, Fluent v8)",
5
+ "_comment_versions": "react/react-dom are pinned EXACTLY to 16.14.0 and @fluentui/react to 8.121.1 because this is a control-type=\"virtual\" control: both are externalised to the platform globals Reactv16 / FluentUIReactv81211, so the compiled-against version MUST equal the served version. Measured live 2026-08-02 on orge2e285aa: the model-driven host serves React 17.0.2 under the Reactv16 alias and defines NO Reactv18. Fluent 8.121.1 AND 9.68.0 (FluentUIReactv940) are both served, but a customizer's cells are rendered inside Microsoft's v8 OneGrid tree — see the manifest for why that makes v8 the right contract here.",
6
6
  "scripts": {
7
- "build": "webpack --mode production",
8
- "dev": "webpack --mode development --watch",
9
- "typecheck": "tsc --noEmit",
10
- "lint": "eslint src --ext .ts,.tsx"
7
+ "build": "pcf-scripts build",
8
+ "clean": "pcf-scripts clean",
9
+ "rebuild": "pcf-scripts rebuild",
10
+ "start": "pcf-scripts start",
11
+ "refreshTypes": "pcf-scripts refreshTypes",
12
+ "dev": "vite --config vite.config.ts",
13
+ "dev:token": "npm run auth:token && npm run dev",
14
+ "auth:token": "pcf-dev-token",
15
+ "typecheck:dev": "tsc -p tsconfig.dev.json --noEmit",
16
+ "harness:check": "npm run typecheck:dev && vite build --config vite.config.ts"
11
17
  },
12
18
  "dependencies": {
13
- "react": "^18.2.0",
14
- "react-dom": "^18.2.0",
15
- "@fluentui/react-components": "^9.46.2"
19
+ "@fluentui/react": "8.121.1",
20
+ "@types/powerapps-component-framework": "^1.3.18",
21
+ "react": "16.14.0",
22
+ "react-dom": "16.14.0"
16
23
  },
17
24
  "devDependencies": {
18
- "@types/react": "^18.2.0",
19
- "@types/react-dom": "^18.2.0",
20
- "ts-loader": "^9.5.1",
21
- "typescript": "^5.3.3",
22
- "webpack": "^5.89.0",
23
- "webpack-cli": "^5.1.4"
24
- }
25
+ "@dataverse-kit/pcf-dev-harness": "^0.2.0",
26
+ "@types/node": "^18.16.9",
27
+ "@types/react": "^16.14.34",
28
+ "@types/react-dom": "^16.9.17",
29
+ "@typescript-eslint/parser": "^6.21.0",
30
+ "@vitejs/plugin-react": "^4.3.4",
31
+ "pcf-scripts": "^1",
32
+ "pcf-start": "^1",
33
+ "typescript": "^5.3.0",
34
+ "vite": "^5.4.0"
35
+ },
36
+ "browserslist": [
37
+ "last 2 versions"
38
+ ]
25
39
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "outDir": "./out/controls"
3
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "skipLibCheck": true
6
+ },
7
+ "//": [
8
+ "★ `exclude` is INHERITED through `extends`, so it MUST be overridden here — otherwise",
9
+ "dev/ stays excluded from its own project, `include` resolves to nothing, and",
10
+ "`npm run typecheck:dev` passes while checking zero files: a green gate gating nothing.",
11
+ "dev/main.tsx imports the real cellRendererOverrides, so this also typechecks the",
12
+ "customizer against the grid contract."
13
+ ],
14
+ "exclude": ["node_modules", "out"],
15
+ "include": ["dev", "vite.config.ts"]
16
+ }
@@ -1,15 +1,22 @@
1
1
  {
2
+ "extends": "./node_modules/pcf-scripts/tsconfig_base.json",
2
3
  "compilerOptions": {
3
- "target": "ES2018",
4
- "module": "ESNext",
5
- "moduleResolution": "node",
6
- "jsx": "react-jsx",
7
- "strict": true,
4
+ "typeRoots": ["node_modules/@types"],
5
+ "jsx": "react",
8
6
  "esModuleInterop": true,
9
- "forceConsistentCasingInFileNames": true,
10
- "skipLibCheck": true,
11
- "declaration": false,
12
- "lib": ["DOM", "DOM.Iterable", "ES2018"]
7
+ "experimentalDecorators": true,
8
+ "skipLibCheck": true
13
9
  },
14
- "include": ["src"]
10
+ "//": [
11
+ "★ `exclude` REPLACES the base's [\"./node_modules\"] — node_modules MUST be re-listed here.",
12
+ "pcf-scripts runs ts-loader in full (non-transpileOnly) mode, and ts-loader seeds its file",
13
+ "list from EVERY file the tsconfig resolves — not just what webpack reaches from the entry.",
14
+ "So without this exclude the Vite harness under dev/ would be typechecked as part of the",
15
+ "control build and could fail it. dev/ has its own project: tsconfig.dev.json.",
16
+ "",
17
+ "`jsx: react` (classic) rather than `react-jsx`: the automatic runtime emits imports of",
18
+ "`react/jsx-runtime`, which does not exist in React 16 — the version this control is pinned",
19
+ "to so it matches the platform's Reactv16 global."
20
+ ],
21
+ "exclude": ["node_modules", "out", "dev", "vite.config.ts"]
15
22
  }
@@ -0,0 +1,64 @@
1
+ import { defineConfig, loadEnv } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import * as path from 'path';
4
+ import { dataverseProxy } from '@dataverse-kit/pcf-dev-harness/node';
5
+
6
+ /**
7
+ * Live-org dev harness for {{componentName}} — `npm run dev` → http://localhost:8182
8
+ *
9
+ * WHY THIS EXISTS, given `npm start` already runs the Microsoft PCF harness:
10
+ * a grid customizer renders NOTHING of its own. `pcf-scripts start` will load this control
11
+ * happily and show an empty canvas, because the thing being customized — Microsoft's
12
+ * PowerAppsOneGrid — is not present in that harness and never calls the overrides. So the
13
+ * stock harness can tell you the control instantiates, and nothing else.
14
+ *
15
+ * `dev/harness/MockGrid.tsx` fills that gap: a minimal grid that calls the SAME
16
+ * `cellRendererOverrides` map, with the same `(props, params)` arguments and the same
17
+ * fall-through-on-undefined rule as OneGrid. Two data sources (see `dev/main.tsx`):
18
+ * - Mock — a hard-coded recordset covering each overridden data type. Needs no org.
19
+ * - Live — real rows AND real column data types from Dataverse metadata, through the
20
+ * same-origin `/api/data` proxy. Enabled when VITE_DYNAMICS_URL is set.
21
+ *
22
+ * The proxy and its in-process Azure-CLI token refresh come from
23
+ * `@dataverse-kit/pcf-dev-harness/node`: the bearer token is injected SERVER-SIDE and never
24
+ * reaches the browser. `az login` + `npm run dev` is enough — `npm run auth:token` only
25
+ * writes an optional static fallback.
26
+ */
27
+ export default defineConfig(({ mode }) => {
28
+ // Read .env* from the PROJECT root (__dirname), not `root: 'dev'`. The empty prefix loads
29
+ // every var server-side (URL + tenant + any static token); `envPrefix` below still limits
30
+ // what can reach the client bundle.
31
+ const env = loadEnv(mode, __dirname, '');
32
+ const live = dataverseProxy({
33
+ dynamicsUrl: env.VITE_DYNAMICS_URL || '',
34
+ tenant: env.DYNAMICS_TENANT,
35
+ // Array: `.env.local` is the documented file, `.env` is tolerated as a fallback so a
36
+ // token written by a different tool still works. First file defining DYNAMICS_TOKEN wins.
37
+ envFile: [path.resolve(__dirname, '.env.local'), path.resolve(__dirname, '.env')],
38
+ pluginName: '{{componentName}}-live-token',
39
+ });
40
+
41
+ return {
42
+ root: 'dev',
43
+ // .env* live at the project root, not the `dev` root, so client code still sees
44
+ // import.meta.env.VITE_DYNAMICS_URL (which gates Live mode).
45
+ envDir: __dirname,
46
+ // SECURITY: the bearer token is acquired server-side by dataverseProxy and is never put
47
+ // in a VITE_-prefixed var, so Vite cannot inline it into the client bundle. envPrefix
48
+ // narrows client exposure to VITE_DYNAMICS_URL alone; the static fallback DYNAMICS_TOKEN
49
+ // is deliberately unprefixed for the same reason.
50
+ envPrefix: 'VITE_DYNAMICS_URL',
51
+ // jsxRuntime: 'classic' — the automatic runtime emits `react/jsx-runtime`, which does not
52
+ // exist in React 16. This control is pinned to 16.14.0 to match the platform's Reactv16
53
+ // global, and tsconfig sets `"jsx": "react"` (classic) for the same reason.
54
+ plugins: [react({ jsxRuntime: 'classic' }), live.plugin],
55
+ server: {
56
+ // 8182, because `pcf-scripts start` owns 8181 — the two harnesses can run side by side.
57
+ // strictPort so a clash fails loudly instead of silently moving to another port.
58
+ port: 8182,
59
+ strictPort: true,
60
+ open: true,
61
+ proxy: live.proxy,
62
+ },
63
+ };
64
+ });
@@ -0,0 +1,63 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <manifest>
3
+ <control namespace="{{namespace}}" constructor="{{componentName}}" version="1.0.0" display-name-key="{{namespace}}_{{componentName}}" description-key="{{namespace}}_{{componentName}}_Desc" control-type="virtual">
4
+ <!--
5
+ ★ A GRID CUSTOMIZER IS NOT A GRID. It renders nothing of its own.
6
+
7
+ It is bound to Microsoft's PowerAppsOneGrid via that grid's
8
+ `GridCustomizerControlFullName` parameter. On init the platform hands it an event
9
+ name through the BOUND `EventName` property below; the control answers by calling
10
+ `context.factory.fireEvent(eventName, customizer)` with an object of cell renderer /
11
+ editor overrides. OneGrid then calls those overrides while painting its own cells.
12
+
13
+ Consequences that shape this whole template:
14
+ • `EventName` is the ONLY property. It must be `usage="bound"` and `required="true"`.
15
+ • `updateView` returns an empty Fragment — see index.ts.
16
+ • ★ A customizer CANNOT receive maker-set `usage="input"` properties. The grid's
17
+ "Customizer control" field references this control by NAME only, with nowhere to
18
+ pass a value, so any input property is `undefined` at runtime — and reading `.raw`
19
+ off it throws inside init, which the host SWALLOWS (you get a silently missing
20
+ customizer, not an error). Configure via customizers/config.ts, or read a Dataverse
21
+ environment variable at runtime (see README).
22
+ -->
23
+ <property name="EventName" display-name-key="EventName" description-key="EventName_Desc" of-type="SingleLine.Text" usage="bound" required="true" />
24
+ <resources>
25
+ <code path="index.ts" order="1"/>
26
+ <!--
27
+ ★ These versions must EQUAL a version the PLATFORM ACTUALLY SERVES — not merely one
28
+ pcf-scripts accepts. pcf-scripts matches a {minVersion, maxVersion} row in its
29
+ PlatformLibraryVersions.json and remaps the bundle to that row's global alias, but it
30
+ never checks the runtime. Trusting the tool produces a green build and a dead control.
31
+
32
+ Measured on live model-driven pages (orge2e285aa, 2026-08-02):
33
+
34
+ Reactv16 = React 17.0.2 ← the alias says 16; the served React is 17.0.2
35
+ Reactv17 = undefined
36
+ Reactv18 = undefined ← pcf-scripts HAS an 18.3.1 row; no org serves it
37
+ ReactDOMv16 = present
38
+ ReactDOMv18 = undefined
39
+ FluentUIReactv81211 = present ← Fluent 8.121.1, loaded on every page
40
+ FluentUIReactv940 = present ← Fluent 9.68.0 IS served (see below)
41
+ FluentUIReactv8290 = loaded ON DEMAND — appears only on a page hosting a control
42
+ that declares Fluent 8.29.0, which is how the injection works
43
+
44
+ 16.14.0, not 17.0.2, is deliberate: the model-driven host serves 17.0.2 but the canvas
45
+ host serves 16.14.0, so 16.14.0 is the floor both satisfy.
46
+
47
+ ★ WHY v8 AND NOT v9, given the platform serves both. A customizer's elements are
48
+ rendered by OneGrid INSIDE OneGrid's own React tree, and OneGrid is Fluent v8. A v8
49
+ cell inherits the host's theme for free. A v9 cell would need its own <FluentProvider>
50
+ wrapper per cell to get Griffel styles + tokens, and would then be themed
51
+ independently of the grid it sits in. v8 is the contract here.
52
+
53
+ A virtual control CANNOT bundle its own React: it returns a ReactElement that the
54
+ PLATFORM renders with the PLATFORM's React, so a second React instance breaks it.
55
+ That is why react/react-dom and @fluentui/react are pinned EXACTLY in package.json to
56
+ the versions declared here — compiled-against must equal served.
57
+ -->
58
+ <platform-library name="React" version="16.14.0" />
59
+ <platform-library name="Fluent" version="8.121.1" />
60
+ <resx path="strings/{{componentName}}.1033.resx" version="1.0.0" />
61
+ </resources>
62
+ </control>
63
+ </manifest>
@@ -0,0 +1,38 @@
1
+ import type { CellEditorOverrides } from '../types';
2
+
3
+ /**
4
+ * Cell EDITOR overrides — what a cell becomes when the user starts editing it.
5
+ *
6
+ * Empty by default: OneGrid's built-in editors already handle every column type correctly,
7
+ * including the lookup search and option-set pickers that are genuinely hard to reimplement.
8
+ * Override only where the default is wrong for your data.
9
+ *
10
+ * The keys and the fall-through rule are identical to `CellRendererOverrides`: key by
11
+ * `ColumnDataType`, narrow to a column with `editorParams.colDefs[editorParams.columnIndex]`,
12
+ * and return `undefined` to keep the platform editor.
13
+ *
14
+ * A worked example — clamp a percentage column to 0–100:
15
+ *
16
+ * ```tsx
17
+ * import * as React from 'react';
18
+ * import { SpinButton } from '@fluentui/react';
19
+ *
20
+ * export const cellEditorOverrides: CellEditorOverrides = {
21
+ * Decimal: (defaultProps, editorParams) => {
22
+ * const name = editorParams.colDefs[editorParams.columnIndex]?.name;
23
+ * if (name !== 'dvk_percentcomplete') return undefined;
24
+ * return (
25
+ * <SpinButton
26
+ * min={0}
27
+ * max={100}
28
+ * step={5}
29
+ * defaultValue={String(defaultProps.value ?? 0)}
30
+ * // Commit through onChange; the grid writes the value back to the record.
31
+ * onChange={(_e, v) => defaultProps.onChange(Number(v ?? 0))}
32
+ * />
33
+ * );
34
+ * },
35
+ * };
36
+ * ```
37
+ */
38
+ export const cellEditorOverrides: CellEditorOverrides = {};