@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,71 @@
1
+ import { apiFetch, toHttpError } from '@dataverse-kit/pcf-dev-harness/shell';
2
+
3
+ /**
4
+ * TIER 2 of the dev harness — the {{componentName}}-specific live-org read/write.
5
+ *
6
+ * The reusable parts (shell, proxy, token refresh, `apiFetch`'s one-shot 401/403 retry) come
7
+ * from `@dataverse-kit/pcf-dev-harness`. This file is what only THIS control can supply:
8
+ * reading and writing a SINGLE COLUMN on a single record — which is exactly what a field
9
+ * control does, so the harness exercises the same round-trip the deployed control performs.
10
+ *
11
+ * All calls go through the same-origin `/api/data` proxy in `vite.config.ts`, which injects
12
+ * the bearer token server-side — no token ever reaches the browser.
13
+ */
14
+
15
+ export type { HttpError } from '@dataverse-kit/pcf-dev-harness/shell';
16
+
17
+ /** The raw value plus the platform's formatted rendering, when the column has one. */
18
+ export interface FieldRead {
19
+ raw: unknown;
20
+ formatted?: string;
21
+ }
22
+
23
+ /**
24
+ * Read one column off one record.
25
+ *
26
+ * @param entitySetName plural set name, e.g. `accounts`
27
+ * @param recordId GUID, with or without braces
28
+ * @param column logical name, e.g. `accountcategorycode`
29
+ */
30
+ export async function readField(
31
+ entitySetName: string,
32
+ recordId: string,
33
+ column: string,
34
+ ): Promise<FieldRead> {
35
+ const id = recordId.replace(/[{}]/g, '').trim();
36
+ const res = await apiFetch(`/api/data/v9.2/${entitySetName}(${id})?$select=${column}`, {
37
+ // Ask for the formatted-value annotation so option-set labels / currency symbols show
38
+ // the same text the platform renders on a form.
39
+ headers: { Prefer: 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"' },
40
+ });
41
+ if (!res.ok) throw await toHttpError(res);
42
+
43
+ const body = (await res.json()) as Record<string, unknown>;
44
+ return {
45
+ raw: body[column] ?? null,
46
+ formatted: body[`${column}@OData.Community.Display.V1.FormattedValue`] as string | undefined,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Write one column back to one record — the save half of the round-trip.
52
+ *
53
+ * PATCH with a single property, exactly as the deployed control's `getOutputs` → platform save
54
+ * would. Returns nothing; a non-2xx throws an HttpError the caller can surface.
55
+ */
56
+ export async function saveField(
57
+ entitySetName: string,
58
+ recordId: string,
59
+ column: string,
60
+ value: unknown,
61
+ ): Promise<void> {
62
+ const id = recordId.replace(/[{}]/g, '').trim();
63
+ const res = await apiFetch(`/api/data/v9.2/${entitySetName}(${id})`, {
64
+ method: 'PATCH',
65
+ headers: { 'Content-Type': 'application/json' },
66
+ // `null` is meaningful here — it clears the column, which is a real thing a field
67
+ // control must be able to do, so it is NOT coerced away.
68
+ body: JSON.stringify({ [column]: value === undefined ? null : value }),
69
+ });
70
+ if (!res.ok) throw await toHttpError(res);
71
+ }
@@ -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,212 @@
1
+ import * as React from 'react';
2
+ // ReactDOM.render, NOT createRoot — this control is pinned to React 16.14.0 (the floor the
3
+ // canvas runtime serves), 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 { {{componentName}}Component } from '../{{componentName}}/{{componentName}}Component';
13
+ import { readField, saveField, type HttpError } from './harness/dataverse';
14
+
15
+ /**
16
+ * Dev harness — renders the control against either a local value or a REAL Dataverse column.
17
+ *
18
+ * - Mock — the local value below. Default; works with no org.
19
+ * - Live — read a column off a record via the /api/data proxy, edit it in the control, and
20
+ * Save it back. Enabled only when VITE_DYNAMICS_URL is set in `.env.local`.
21
+ *
22
+ * The read/edit/save round-trip is the point: `npm start` (pcf-scripts, :8181) can only feed
23
+ * this control hand-typed values, so it never proves the control round-trips real org data.
24
+ */
25
+
26
+ interface FieldBinding {
27
+ entitySetName: string;
28
+ recordId: string;
29
+ column: string;
30
+ }
31
+
32
+ // VITE_-prefixed vars reach client code; this one only GATES Live mode. The token is
33
+ // deliberately not read here — it would be inlined into the bundle. The proxy injects it.
34
+ const LIVE_ENABLED = !!import.meta.env.VITE_DYNAMICS_URL;
35
+
36
+ /**
37
+ * Minimal stand-in for the PCF context. Only the OptionSet variant reads it, for the bound
38
+ * column's choice metadata — note the UPPERCASE `Options`/`Value`/`Label`, which is the live
39
+ * PCF API shape, not the form-builder's design-time config. Harmless for the other variants,
40
+ * which ignore `context` entirely.
41
+ */
42
+ const MOCK_CONTEXT = {
43
+ parameters: {
44
+ value: {
45
+ attributes: {
46
+ Options: [
47
+ { Value: 1, Label: 'Option A' },
48
+ { Value: 2, Label: 'Option B' },
49
+ { Value: 3, Label: 'Option C' },
50
+ ],
51
+ },
52
+ },
53
+ },
54
+ } as unknown as ComponentFramework.Context<any>;
55
+
56
+ /** Turn a failed live call into a red status line, calling out auth failures specifically. */
57
+ function describeError(e: unknown, verb: string): React.ReactNode {
58
+ const err = e as HttpError;
59
+ // The dev server refreshes the token in the background, so a 401/403 that survives the
60
+ // client retry means the `az login` session itself lapsed — not just the short-lived token.
61
+ const msg =
62
+ err.status === 401 || err.status === 403
63
+ ? 'Not authorized — your `az login` session has expired. Run `az login`, then try again.'
64
+ : `Failed to ${verb}: ${err.message}`;
65
+ return <span style={{ color: '#a4262c' }}>{msg}</span>;
66
+ }
67
+
68
+ const Harness: React.FC = () => {
69
+ // Panel CONFIG persists across reloads (localStorage); the fetched VALUE does not.
70
+ const [width, setWidth] = usePersistedState('width', 320);
71
+ const [height, setHeight] = usePersistedState('height', 0);
72
+ const [theme, setTheme] = usePersistedState<ThemeChoice>('theme', 'light');
73
+ const [dataSource, setDataSource] = usePersistedState<DataSource>('dataSource', 'mock');
74
+ const [placeholder, setPlaceholder] = usePersistedState('placeholder', 'Enter a value');
75
+ const [disabled, setDisabled] = usePersistedState('disabled', false);
76
+ const [binding, setBinding] = usePersistedState<FieldBinding>('binding', {
77
+ entitySetName: 'accounts',
78
+ recordId: '',
79
+ column: 'accountcategorycode',
80
+ });
81
+
82
+ const [value, setValue] = React.useState<{{tsType}}>({{valueDefault}});
83
+ const [liveStatus, setLiveStatus] = React.useState<React.ReactNode>(null);
84
+
85
+ // On reload with a restored Live selection: clear and prompt for Load — or fall back to Mock
86
+ // when this session has no org configured. Runs once on mount.
87
+ React.useEffect(() => {
88
+ if (dataSource !== 'live') return;
89
+ if (!LIVE_ENABLED) {
90
+ setDataSource('mock');
91
+ return;
92
+ }
93
+ setLiveStatus(<span style={{ color: '#605e5c' }}>Binding restored — click Load.</span>);
94
+ // Mount-only by design: `[]` deliberately omits dataSource/LIVE_ENABLED, because this
95
+ // reconciles the RESTORED selection once and must not re-run when it later changes.
96
+ // (No eslint-disable here: this template's .eslintrc loads no plugins — a devDependency
97
+ // is not enough — so a `react-hooks/exhaustive-deps` disable names an unresolvable rule,
98
+ // which `pcf-scripts build` treats as an ESLint ERROR. Gated by gridCustomizer.test.ts.)
99
+ }, []);
100
+
101
+ const onDataSourceChange = (next: DataSource): void => {
102
+ setDataSource(next);
103
+ setValue({{valueDefault}});
104
+ setLiveStatus(
105
+ next === 'mock' ? null : <span style={{ color: '#605e5c' }}>Enter a binding and click Load.</span>,
106
+ );
107
+ };
108
+
109
+ const onLoadLive = (): void => {
110
+ void (async () => {
111
+ if (!binding.entitySetName || !binding.recordId || !binding.column) {
112
+ setLiveStatus(<span style={{ color: '#a4262c' }}>Enter entity set, record id and column.</span>);
113
+ return;
114
+ }
115
+ setLiveStatus(<span style={{ color: '#605e5c' }}>Loading…</span>);
116
+ try {
117
+ const { raw, formatted } = await readField(binding.entitySetName, binding.recordId, binding.column);
118
+ // The control owns its own typing; the harness hands the raw platform value straight
119
+ // through, exactly as the PCF host does via `context.parameters.value.raw`.
120
+ setValue(raw as {{tsType}});
121
+ setLiveStatus(
122
+ <span style={{ color: '#107c10' }}>
123
+ Loaded{formatted ? ` — "${formatted}"` : ''}.
124
+ </span>,
125
+ );
126
+ } catch (e) {
127
+ setLiveStatus(describeError(e, 'load'));
128
+ }
129
+ })();
130
+ };
131
+
132
+ const onSave = (): void => {
133
+ void (async () => {
134
+ setLiveStatus(<span style={{ color: '#605e5c' }}>Saving…</span>);
135
+ try {
136
+ await saveField(binding.entitySetName, binding.recordId, binding.column, value);
137
+ setLiveStatus(<span style={{ color: '#107c10' }}>Saved to the org.</span>);
138
+ } catch (e) {
139
+ setLiveStatus(describeError(e, 'save'));
140
+ }
141
+ })();
142
+ };
143
+
144
+ const bindingFields: HarnessField[] = [
145
+ {
146
+ kind: 'text',
147
+ label: 'Entity set (plural)',
148
+ placeholder: 'accounts',
149
+ value: binding.entitySetName,
150
+ onChange: (v) => setBinding((b) => ({ ...b, entitySetName: v })),
151
+ },
152
+ {
153
+ kind: 'text',
154
+ label: 'Record id (GUID)',
155
+ placeholder: '00000000-0000-0000-0000-000000000000',
156
+ value: binding.recordId,
157
+ onChange: (v) => setBinding((b) => ({ ...b, recordId: v })),
158
+ },
159
+ {
160
+ kind: 'text',
161
+ label: 'Column (logical name)',
162
+ placeholder: 'accountcategorycode',
163
+ value: binding.column,
164
+ onChange: (v) => setBinding((b) => ({ ...b, column: v })),
165
+ },
166
+ ];
167
+
168
+ return (
169
+ <HarnessShell
170
+ title="{{componentName}} — dev harness"
171
+ subtitle="Field control ({{propertyType}})"
172
+ width={width}
173
+ onWidthChange={setWidth}
174
+ height={height}
175
+ onHeightChange={setHeight}
176
+ theme={theme}
177
+ onThemeChange={setTheme}
178
+ dataSource={dataSource}
179
+ onDataSourceChange={onDataSourceChange}
180
+ liveEnabled={LIVE_ENABLED}
181
+ bindingFields={bindingFields}
182
+ onLoadLive={onLoadLive}
183
+ liveStatus={liveStatus}
184
+ fields={[
185
+ { kind: 'text', label: 'placeholder', value: placeholder, onChange: setPlaceholder },
186
+ { kind: 'boolean', label: 'disabled', value: disabled, onChange: setDisabled },
187
+ ]}
188
+ >
189
+ <div style={{ width: width || 320 }}>
190
+ <{{componentName}}Component
191
+ value={value}
192
+ placeholder={placeholder}
193
+ disabled={disabled}
194
+ onChange={setValue}
195
+ onBlur={() => undefined}
196
+ context={MOCK_CONTEXT}
197
+ />
198
+ {dataSource === 'live' && (
199
+ <button
200
+ type="button"
201
+ onClick={onSave}
202
+ style={{ marginTop: 12, padding: '6px 14px', cursor: 'pointer' }}
203
+ >
204
+ Save to org
205
+ </button>
206
+ )}
207
+ </div>
208
+ </HarnessShell>
209
+ );
210
+ };
211
+
212
+ 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=
@@ -3,3 +3,7 @@ out/
3
3
  generated/
4
4
  *.log
5
5
  .env
6
+ .env.local
7
+ .env.local.backup
8
+ dev/dist/
9
+ obj/
@@ -7,7 +7,12 @@
7
7
  "clean": "pcf-scripts clean",
8
8
  "rebuild": "pcf-scripts rebuild",
9
9
  "start": "pcf-scripts start",
10
- "refreshTypes": "pcf-scripts refreshTypes"
10
+ "refreshTypes": "pcf-scripts refreshTypes",
11
+ "dev": "vite --config vite.config.ts",
12
+ "dev:token": "npm run auth:token && npm run dev",
13
+ "auth:token": "pcf-dev-token",
14
+ "typecheck:dev": "tsc -p tsconfig.dev.json --noEmit",
15
+ "harness:check": "npm run typecheck:dev && vite build --config vite.config.ts"
11
16
  },
12
17
  "dependencies": {
13
18
  "@fluentui/react": "^8.120.0",
@@ -16,15 +21,18 @@
16
21
  "react-dom": "^16.14.0"
17
22
  },
18
23
  "devDependencies": {
24
+ "@dataverse-kit/pcf-dev-harness": "^0.2.0",
19
25
  "@microsoft/eslint-config-spfx": "^1.18.2",
20
26
  "@types/node": "^18.16.9",
21
27
  "@types/react": "^16.14.34",
22
28
  "@types/react-dom": "^16.9.17",
23
29
  "@typescript-eslint/parser": "^6.21.0",
30
+ "@vitejs/plugin-react": "^4.3.4",
24
31
  "eslint-plugin-react-hooks": "^4.6.0",
25
32
  "pcf-scripts": "^1",
26
33
  "pcf-start": "^1",
27
- "typescript": "^5.3.0"
34
+ "typescript": "^5.3.0",
35
+ "vite": "^5.4.0"
28
36
  },
29
37
  "browserslist": [
30
38
  "last 2 versions"
@@ -0,0 +1,15 @@
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 control's App, which pulls the shared component in too."
12
+ ],
13
+ "exclude": ["node_modules", "out"],
14
+ "include": ["dev", "vite.config.ts"]
15
+ }
@@ -1,8 +1,24 @@
1
1
  {
2
2
  "extends": "./node_modules/pcf-scripts/tsconfig_base.json",
3
3
  "compilerOptions": {
4
- "typeRoots": ["node_modules/@types"],
4
+ "typeRoots": [
5
+ "node_modules/@types"
6
+ ],
5
7
  "jsx": "react",
6
- "experimentalDecorators": true
7
- }
8
- }
8
+ "experimentalDecorators": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "//": [
12
+ "\u2605 `exclude` REPLACES the base's [\"./node_modules\"] \u2014 node_modules MUST be re-listed here.",
13
+ "pcf-scripts runs ts-loader in full (non-transpileOnly) mode, and ts-loader seeds its file",
14
+ "list from EVERY file the tsconfig resolves \u2014 not just what webpack reaches from the entry.",
15
+ "So without this exclude the Vite harness under dev/ would be typechecked as part of the",
16
+ "control build and could fail it. dev/ has its own project: tsconfig.dev.json."
17
+ ],
18
+ "exclude": [
19
+ "node_modules",
20
+ "out",
21
+ "dev",
22
+ "vite.config.ts"
23
+ ]
24
+ }
@@ -0,0 +1,60 @@
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
+ * Complements `npm start` (the pcf-scripts harness on :8181) rather than replacing it: this
10
+ * control is control-type="standard" on React 16 and bundles its own React, so it loads fine
11
+ * in the stock harness. What the stock harness cannot do is show you REAL org data — it only
12
+ * takes hand-typed values. This one reads and writes an actual Dataverse column.
13
+ *
14
+ * Two data sources (see `dev/main.tsx`):
15
+ * - Mock — a local value. Default; needs no org.
16
+ * - Live — read a column off a real record through the same-origin `/api/data` proxy, edit
17
+ * it, and save it back. Enabled only when VITE_DYNAMICS_URL is set in `.env.local`.
18
+ *
19
+ * The proxy and its in-process Azure-CLI token refresh come from
20
+ * `@dataverse-kit/pcf-dev-harness/node`: the bearer token is injected SERVER-SIDE and never
21
+ * reaches the browser. `az login` + `npm run dev` is enough — `npm run auth:token` only writes
22
+ * an optional static fallback.
23
+ */
24
+ export default defineConfig(({ mode }) => {
25
+ // Read .env* from the PROJECT root (__dirname), not `root: 'dev'`. The empty prefix loads
26
+ // every var server-side; `envPrefix` below still limits what reaches the client bundle.
27
+ const env = loadEnv(mode, __dirname, '');
28
+ const live = dataverseProxy({
29
+ dynamicsUrl: env.VITE_DYNAMICS_URL || '',
30
+ tenant: env.DYNAMICS_TENANT,
31
+ // Array: `.env.local` is the documented file, `.env` is tolerated as a fallback so a token
32
+ // written by another tool still works. First file defining DYNAMICS_TOKEN wins.
33
+ envFile: [path.resolve(__dirname, '.env.local'), path.resolve(__dirname, '.env')],
34
+ pluginName: '{{componentName}}-live-token',
35
+ });
36
+
37
+ return {
38
+ root: 'dev',
39
+ // .env* live at the project root, not the `dev` root, so client code still sees
40
+ // import.meta.env.VITE_DYNAMICS_URL (which gates Live mode).
41
+ envDir: __dirname,
42
+ // SECURITY: the bearer token is acquired server-side by dataverseProxy and is never put in
43
+ // a VITE_-prefixed var, so Vite cannot inline it into the client bundle. envPrefix narrows
44
+ // client exposure to VITE_DYNAMICS_URL alone; the static fallback DYNAMICS_TOKEN is
45
+ // deliberately unprefixed for the same reason.
46
+ envPrefix: 'VITE_DYNAMICS_URL',
47
+ // jsxRuntime: 'classic' — the automatic runtime emits `react/jsx-runtime`, which does not
48
+ // exist in React 16. This control pins 16.14.0 (the floor the canvas runtime serves), and
49
+ // tsconfig sets `"jsx": "react"` (classic) for the same reason.
50
+ plugins: [react({ jsxRuntime: 'classic' }), live.plugin],
51
+ server: {
52
+ // 8182, because `pcf-scripts start` owns 8181 — both harnesses can run side by side.
53
+ // strictPort so a clash fails loudly instead of silently moving to another port.
54
+ port: 8182,
55
+ strictPort: true,
56
+ open: true,
57
+ proxy: live.proxy,
58
+ },
59
+ };
60
+ });
@@ -11,7 +11,12 @@ export interface I{{componentName}}ComponentProps {
11
11
  disabled?: boolean;
12
12
  onChange: (value: {{tsType}}) => void;
13
13
  onBlur: () => void;
14
- context: ComponentFramework.Context<any>;
14
+ /**
15
+ * The bound PCF context. OPTIONAL so the dev harness can render this component
16
+ * without a platform context (only the optionset variant reads it, for the
17
+ * bound column's choice metadata). The PCF host always supplies it.
18
+ */
19
+ context?: ComponentFramework.Context<any>;
15
20
  }
16
21
 
17
22
  export const {{componentName}}Component: React.FC<I{{componentName}}ComponentProps> = ({
@@ -0,0 +1,7 @@
1
+ {
2
+ "IsEncrypted": false,
3
+ "Values": {
4
+ "FUNCTIONS_WORKER_RUNTIME": "node",
5
+ "AzureWebJobsStorage": ""
6
+ }
7
+ }
@@ -5,8 +5,16 @@ export default defineConfig({
5
5
  plugins: [react()],
6
6
  build: { outDir: 'dist' },
7
7
  server: {
8
+ // ★ PINNED, and `strictPort` is load-bearing. The root `dev` script runs
9
+ // `swa start http://localhost:5173`, so the emulator proxies to a hard-coded URL. Vite's
10
+ // default on a busy port is to move silently to 5174/5175/…, which would leave `swa`
11
+ // proxying to a dead port — the shell would serve and every module would fail. `strictPort`
12
+ // turns that into a loud failure instead.
13
+ port: 5173,
14
+ strictPort: true,
8
15
  proxy: {
9
- // Local: proxy /api to the Functions host (swa start wires this in production).
16
+ // Only used when Vite is hit DIRECTLY on :5173 (`npm --prefix frontend run dev` without
17
+ // the emulator). Under `npm run dev` the SWA emulator owns /api routing.
10
18
  '/api': 'http://localhost:7071',
11
19
  },
12
20
  },
@@ -5,8 +5,10 @@
5
5
  "description": "{{projectName}} — Azure Static Web App (React Fluent {{componentLibrary}} + Functions API)",
6
6
  "scripts": {
7
7
  "install:all": "npm install && npm --prefix frontend install && npm --prefix api install",
8
- "dev": "swa start frontend --api-location api --run \"npm --prefix frontend run dev\"",
8
+ "dev": "npm --prefix api run build && swa start http://localhost:5173 --api-location api --run \"npm --prefix frontend run dev\"",
9
+ "dev:api": "npm --prefix api run watch",
9
10
  "build": "npm --prefix frontend run build && npm --prefix api run build",
11
+ "deploy": "swa deploy ./frontend/dist --api-location ./api --api-language node --api-version 20 --env production",
10
12
  "typecheck": "npm --prefix frontend run typecheck && npm --prefix api run typecheck"
11
13
  },
12
14
  "devDependencies": {
@@ -3,5 +3,8 @@
3
3
  "rewrite": "/index.html",
4
4
  "exclude": ["/api/*", "/assets/*"]
5
5
  },
6
- "routes": [{ "route": "/api/*", "allowedRoles": ["anonymous"] }]
6
+ "routes": [{ "route": "/api/*", "allowedRoles": ["anonymous"] }],
7
+ "platform": {
8
+ "apiRuntime": "node:20"
9
+ }
7
10
  }
@@ -1,28 +0,0 @@
1
- import type { IGridCustomizer, CellRendererOverrides } from './types/grid-types';
2
- import {
3
- StatusBadgeRenderer,
4
- CurrencyRenderer,
5
- DateRenderer,
6
- } from './cell-renderers';
7
-
8
- /**
9
- * {{componentName}} — editable-grid customizer. Map a column data type (or a
10
- * specific column logical name) to a cell renderer. Register this customizer on
11
- * the grid control's customizer property.
12
- */
13
- export class {{componentName}} implements IGridCustomizer {
14
- public cellRendererOverrides: CellRendererOverrides = {
15
- // By data type
16
- OptionSet: StatusBadgeRenderer,
17
- Currency: CurrencyRenderer,
18
- // By column logical name
19
- statuscode: StatusBadgeRenderer,
20
- statecode: StatusBadgeRenderer,
21
- createdon: DateRenderer,
22
- modifiedon: DateRenderer,
23
- };
24
-
25
- public cellEditorOverrides?: CellRendererOverrides = {
26
- // Add custom inline editors here, keyed the same way as renderers.
27
- };
28
- }
@@ -1,35 +0,0 @@
1
- import * as React from 'react';
2
- import { Badge, Text } from '@fluentui/react-components';
3
- import type { CellRenderer } from './types/grid-types';
4
- import { getStatusColor } from './utils/color-utils';
5
-
6
- /** Render an option-set / status value as a colored badge. */
7
- export const StatusBadgeRenderer: CellRenderer = ({ value, formattedValue }) => {
8
- const text = formattedValue ?? String(value ?? '');
9
- return (
10
- <Badge appearance="filled" style={{ backgroundColor: getStatusColor(text) }}>
11
- {text}
12
- </Badge>
13
- );
14
- };
15
-
16
- /** Render a number as currency. */
17
- export const CurrencyRenderer: CellRenderer = ({ value }) => {
18
- const n = typeof value === 'number' ? value : Number(value);
19
- const text = Number.isNaN(n)
20
- ? ''
21
- : new Intl.NumberFormat(undefined, { style: 'currency', currency: 'USD' }).format(n);
22
- return <Text>{text}</Text>;
23
- };
24
-
25
- /** Render a 0–100 number as a percentage. */
26
- export const ProgressRenderer: CellRenderer = ({ value }) => {
27
- const n = Math.max(0, Math.min(100, Number(value) || 0));
28
- return <Text>{n}%</Text>;
29
- };
30
-
31
- /** Render a date value as a localized date string. */
32
- export const DateRenderer: CellRenderer = ({ value }) => {
33
- const d = value instanceof Date ? value : new Date(String(value));
34
- return <Text>{Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString()}</Text>;
35
- };
@@ -1,4 +0,0 @@
1
- export { {{componentName}} } from './GridCustomizer';
2
- export * from './cell-renderers';
3
- export * from './types/grid-types';
4
- export * from './utils/color-utils';
@@ -1,30 +0,0 @@
1
- import type * as React from 'react';
2
-
3
- export interface GridColumn {
4
- name: string;
5
- displayName: string;
6
- dataType: string;
7
- }
8
-
9
- export interface CellRendererProps {
10
- value: unknown;
11
- formattedValue?: string;
12
- column: GridColumn;
13
- rowData: Record<string, unknown>;
14
- entityId: string;
15
- entityName: string;
16
- isEditing: boolean;
17
- isDisabled: boolean;
18
- onChange?: (value: unknown) => void;
19
- }
20
-
21
- export type CellRenderer = React.FC<CellRendererProps>;
22
-
23
- export type CellRendererOverrides = {
24
- [key: string]: CellRenderer;
25
- };
26
-
27
- export interface IGridCustomizer {
28
- cellRendererOverrides: CellRendererOverrides;
29
- cellEditorOverrides?: CellRendererOverrides;
30
- }