@khester/create-dynamics-app 3.0.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.
- package/README.md +2 -2
- package/dist/artifacts/registry.d.ts.map +1 -1
- package/dist/artifacts/registry.js +76 -20
- package/dist/artifacts/registry.js.map +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/templatePlan.d.ts.map +1 -1
- package/dist/templatePlan.js +20 -3
- package/dist/templatePlan.js.map +1 -1
- package/dist/utils/copyTemplate.d.ts.map +1 -1
- package/dist/utils/copyTemplate.js +4 -0
- package/dist/utils/copyTemplate.js.map +1 -1
- package/package.json +1 -1
- package/templates/_shared/dataverse-seam/XrmApiService.ts +26 -5
- 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/host-swap/README.md +57 -2
- package/templates/host-swap/_variants/host.custom-page/src/App.tsx +9 -5
- package/templates/host-swap/_variants/host.pcf/ControlManifest.Input.xml +26 -2
- package/templates/host-swap/_variants/host.pcf/gitignore +7 -0
- package/templates/host-swap/_variants/host.pcf/index.ts +21 -3
- package/templates/host-swap/_variants/host.pcf/package.json +20 -4
- package/templates/host-swap/_variants/host.pcf/{{componentName}}.pcfproj +67 -0
- package/templates/host-swap/_variants/host.web-resource/src/App.tsx +5 -1
- package/templates/host-swap/src/Component.tsx +54 -7
- package/templates/host-swap/src/useHostWidth.ts +53 -0
- package/templates/pcf-dataset/dev/harness/dataverse.ts +82 -0
- package/templates/pcf-dataset/dev/index.html +20 -0
- package/templates/pcf-dataset/dev/main.tsx +194 -0
- package/templates/pcf-dataset/dev/vite-env.d.ts +1 -0
- package/templates/pcf-dataset/env.local.example +24 -0
- package/templates/pcf-dataset/eslintrc.json +11 -0
- package/templates/pcf-dataset/gitignore +4 -0
- package/templates/pcf-dataset/package.json +18 -8
- package/templates/pcf-dataset/pcfconfig.json +3 -0
- package/templates/pcf-dataset/tsconfig.dev.json +15 -0
- package/templates/pcf-dataset/tsconfig.json +11 -2
- package/templates/pcf-dataset/vite.config.ts +63 -0
- package/templates/pcf-dataset/{{componentName}}/ControlManifest.Input.xml +52 -0
- package/templates/pcf-dataset/{{componentName}}/{{componentName}}App.tsx +64 -0
- package/templates/pcf-dataset/{{{componentName}}Component.tsx → {{componentName}}/{{componentName}}Component.tsx} +9 -46
- 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/eslintrc.json +11 -0
- package/templates/pcf-field/gitignore +4 -0
- package/templates/pcf-field/package.json +11 -2
- package/templates/pcf-field/pcfconfig.json +3 -0
- 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}}/ControlManifest.Input.xml +42 -0
- package/templates/pcf-field/{{{componentName}}Component.tsx → {{componentName}}/{{componentName}}Component.tsx} +6 -1
- package/templates/power-pages-island/README.md +12 -0
- package/templates/power-pages-island/src/App.tsx +13 -1
- package/templates/power-pages-island/src/Component.tsx +54 -7
- package/templates/power-pages-island/src/useHostWidth.ts +53 -0
- package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +8 -2
- 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
- package/templates/pcf-dataset/ControlManifest.Input.xml +0 -16
- package/templates/pcf-field/ControlManifest.Input.xml +0 -17
- /package/templates/pcf-dataset/{index.ts → {{componentName}}/index.ts} +0 -0
- /package/templates/pcf-dataset/{strings → {{componentName}}/strings}/{{componentName}}.1033.resx +0 -0
- /package/templates/pcf-field/{index.ts → {{componentName}}/index.ts} +0 -0
- /package/templates/pcf-field/{strings → {{componentName}}/strings}/{{componentName}}.1033.resx +0 -0
|
@@ -1,28 +1,118 @@
|
|
|
1
|
-
# {{componentName}}
|
|
1
|
+
# {{componentName}} — Power Apps grid customizer
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
3
|
+
A **PCF virtual control** that overrides how Microsoft's editable grid (PowerAppsOneGrid)
|
|
4
|
+
paints its cells: option sets become coloured badges, currency is right-aligned and coloured
|
|
5
|
+
by sign, yes/no becomes a tick or cross, and nominated numeric columns become progress bars.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
It is **not a grid**. It renders nothing of its own — it hands the platform's grid a map of
|
|
8
|
+
cell renderers and the grid does the drawing.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
OneGrid ──(bound EventName)──▶ {{componentName}}.init
|
|
12
|
+
└─ factory.fireEvent(eventName, { cellRendererOverrides, cellEditorOverrides })
|
|
13
|
+
OneGrid ◀─────────────────────┘ then calls those overrides while painting each cell
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
7
17
|
|
|
8
18
|
```bash
|
|
9
19
|
npm install
|
|
10
|
-
|
|
11
|
-
npm run
|
|
20
|
+
|
|
21
|
+
npm run dev # ★ the working local loop — http://localhost:8182
|
|
22
|
+
npm run build # pcf-scripts build → out/controls/{{componentName}}/
|
|
12
23
|
```
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
`npm start` (the stock `pcf-scripts` harness) will load the control and show a **blank
|
|
26
|
+
canvas**. That is correct, not a bug: the harness has no OneGrid, so nothing ever calls the
|
|
27
|
+
overrides. `npm run dev` runs a stand-in grid that calls them exactly as the platform does.
|
|
28
|
+
|
|
29
|
+
## Local development
|
|
30
|
+
|
|
31
|
+
`npm run dev` serves the harness in `dev/`:
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
| Data source | What it gives you |
|
|
34
|
+
|---|---|
|
|
35
|
+
| **Mock** (default) | One column per overridden data type, including an unmapped status label so you can see the derived-colour fallback. No org needed. |
|
|
36
|
+
| **Live** | Real rows **and real column data types** from your org, read from entity metadata. |
|
|
18
37
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
38
|
+
Live mode needs one line of config plus an Azure login:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cp env.local.example .env.local # set VITE_DYNAMICS_URL=https://<org>.crm.dynamics.com
|
|
42
|
+
az login
|
|
43
|
+
npm run dev
|
|
24
44
|
```
|
|
25
45
|
|
|
46
|
+
The dev server acquires and refreshes the bearer token **in-process, server-side** — it is
|
|
47
|
+
never exposed to the browser bundle. See `env.local.example` for the full contract.
|
|
48
|
+
|
|
49
|
+
> **Why Live mode reads metadata.** A customizer is dispatched purely by `ColumnDataType`.
|
|
50
|
+
> Currency and Decimal are both `number` on the wire; Picklist and Integer are too. Guessing
|
|
51
|
+
> the type from a sample value would exercise the wrong handler, so the harness reads the
|
|
52
|
+
> real `AttributeType` — the same classification the platform uses.
|
|
53
|
+
|
|
54
|
+
## Customizing it
|
|
55
|
+
|
|
56
|
+
| File | What it is for |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `{{componentName}}/customizers/config.ts` | **Start here.** Status colours, which columns get progress bars, currency colours. |
|
|
59
|
+
| `{{componentName}}/customizers/cells.tsx` | What each custom cell looks like. |
|
|
60
|
+
| `{{componentName}}/customizers/CellRendererOverrides.tsx` | Which data type maps to which cell, and how to scope one to a single column. |
|
|
61
|
+
| `{{componentName}}/customizers/CellEditorOverrides.tsx` | Edit-mode overrides. Empty by default — the built-in editors are good. |
|
|
62
|
+
| `{{componentName}}/types.ts` | The platform contract. Vendored: Microsoft ships no package for it. |
|
|
63
|
+
|
|
64
|
+
Three rules the compiler cannot enforce for you:
|
|
65
|
+
|
|
66
|
+
1. **Override keys are column DATA TYPES, never logical names.** `{ statuscode: … }` never
|
|
67
|
+
fires. Register under `OptionSet` and narrow with
|
|
68
|
+
`params.colDefs[params.columnIndex].name`.
|
|
69
|
+
2. **Return `undefined` to fall through** to the grid's own renderer. Returning an empty
|
|
70
|
+
element blanks the cell instead.
|
|
71
|
+
3. **Render `props.formattedValue`, not `props.value`.** `formattedValue` is already
|
|
72
|
+
localized, currency-symbolled and resolved to option labels; `value` is raw.
|
|
73
|
+
|
|
74
|
+
### There is no maker-facing configuration
|
|
75
|
+
|
|
76
|
+
A grid customizer **cannot receive `usage="input"` properties**. The grid's *Customizer
|
|
77
|
+
control* field references this control by name only, with nowhere to put a value, so any
|
|
78
|
+
input property is `undefined` at runtime — and reading `.raw` off it throws inside `init()`,
|
|
79
|
+
which the host silently swallows. Configure in `config.ts` and redeploy.
|
|
80
|
+
|
|
81
|
+
If you need rules that change without a redeploy, read them from a Dataverse **environment
|
|
82
|
+
variable** in `init()`. That requires `<uses-feature name="WebAPI" required="true" />` in the
|
|
83
|
+
manifest — without it `context.webAPI` exists but throws. (If you have the `claude-workspace`
|
|
84
|
+
checkout, `shared/pcf-gallery/controls/color-grid` is a complete worked example, including
|
|
85
|
+
throttled re-reads so an already-open grid picks up a change.)
|
|
86
|
+
|
|
26
87
|
## Deploy
|
|
27
88
|
|
|
28
|
-
|
|
89
|
+
A customizer is a **PCF control**, so it ships in a solution — not as a web resource.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# 1. package (from the workspace root)
|
|
93
|
+
msft/04-dynamics/scripts/deploy/package-control.sh \
|
|
94
|
+
--source ./{{projectName}} --version 1.0.0 --output ./dist
|
|
95
|
+
|
|
96
|
+
# 2. import
|
|
97
|
+
pac solution import --path ./dist/{{componentName}}_1.0.0.zip --publish-changes
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Then bind it to a table's grid by setting `GridCustomizerControlFullName` on that table's
|
|
101
|
+
PowerAppsOneGrid configuration (a `customcontroldefaultconfigs` record) to
|
|
102
|
+
`<prefix>_{{namespace}}.{{componentName}}` — `org-bootstrap.sh`'s `controls` + `bindings`
|
|
103
|
+
steps do this, or set **Customizer control** in the table's grid properties in the maker
|
|
104
|
+
portal.
|
|
105
|
+
|
|
106
|
+
★ **Bump `version=` in `ControlManifest.Input.xml` on every change.** The platform caches by
|
|
107
|
+
`(namespace.constructor, version)`; without a bump it keeps serving the previous build and
|
|
108
|
+
you will debug a fix that was never deployed.
|
|
109
|
+
|
|
110
|
+
## Platform versions
|
|
111
|
+
|
|
112
|
+
`React 16.14.0` + `Fluent 8.121.1`, both declared as `<platform-library>` and pinned exactly
|
|
113
|
+
in `package.json`. A virtual control returns a ReactElement that the **platform** renders, so
|
|
114
|
+
compiled-against must equal served. Measured live 2026-08-02: the model-driven host serves
|
|
115
|
+
React 17.0.2 under the `Reactv16` alias, defines no `Reactv18`, and serves Fluent 8.121.1
|
|
116
|
+
**and** 9.68.0. v8 is used here because the cells render inside OneGrid's own v8 React tree —
|
|
117
|
+
a v9 cell would need its own `FluentProvider` and would be themed independently of the grid
|
|
118
|
+
around it. The manifest carries the full measurement.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
CellRendererOverrides,
|
|
4
|
+
CellRendererProps,
|
|
5
|
+
ColumnDataType,
|
|
6
|
+
ColumnDefinition,
|
|
7
|
+
GetRendererParams,
|
|
8
|
+
} from '../../{{componentName}}/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A stand-in for Microsoft's PowerAppsOneGrid, present only so the customizer has something
|
|
12
|
+
* to customize during local development.
|
|
13
|
+
*
|
|
14
|
+
* ★ Its whole value is that it calls the overrides EXACTLY the way the real grid does:
|
|
15
|
+
* - looks the handler up by the column's `ColumnDataType`, never by its logical name;
|
|
16
|
+
* - passes `(CellRendererProps, GetRendererParams)`, with `colDefs` + `columnIndex` so a
|
|
17
|
+
* handler can narrow to one column;
|
|
18
|
+
* - treats a `null`/`undefined` return as "use the default renderer" rather than as an
|
|
19
|
+
* empty cell.
|
|
20
|
+
*
|
|
21
|
+
* If a renderer works here and not in the org, the difference is the platform (versions,
|
|
22
|
+
* bindings, publish state) — not the contract. It deliberately does NOT reimplement sorting,
|
|
23
|
+
* filtering, selection, paging or inline editing: the real grid owns all of those, and a
|
|
24
|
+
* fake one that drifted from them would be worse than none.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** One column of the fake grid: the platform's `ColumnDefinition` plus a display width. */
|
|
28
|
+
export interface HarnessColumn {
|
|
29
|
+
name: string;
|
|
30
|
+
displayName: string;
|
|
31
|
+
dataType: ColumnDataType;
|
|
32
|
+
width?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** One cell: the raw value the platform would pass, and the text it would have painted. */
|
|
36
|
+
export interface HarnessCell {
|
|
37
|
+
raw: unknown;
|
|
38
|
+
formatted: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One row, keyed by column logical name. */
|
|
42
|
+
export type HarnessRow = Record<string, HarnessCell>;
|
|
43
|
+
|
|
44
|
+
export interface MockGridProps {
|
|
45
|
+
columns: HarnessColumn[];
|
|
46
|
+
rows: HarnessRow[];
|
|
47
|
+
overrides: CellRendererOverrides;
|
|
48
|
+
/** Row height in px — the real grid gives cells a fixed height, so mirror that. */
|
|
49
|
+
rowHeight?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ROW_HEIGHT = 42;
|
|
53
|
+
const BORDER = '#edebe9';
|
|
54
|
+
|
|
55
|
+
const headerCell: React.CSSProperties = {
|
|
56
|
+
textAlign: 'left',
|
|
57
|
+
fontSize: 12,
|
|
58
|
+
fontWeight: 600,
|
|
59
|
+
color: '#323130',
|
|
60
|
+
padding: '0 8px',
|
|
61
|
+
height: 36,
|
|
62
|
+
borderBottom: `1px solid ${BORDER}`,
|
|
63
|
+
whiteSpace: 'nowrap',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const MockGrid: React.FC<MockGridProps> = ({
|
|
67
|
+
columns,
|
|
68
|
+
rows,
|
|
69
|
+
overrides,
|
|
70
|
+
rowHeight = ROW_HEIGHT,
|
|
71
|
+
}) => {
|
|
72
|
+
// Built once per render and shared by every cell, exactly as the grid does — a handler
|
|
73
|
+
// that inspects sibling columns sees the same array the platform would give it.
|
|
74
|
+
const colDefs: ColumnDefinition[] = columns.map((c, i) => ({
|
|
75
|
+
name: c.name,
|
|
76
|
+
displayName: c.displayName,
|
|
77
|
+
dataType: c.dataType,
|
|
78
|
+
isPrimary: i === 0,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
if (columns.length === 0) {
|
|
82
|
+
return (
|
|
83
|
+
<div style={{ color: '#605e5c', fontSize: 13, padding: 8 }}>
|
|
84
|
+
No columns — pick a data source and load.
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div style={{ border: `1px solid ${BORDER}`, background: '#fff', overflowX: 'auto' }}>
|
|
91
|
+
<table style={{ borderCollapse: 'collapse', width: '100%', tableLayout: 'fixed' }}>
|
|
92
|
+
<colgroup>
|
|
93
|
+
{columns.map((c) => (
|
|
94
|
+
<col key={c.name} style={{ width: c.width ? `${c.width}px` : 'auto' }} />
|
|
95
|
+
))}
|
|
96
|
+
</colgroup>
|
|
97
|
+
<thead>
|
|
98
|
+
<tr>
|
|
99
|
+
{columns.map((c) => (
|
|
100
|
+
<th key={c.name} style={headerCell} title={`${c.name} — ${c.dataType}`}>
|
|
101
|
+
{c.displayName}
|
|
102
|
+
<span style={{ color: '#a19f9d', fontWeight: 400 }}> · {c.dataType}</span>
|
|
103
|
+
</th>
|
|
104
|
+
))}
|
|
105
|
+
</tr>
|
|
106
|
+
</thead>
|
|
107
|
+
<tbody>
|
|
108
|
+
{rows.map((row, rowIndex) => (
|
|
109
|
+
<tr key={rowIndex}>
|
|
110
|
+
{columns.map((col, columnIndex) => {
|
|
111
|
+
const cell = row[col.name] ?? { raw: undefined, formatted: '' };
|
|
112
|
+
|
|
113
|
+
const props: CellRendererProps = {
|
|
114
|
+
value: cell.raw,
|
|
115
|
+
formattedValue: cell.formatted,
|
|
116
|
+
columnDataType: col.dataType,
|
|
117
|
+
rowHeight,
|
|
118
|
+
columnEditable: false,
|
|
119
|
+
isRightAligned:
|
|
120
|
+
col.dataType === 'Currency' ||
|
|
121
|
+
col.dataType === 'Decimal' ||
|
|
122
|
+
col.dataType === 'Integer' ||
|
|
123
|
+
col.dataType === 'FloatingPoint',
|
|
124
|
+
// Part of the contract, unused by a read-only harness. A renderer that
|
|
125
|
+
// calls it in the org would still compile against the same signature.
|
|
126
|
+
onValueChange: () => undefined,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const params: GetRendererParams = {
|
|
130
|
+
colDefs,
|
|
131
|
+
columnIndex,
|
|
132
|
+
rowData: { __rec_id: String(rowIndex) },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handler = overrides[col.dataType];
|
|
136
|
+
const custom = handler ? handler(props, params) : undefined;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<td
|
|
140
|
+
key={col.name}
|
|
141
|
+
style={{
|
|
142
|
+
height: rowHeight,
|
|
143
|
+
padding: '0 8px',
|
|
144
|
+
borderBottom: `1px solid ${BORDER}`,
|
|
145
|
+
fontSize: 13,
|
|
146
|
+
overflow: 'hidden',
|
|
147
|
+
textOverflow: 'ellipsis',
|
|
148
|
+
whiteSpace: 'nowrap',
|
|
149
|
+
// The real grid right-aligns numerics itself; a custom renderer that
|
|
150
|
+
// returns undefined must therefore still land in the default layout.
|
|
151
|
+
textAlign: props.isRightAligned && !custom ? 'right' : 'left',
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
{/* ★ null/undefined => the grid's own rendering, not an empty cell. */}
|
|
155
|
+
{custom ?? cell.formatted}
|
|
156
|
+
</td>
|
|
157
|
+
);
|
|
158
|
+
})}
|
|
159
|
+
</tr>
|
|
160
|
+
))}
|
|
161
|
+
</tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
@@ -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>
|