@khester/create-dynamics-app 3.2.0 → 3.4.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.
@@ -10,7 +10,13 @@ import {
10
10
  type HarnessField,
11
11
  } from '@dataverse-kit/pcf-dev-harness/shell';
12
12
  import { {{componentName}}Component } from '../{{componentName}}/{{componentName}}Component';
13
- import { readField, saveField, type HttpError } from './harness/dataverse';
13
+ import {
14
+ readField,
15
+ saveField,
16
+ readChoiceOptions,
17
+ type ChoiceOption,
18
+ type HttpError,
19
+ } from './harness/dataverse';
14
20
 
15
21
  /**
16
22
  * Dev harness — renders the control against either a local value or a REAL Dataverse column.
@@ -38,20 +44,38 @@ const LIVE_ENABLED = !!import.meta.env.VITE_DYNAMICS_URL;
38
44
  * column's choice metadata — note the UPPERCASE `Options`/`Value`/`Label`, which is the live
39
45
  * PCF API shape, not the form-builder's design-time config. Harmless for the other variants,
40
46
  * which ignore `context` entirely.
47
+ *
48
+ * These placeholder options are for MOCK mode only. In Live mode the harness replaces them with
49
+ * the column's REAL choices — see `buildContext` / `readChoiceOptions`.
41
50
  */
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>;
51
+ const MOCK_OPTIONS: ChoiceOption[] = [
52
+ { Value: 1, Label: 'Option A' },
53
+ { Value: 2, Label: 'Option B' },
54
+ { Value: 3, Label: 'Option C' },
55
+ ];
56
+
57
+ /**
58
+ * Wrap a set of choices in the PCF context shape the control reads.
59
+ *
60
+ * ★ Live mode MUST NOT use the placeholder options — not even as a fallback when the metadata
61
+ * lookup returns nothing. Two distinct failures, and the second is the worse one:
62
+ *
63
+ * - placeholders in live mode with a NON-matching value (the original defect): a real value like
64
+ * 100000002 matches none of 1/2/3, the Dropdown shows its placeholder, and the control looks
65
+ * broken while the read was correct. Recorded against row 02's G5.
66
+ * - placeholders in live mode with a MATCHING value (found in review of #306): the shipped
67
+ * default binding is `accounts` / `accountcategorycode`, whose real values are
68
+ * 1 = "Preferred Customer" and 2 = "Standard". Falling back to the placeholders renders
69
+ * "Option A" — plausible, confidently wrong, and indistinguishable from working. A blank
70
+ * reads as broken; a wrong label reads as fine. Strictly worse than the bug being fixed.
71
+ *
72
+ * So live mode passes `[]` when metadata is unavailable: the optionset variant then degrades to
73
+ * the numeric input it already documents for a control with no choice metadata, which is honest.
74
+ */
75
+ const buildContext = (options: ChoiceOption[]): ComponentFramework.Context<any> =>
76
+ ({
77
+ parameters: { value: { attributes: { Options: options } } },
78
+ }) as unknown as ComponentFramework.Context<any>;
55
79
 
56
80
  /** Turn a failed live call into a red status line, calling out auth failures specifically. */
57
81
  function describeError(e: unknown, verb: string): React.ReactNode {
@@ -71,6 +95,9 @@ const Harness: React.FC = () => {
71
95
  const [height, setHeight] = usePersistedState('height', 0);
72
96
  const [theme, setTheme] = usePersistedState<ThemeChoice>('theme', 'light');
73
97
  const [dataSource, setDataSource] = usePersistedState<DataSource>('dataSource', 'mock');
98
+ // Live choice metadata for the bound column. NOT persisted: it belongs to whichever binding
99
+ // was last loaded, and a stale set from a previous column would mislabel the value.
100
+ const [liveOptions, setLiveOptions] = React.useState<ChoiceOption[] | null>(null);
74
101
  const [placeholder, setPlaceholder] = usePersistedState('placeholder', 'Enter a value');
75
102
  const [disabled, setDisabled] = usePersistedState('disabled', false);
76
103
  const [binding, setBinding] = usePersistedState<FieldBinding>('binding', {
@@ -101,6 +128,9 @@ const Harness: React.FC = () => {
101
128
  const onDataSourceChange = (next: DataSource): void => {
102
129
  setDataSource(next);
103
130
  setValue({{valueDefault}});
131
+ // Drop the live choices with the live value. Keeping them would label MOCK mode with the
132
+ // last-loaded column's real options — the mirror image of the bug this fetch fixes.
133
+ setLiveOptions(null);
104
134
  setLiveStatus(
105
135
  next === 'mock' ? null : <span style={{ color: '#605e5c' }}>Enter a binding and click Load.</span>,
106
136
  );
@@ -114,13 +144,28 @@ const Harness: React.FC = () => {
114
144
  }
115
145
  setLiveStatus(<span style={{ color: '#605e5c' }}>Loading…</span>);
116
146
  try {
117
- const { raw, formatted } = await readField(binding.entitySetName, binding.recordId, binding.column);
147
+ // Fetched together: the VALUE is meaningless to an option-set control without the
148
+ // choices to resolve it against, and issuing them in parallel keeps Load one round trip
149
+ // deep from the user's point of view. `readChoiceOptions` resolves to null for a column
150
+ // that has no choices, which is the normal case for every non-optionset variant.
151
+ const [{ raw, formatted }, options] = await Promise.all([
152
+ readField(binding.entitySetName, binding.recordId, binding.column),
153
+ // `.catch` makes the documented contract true: readChoiceOptions handles non-ok
154
+ // RESPONSES, but a fetch rejection or a JSON parse failure would otherwise propagate
155
+ // through Promise.all and discard a perfectly good value read. (Review of #306.)
156
+ readChoiceOptions(binding.entitySetName, binding.column).catch(() => null),
157
+ ]);
158
+ setLiveOptions(options);
118
159
  // The control owns its own typing; the harness hands the raw platform value straight
119
160
  // through, exactly as the PCF host does via `context.parameters.value.raw`.
120
161
  setValue(raw as {{tsType}});
162
+ // ★ A null metadata result gets its own signal. Without one, a choice column whose
163
+ // metadata could not be read looks identical to a successful load — green status, empty
164
+ // dropdown — and the reader has nothing to go on. (Review of #306.)
121
165
  setLiveStatus(
122
- <span style={{ color: '#107c10' }}>
166
+ <span style={{ color: options === null ? '#8a6116' : '#107c10' }}>
123
167
  Loaded{formatted ? ` — "${formatted}"` : ''}.
168
+ {options === null ? ' No choice metadata for this column — the value is shown raw.' : ''}
124
169
  </span>,
125
170
  );
126
171
  } catch (e) {
@@ -193,7 +238,7 @@ const Harness: React.FC = () => {
193
238
  disabled={disabled}
194
239
  onChange={setValue}
195
240
  onBlur={() => undefined}
196
- context={MOCK_CONTEXT}
241
+ context={buildContext(dataSource === 'live' ? (liveOptions ?? []) : MOCK_OPTIONS)}
197
242
  />
198
243
  {dataSource === 'live' && (
199
244
  <button
@@ -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
  }