@khester/create-dynamics-app 3.1.0 → 3.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 (49) 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 +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/grid-customizer/README.md +105 -15
  9. package/templates/grid-customizer/dev/harness/MockGrid.tsx +165 -0
  10. package/templates/grid-customizer/dev/harness/dataverse.ts +168 -0
  11. package/templates/grid-customizer/dev/index.html +20 -0
  12. package/templates/grid-customizer/dev/main.tsx +260 -0
  13. package/templates/grid-customizer/dev/vite-env.d.ts +1 -0
  14. package/templates/grid-customizer/env.local.example +24 -0
  15. package/templates/grid-customizer/eslintrc.json +11 -0
  16. package/templates/grid-customizer/featureconfig.json +3 -0
  17. package/templates/grid-customizer/gitignore +3 -0
  18. package/templates/grid-customizer/package.json +30 -16
  19. package/templates/grid-customizer/pcfconfig.json +3 -0
  20. package/templates/grid-customizer/tsconfig.dev.json +16 -0
  21. package/templates/grid-customizer/tsconfig.json +17 -10
  22. package/templates/grid-customizer/vite.config.ts +64 -0
  23. package/templates/grid-customizer/{{componentName}}/ControlManifest.Input.xml +63 -0
  24. package/templates/grid-customizer/{{componentName}}/customizers/CellEditorOverrides.tsx +38 -0
  25. package/templates/grid-customizer/{{componentName}}/customizers/CellRendererOverrides.tsx +109 -0
  26. package/templates/grid-customizer/{{componentName}}/customizers/cells.tsx +137 -0
  27. package/templates/grid-customizer/{{componentName}}/customizers/config.ts +84 -0
  28. package/templates/grid-customizer/{{componentName}}/index.ts +81 -0
  29. package/templates/grid-customizer/{{componentName}}/strings/{{componentName}}.1033.resx +56 -0
  30. package/templates/grid-customizer/{{componentName}}/types.ts +168 -0
  31. package/templates/pcf-dataset/dev/main.tsx +5 -1
  32. package/templates/pcf-dataset/{{componentName}}/ControlManifest.Input.xml +8 -1
  33. package/templates/pcf-field/dev/harness/dataverse.ts +71 -0
  34. package/templates/pcf-field/dev/index.html +20 -0
  35. package/templates/pcf-field/dev/main.tsx +212 -0
  36. package/templates/pcf-field/dev/vite-env.d.ts +1 -0
  37. package/templates/pcf-field/env.local.example +24 -0
  38. package/templates/pcf-field/gitignore +4 -0
  39. package/templates/pcf-field/package.json +10 -2
  40. package/templates/pcf-field/tsconfig.dev.json +15 -0
  41. package/templates/pcf-field/tsconfig.json +20 -4
  42. package/templates/pcf-field/vite.config.ts +60 -0
  43. package/templates/pcf-field/{{componentName}}/{{componentName}}Component.tsx +6 -1
  44. package/templates/grid-customizer/src/GridCustomizer.ts +0 -28
  45. package/templates/grid-customizer/src/cell-renderers.tsx +0 -35
  46. package/templates/grid-customizer/src/index.ts +0 -4
  47. package/templates/grid-customizer/src/types/grid-types.ts +0 -30
  48. package/templates/grid-customizer/src/utils/color-utils.ts +0 -24
  49. package/templates/grid-customizer/webpack.config.js +0 -17
@@ -0,0 +1,168 @@
1
+ import { apiFetch, toHttpError } from '@dataverse-kit/pcf-dev-harness/shell';
2
+ import type { ColumnDataType } from '../../{{componentName}}/types';
3
+ import type { HarnessColumn, HarnessRow } from './MockGrid';
4
+
5
+ /**
6
+ * TIER 2 of the dev harness — the {{componentName}}-specific live-org read.
7
+ *
8
+ * The reusable parts (shell, proxy, token refresh, `apiFetch`'s one-shot 401/403 retry) come
9
+ * from `@dataverse-kit/pcf-dev-harness`. This file supplies what only THIS control needs,
10
+ * and it is not the rows: it is the **column data types**.
11
+ *
12
+ * ★ A customizer is dispatched entirely by `ColumnDataType`. Guessing the type from a sample
13
+ * value cannot work — a Currency and a Decimal are both `number` over the wire, a Picklist
14
+ * and an Integer are both `number`, a DateOnly and a DateAndTime are both strings — so the
15
+ * harness would exercise the wrong handler (or none) and tell you nothing. It therefore
16
+ * reads the real `AttributeType` from entity metadata, the same classification the platform
17
+ * uses when it decides which override to call.
18
+ *
19
+ * All calls go through the same-origin `/api/data` proxy in `vite.config.ts`, which injects
20
+ * the bearer token server-side — no token ever reaches the browser. READ-ONLY: the harness
21
+ * never writes, so there is no save counterpart.
22
+ */
23
+
24
+ export type { HttpError } from '@dataverse-kit/pcf-dev-harness/shell';
25
+
26
+ /** What the grid contract calls a column, keyed by Dataverse's `AttributeTypeCode`. */
27
+ const ATTRIBUTE_TYPE_TO_COLUMN_TYPE: Readonly<Record<string, ColumnDataType>> = {
28
+ // The three that actually drive this template's overrides.
29
+ Picklist: 'OptionSet',
30
+ State: 'OptionSet',
31
+ Status: 'OptionSet',
32
+ Boolean: 'TwoOptions',
33
+ Money: 'Currency',
34
+ Decimal: 'Decimal',
35
+ Double: 'FloatingPoint',
36
+ // Everything below is mapped for completeness, so a column you add an override for later
37
+ // is dispatched correctly without touching this file.
38
+ Integer: 'Integer',
39
+ BigInt: 'Integer',
40
+ String: 'Text',
41
+ Memo: 'TextArea',
42
+ DateTime: 'DateAndTime',
43
+ Lookup: 'Lookup',
44
+ Customer: 'Customer',
45
+ Owner: 'Owner',
46
+ Virtual: 'Text',
47
+ Uniqueidentifier: 'Text',
48
+ EntityName: 'Text',
49
+ };
50
+
51
+ /**
52
+ * ★ Lookup-typed columns are mapped above but cannot be LOADED by this harness. Dataverse
53
+ * requires the `_<name>_value` form in `$select` for a lookup, while the metadata call wants
54
+ * the plain logical name — so a binding that lists one gets a clean metadata read and then a
55
+ * 400 on the row fetch. Drop lookups from the Columns box; the mapping is here so that a
56
+ * lookup override you add later dispatches correctly in the org, where the platform supplies
57
+ * the columns rather than you typing them.
58
+ */
59
+
60
+ interface AttributeMetadata {
61
+ LogicalName: string;
62
+ AttributeType: string;
63
+ DisplayName?: { UserLocalizedLabel?: { Label?: string } | null } | null;
64
+ }
65
+
66
+ /** OData annotation carrying the text the platform itself would paint in the cell. */
67
+ const FORMATTED = '@OData.Community.Display.V1.FormattedValue';
68
+
69
+ /** Resolve an entity SET name (`accounts`) to its logical name (`account`) for metadata. */
70
+ async function resolveLogicalName(entitySetName: string): Promise<string> {
71
+ const params = new URLSearchParams({
72
+ $select: 'LogicalName',
73
+ $filter: `EntitySetName eq '${entitySetName.replace(/'/g, "''")}'`,
74
+ });
75
+ const res = await apiFetch(`/api/data/v9.2/EntityDefinitions?${params}`);
76
+ if (!res.ok) throw await toHttpError(res);
77
+ const body = (await res.json()) as { value?: { LogicalName: string }[] };
78
+ const logicalName = body.value?.[0]?.LogicalName;
79
+ if (!logicalName) {
80
+ throw new Error(`No table has entity set name "${entitySetName}".`);
81
+ }
82
+ return logicalName;
83
+ }
84
+
85
+ /**
86
+ * Read `AttributeType` + display label for every requested column, in the order given.
87
+ *
88
+ * Fetches the table's WHOLE attribute set (~200 rows for `account`) rather than filtering to
89
+ * the requested columns. Deliberate: an `or`-chain built from user-typed field names is the
90
+ * one place in this file where input would reach an OData filter expression, and the saving
91
+ * is a few hundred KB on a dev-only call made once per Load.
92
+ */
93
+ async function fetchColumnTypes(
94
+ logicalName: string,
95
+ fields: string[],
96
+ ): Promise<HarnessColumn[]> {
97
+ const params = new URLSearchParams({ $select: 'LogicalName,AttributeType,DisplayName' });
98
+ const res = await apiFetch(
99
+ `/api/data/v9.2/EntityDefinitions(LogicalName='${logicalName}')/Attributes?${params}`,
100
+ );
101
+ if (!res.ok) throw await toHttpError(res);
102
+ const body = (await res.json()) as { value?: AttributeMetadata[] };
103
+
104
+ const byName = new Map((body.value ?? []).map((a) => [a.LogicalName, a]));
105
+ return fields.map((f, i) => {
106
+ const meta = byName.get(f);
107
+ return {
108
+ name: f,
109
+ displayName: meta?.DisplayName?.UserLocalizedLabel?.Label ?? f,
110
+ // An unknown or missing attribute falls back to Text — it then renders through the
111
+ // default path, which is the honest outcome for a column we cannot classify.
112
+ dataType: (meta && ATTRIBUTE_TYPE_TO_COLUMN_TYPE[meta.AttributeType]) ?? 'Text',
113
+ width: i === 0 ? 220 : 160,
114
+ };
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Fetch `top` rows of `select` columns, with each column's real data type attached.
120
+ *
121
+ * @param entitySetName plural set name, e.g. `accounts`
122
+ * @param select comma-separated logical names, e.g. `name,revenue,statuscode`
123
+ * @param top max rows
124
+ */
125
+ export async function fetchGridData(
126
+ entitySetName: string,
127
+ select: string,
128
+ top: string,
129
+ ): Promise<{ columns: HarnessColumn[]; rows: HarnessRow[] }> {
130
+ const fields = select
131
+ .split(',')
132
+ .map((s) => s.trim())
133
+ .filter(Boolean);
134
+ if (fields.length === 0) throw new Error('Enter at least one column.');
135
+
136
+ const logicalName = await resolveLogicalName(entitySetName);
137
+ const columns = await fetchColumnTypes(logicalName, fields);
138
+
139
+ const params = new URLSearchParams({ $select: fields.join(','), $top: top || '25' });
140
+ const res = await apiFetch(`/api/data/v9.2/${entitySetName}?${params}`, {
141
+ // Ask for the formatted-value annotations the deployed control renders.
142
+ headers: { Prefer: 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"' },
143
+ });
144
+ if (!res.ok) throw await toHttpError(res);
145
+ const body = (await res.json()) as { value?: Record<string, unknown>[] };
146
+
147
+ const rows: HarnessRow[] = (body.value ?? []).map((record) => {
148
+ const row: HarnessRow = {};
149
+ for (const f of fields) {
150
+ const raw = record[f];
151
+ const formatted = record[`${f}${FORMATTED}`];
152
+ row[f] = {
153
+ raw: raw ?? undefined,
154
+ // No annotation means the value needs no formatting (plain text, a number the
155
+ // platform shows as-is) — stringify the raw value, as the grid would.
156
+ formatted:
157
+ formatted !== undefined && formatted !== null
158
+ ? String(formatted)
159
+ : raw === undefined || raw === null
160
+ ? ''
161
+ : String(raw),
162
+ };
163
+ }
164
+ return row;
165
+ });
166
+
167
+ return { columns, rows };
168
+ }
@@ -0,0 +1,20 @@
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>{{componentName}} – Dev Harness</title>
7
+ <style>
8
+ html, body, #root {
9
+ margin: 0;
10
+ padding: 0;
11
+ height: 100%;
12
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
13
+ }
14
+ </style>
15
+ </head>
16
+ <body>
17
+ <div id="root"></div>
18
+ <script type="module" src="./main.tsx"></script>
19
+ </body>
20
+ </html>
@@ -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
  }