@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.
- package/README.md +2 -2
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +23 -5
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/injectDevTools.d.ts +2 -0
- package/dist/injectDevTools.d.ts.map +1 -1
- package/dist/injectDevTools.js +27 -1
- package/dist/injectDevTools.js.map +1 -1
- package/dist/utils/installDependencies.d.ts +15 -0
- package/dist/utils/installDependencies.d.ts.map +1 -1
- package/dist/utils/installDependencies.js +32 -2
- package/dist/utils/installDependencies.js.map +1 -1
- package/package.json +1 -1
- package/templates/_shared/dev-tools/auth/get-token.cjs +1 -1
- package/templates/_shared/dev-tools/metadata-sync/index.js +23 -11
- package/templates/_shared/dev-tools/smoke/test-retrieve.js +45 -16
- package/templates/grid-customizer/README.md +105 -15
- package/templates/grid-customizer/dev/harness/MockGrid.tsx +165 -0
- package/templates/grid-customizer/dev/harness/dataverse.ts +168 -0
- package/templates/grid-customizer/dev/index.html +20 -0
- package/templates/grid-customizer/dev/main.tsx +260 -0
- package/templates/grid-customizer/dev/vite-env.d.ts +1 -0
- package/templates/grid-customizer/env.local.example +24 -0
- package/templates/grid-customizer/eslintrc.json +11 -0
- package/templates/grid-customizer/featureconfig.json +3 -0
- package/templates/grid-customizer/gitignore +3 -0
- package/templates/grid-customizer/package.json +30 -16
- package/templates/grid-customizer/pcfconfig.json +3 -0
- package/templates/grid-customizer/tsconfig.dev.json +16 -0
- package/templates/grid-customizer/tsconfig.json +17 -10
- package/templates/grid-customizer/vite.config.ts +64 -0
- package/templates/grid-customizer/{{componentName}}/ControlManifest.Input.xml +63 -0
- package/templates/grid-customizer/{{componentName}}/customizers/CellEditorOverrides.tsx +38 -0
- package/templates/grid-customizer/{{componentName}}/customizers/CellRendererOverrides.tsx +109 -0
- package/templates/grid-customizer/{{componentName}}/customizers/cells.tsx +137 -0
- package/templates/grid-customizer/{{componentName}}/customizers/config.ts +84 -0
- package/templates/grid-customizer/{{componentName}}/index.ts +81 -0
- package/templates/grid-customizer/{{componentName}}/strings/{{componentName}}.1033.resx +56 -0
- package/templates/grid-customizer/{{componentName}}/types.ts +168 -0
- package/templates/pcf-dataset/dev/main.tsx +5 -1
- package/templates/pcf-dataset/{{componentName}}/ControlManifest.Input.xml +8 -1
- package/templates/pcf-field/dev/harness/dataverse.ts +71 -0
- package/templates/pcf-field/dev/index.html +20 -0
- package/templates/pcf-field/dev/main.tsx +212 -0
- package/templates/pcf-field/dev/vite-env.d.ts +1 -0
- package/templates/pcf-field/env.local.example +24 -0
- package/templates/pcf-field/gitignore +4 -0
- package/templates/pcf-field/package.json +10 -2
- package/templates/pcf-field/tsconfig.dev.json +15 -0
- package/templates/pcf-field/tsconfig.json +20 -4
- package/templates/pcf-field/vite.config.ts +60 -0
- package/templates/pcf-field/{{componentName}}/{{componentName}}Component.tsx +6 -1
- package/templates/static-web-app/api/local.settings.json +7 -0
- package/templates/static-web-app/frontend/vite.config.ts +9 -1
- package/templates/static-web-app/package.json +3 -1
- package/templates/static-web-app/staticwebapp.config.json +4 -1
- package/templates/grid-customizer/src/GridCustomizer.ts +0 -28
- package/templates/grid-customizer/src/cell-renderers.tsx +0 -35
- package/templates/grid-customizer/src/index.ts +0 -4
- package/templates/grid-customizer/src/types/grid-types.ts +0 -30
- package/templates/grid-customizer/src/utils/color-utils.ts +0 -24
- 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
|
+
}
|
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"description": "{{componentName}} —
|
|
5
|
-
"
|
|
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": "
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
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": "
|
|
14
|
-
"
|
|
15
|
-
"
|
|
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
|
-
"@
|
|
19
|
-
"@types/
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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,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
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"moduleResolution": "node",
|
|
6
|
-
"jsx": "react-jsx",
|
|
7
|
-
"strict": true,
|
|
4
|
+
"typeRoots": ["node_modules/@types"],
|
|
5
|
+
"jsx": "react",
|
|
8
6
|
"esModuleInterop": true,
|
|
9
|
-
"
|
|
10
|
-
"skipLibCheck": true
|
|
11
|
-
"declaration": false,
|
|
12
|
-
"lib": ["DOM", "DOM.Iterable", "ES2018"]
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"skipLibCheck": true
|
|
13
9
|
},
|
|
14
|
-
"
|
|
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 = {};
|