@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.
Files changed (93) hide show
  1. package/README.md +2 -2
  2. package/dist/artifacts/registry.d.ts.map +1 -1
  3. package/dist/artifacts/registry.js +76 -20
  4. package/dist/artifacts/registry.js.map +1 -1
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/templatePlan.d.ts.map +1 -1
  8. package/dist/templatePlan.js +20 -3
  9. package/dist/templatePlan.js.map +1 -1
  10. package/dist/utils/copyTemplate.d.ts.map +1 -1
  11. package/dist/utils/copyTemplate.js +4 -0
  12. package/dist/utils/copyTemplate.js.map +1 -1
  13. package/package.json +1 -1
  14. package/templates/_shared/dataverse-seam/XrmApiService.ts +26 -5
  15. package/templates/grid-customizer/README.md +105 -15
  16. package/templates/grid-customizer/dev/harness/MockGrid.tsx +165 -0
  17. package/templates/grid-customizer/dev/harness/dataverse.ts +168 -0
  18. package/templates/grid-customizer/dev/index.html +20 -0
  19. package/templates/grid-customizer/dev/main.tsx +260 -0
  20. package/templates/grid-customizer/dev/vite-env.d.ts +1 -0
  21. package/templates/grid-customizer/env.local.example +24 -0
  22. package/templates/grid-customizer/eslintrc.json +11 -0
  23. package/templates/grid-customizer/featureconfig.json +3 -0
  24. package/templates/grid-customizer/gitignore +3 -0
  25. package/templates/grid-customizer/package.json +30 -16
  26. package/templates/grid-customizer/pcfconfig.json +3 -0
  27. package/templates/grid-customizer/tsconfig.dev.json +16 -0
  28. package/templates/grid-customizer/tsconfig.json +17 -10
  29. package/templates/grid-customizer/vite.config.ts +64 -0
  30. package/templates/grid-customizer/{{componentName}}/ControlManifest.Input.xml +63 -0
  31. package/templates/grid-customizer/{{componentName}}/customizers/CellEditorOverrides.tsx +38 -0
  32. package/templates/grid-customizer/{{componentName}}/customizers/CellRendererOverrides.tsx +109 -0
  33. package/templates/grid-customizer/{{componentName}}/customizers/cells.tsx +137 -0
  34. package/templates/grid-customizer/{{componentName}}/customizers/config.ts +84 -0
  35. package/templates/grid-customizer/{{componentName}}/index.ts +81 -0
  36. package/templates/grid-customizer/{{componentName}}/strings/{{componentName}}.1033.resx +56 -0
  37. package/templates/grid-customizer/{{componentName}}/types.ts +168 -0
  38. package/templates/host-swap/README.md +57 -2
  39. package/templates/host-swap/_variants/host.custom-page/src/App.tsx +9 -5
  40. package/templates/host-swap/_variants/host.pcf/ControlManifest.Input.xml +26 -2
  41. package/templates/host-swap/_variants/host.pcf/gitignore +7 -0
  42. package/templates/host-swap/_variants/host.pcf/index.ts +21 -3
  43. package/templates/host-swap/_variants/host.pcf/package.json +20 -4
  44. package/templates/host-swap/_variants/host.pcf/{{componentName}}.pcfproj +67 -0
  45. package/templates/host-swap/_variants/host.web-resource/src/App.tsx +5 -1
  46. package/templates/host-swap/src/Component.tsx +54 -7
  47. package/templates/host-swap/src/useHostWidth.ts +53 -0
  48. package/templates/pcf-dataset/dev/harness/dataverse.ts +82 -0
  49. package/templates/pcf-dataset/dev/index.html +20 -0
  50. package/templates/pcf-dataset/dev/main.tsx +194 -0
  51. package/templates/pcf-dataset/dev/vite-env.d.ts +1 -0
  52. package/templates/pcf-dataset/env.local.example +24 -0
  53. package/templates/pcf-dataset/eslintrc.json +11 -0
  54. package/templates/pcf-dataset/gitignore +4 -0
  55. package/templates/pcf-dataset/package.json +18 -8
  56. package/templates/pcf-dataset/pcfconfig.json +3 -0
  57. package/templates/pcf-dataset/tsconfig.dev.json +15 -0
  58. package/templates/pcf-dataset/tsconfig.json +11 -2
  59. package/templates/pcf-dataset/vite.config.ts +63 -0
  60. package/templates/pcf-dataset/{{componentName}}/ControlManifest.Input.xml +52 -0
  61. package/templates/pcf-dataset/{{componentName}}/{{componentName}}App.tsx +64 -0
  62. package/templates/pcf-dataset/{{{componentName}}Component.tsx → {{componentName}}/{{componentName}}Component.tsx} +9 -46
  63. package/templates/pcf-field/dev/harness/dataverse.ts +71 -0
  64. package/templates/pcf-field/dev/index.html +20 -0
  65. package/templates/pcf-field/dev/main.tsx +212 -0
  66. package/templates/pcf-field/dev/vite-env.d.ts +1 -0
  67. package/templates/pcf-field/env.local.example +24 -0
  68. package/templates/pcf-field/eslintrc.json +11 -0
  69. package/templates/pcf-field/gitignore +4 -0
  70. package/templates/pcf-field/package.json +11 -2
  71. package/templates/pcf-field/pcfconfig.json +3 -0
  72. package/templates/pcf-field/tsconfig.dev.json +15 -0
  73. package/templates/pcf-field/tsconfig.json +20 -4
  74. package/templates/pcf-field/vite.config.ts +60 -0
  75. package/templates/pcf-field/{{componentName}}/ControlManifest.Input.xml +42 -0
  76. package/templates/pcf-field/{{{componentName}}Component.tsx → {{componentName}}/{{componentName}}Component.tsx} +6 -1
  77. package/templates/power-pages-island/README.md +12 -0
  78. package/templates/power-pages-island/src/App.tsx +13 -1
  79. package/templates/power-pages-island/src/Component.tsx +54 -7
  80. package/templates/power-pages-island/src/useHostWidth.ts +53 -0
  81. package/templates/react-custom-page/tools/deploy/deploy-webresource.cjs +8 -2
  82. package/templates/grid-customizer/src/GridCustomizer.ts +0 -28
  83. package/templates/grid-customizer/src/cell-renderers.tsx +0 -35
  84. package/templates/grid-customizer/src/index.ts +0 -4
  85. package/templates/grid-customizer/src/types/grid-types.ts +0 -30
  86. package/templates/grid-customizer/src/utils/color-utils.ts +0 -24
  87. package/templates/grid-customizer/webpack.config.js +0 -17
  88. package/templates/pcf-dataset/ControlManifest.Input.xml +0 -16
  89. package/templates/pcf-field/ControlManifest.Input.xml +0 -17
  90. /package/templates/pcf-dataset/{index.ts → {{componentName}}/index.ts} +0 -0
  91. /package/templates/pcf-dataset/{strings → {{componentName}}/strings}/{{componentName}}.1033.resx +0 -0
  92. /package/templates/pcf-field/{index.ts → {{componentName}}/index.ts} +0 -0
  93. /package/templates/pcf-field/{strings → {{componentName}}/strings}/{{componentName}}.1033.resx +0 -0
@@ -0,0 +1,109 @@
1
+ import * as React from 'react';
2
+ import type { CellRendererOverrides, CellRendererProps, GetRendererParams } from '../types';
3
+ import { BooleanCell, CurrencyCell, ProgressCell, StatusBadgeCell } from './cells';
4
+ import { PROGRESS_COLUMNS, toNumber } from './config';
5
+
6
+ /**
7
+ * The renderer map handed to OneGrid.
8
+ *
9
+ * ★ Three contract rules, each of which silently produces "my customizer does nothing" when
10
+ * broken:
11
+ *
12
+ * 1. **Keys are COLUMN DATA TYPES, never column logical names.** A `{ statuscode: … }`
13
+ * entry type-errors against `CellRendererOverrides` — and would simply never fire if it
14
+ * were cast away. Scope to a single column INSIDE the handler, as `Decimal` does below.
15
+ * 2. **Return `undefined` to fall through** to the grid's own renderer. That is the opt-out
16
+ * for a row or column this customizer has no opinion about — returning an empty element
17
+ * instead blanks the cell.
18
+ * 3. **Render `props.formattedValue`, not `props.value`.** `value` is raw: an option-set
19
+ * integer, an unrounded decimal, a UTC timestamp. `formattedValue` is what the platform
20
+ * itself would have painted — already localized, already currency-symbolled, already
21
+ * resolved to an option label.
22
+ */
23
+
24
+ /** Per-instance knobs. Everything else is compile-time config in `./config`. */
25
+ export interface RendererOptions {
26
+ /**
27
+ * Columns rendered as a progress bar. Defaults to `config.PROGRESS_COLUMNS`.
28
+ *
29
+ * Overridable so the dev harness can retarget the SAME override map at whatever columns
30
+ * you loaded, instead of shipping a second near-copy of the renderers that could drift
31
+ * from the one the org actually runs.
32
+ */
33
+ progressColumns?: readonly string[];
34
+ }
35
+
36
+ /** The column logical name for the cell currently being rendered. */
37
+ function columnName(params: GetRendererParams): string | undefined {
38
+ return params.colDefs[params.columnIndex]?.name;
39
+ }
40
+
41
+ /** Text the grid would have shown, falling back to the raw value when there is no format. */
42
+ function displayText(props: CellRendererProps): string {
43
+ if (props.formattedValue !== undefined && props.formattedValue !== null) {
44
+ return props.formattedValue;
45
+ }
46
+ return props.value === undefined || props.value === null ? '' : String(props.value);
47
+ }
48
+
49
+ /**
50
+ * True when the cell holds no value.
51
+ *
52
+ * ★ Every handler must check this FIRST and fall through. Dataverse columns are nullable —
53
+ * a Boolean added to a table with existing rows is `null` on all of them — and a renderer
54
+ * that coerces null (`value === true` → false) paints a definite answer for missing data.
55
+ * "No" and "not set" are different facts; the grid's own renderer draws the second correctly.
56
+ */
57
+ function isEmpty(props: CellRendererProps): boolean {
58
+ return props.value === undefined || props.value === null;
59
+ }
60
+
61
+ /** Build the override map. `cellRendererOverrides` below is the configured singleton. */
62
+ export function createCellRendererOverrides(
63
+ options: RendererOptions = {},
64
+ ): CellRendererOverrides {
65
+ const progressColumns = options.progressColumns ?? PROGRESS_COLUMNS;
66
+
67
+ return {
68
+ // Option sets and status/state columns → a coloured pill keyed off the option LABEL.
69
+ // An unset option set falls through: a badge with no text is just a coloured smudge.
70
+ OptionSet: (props) => {
71
+ if (isEmpty(props)) return undefined;
72
+ const label = displayText(props);
73
+ return label ? <StatusBadgeCell label={label} /> : undefined;
74
+ },
75
+
76
+ // Yes/no → tick or cross plus the platform's own label ("Yes"/"No", or the custom pair).
77
+ // ★ Falls through on null — otherwise a nullable Boolean renders the same grey cross as a
78
+ // genuine "No", which silently misrepresents the data.
79
+ TwoOptions: (props) => {
80
+ if (isEmpty(props)) return undefined;
81
+ return (
82
+ <BooleanCell on={props.value === true || props.value === 1} label={displayText(props)} />
83
+ );
84
+ },
85
+
86
+ // Money → right-aligned, red when negative. Falls through when the cell is empty so a
87
+ // blank amount stays blank rather than rendering a coloured "0".
88
+ Currency: (props) => {
89
+ const amount = toNumber(props.value);
90
+ if (amount === undefined) return undefined;
91
+ return <CurrencyCell amount={amount} formatted={displayText(props)} />;
92
+ },
93
+
94
+ // ★ The per-column pattern. Decimal is registered for the whole grid, but only the
95
+ // columns in `progressColumns` get a progress bar — everything else falls through to the
96
+ // default renderer. Without this narrowing, every decimal column in every grid this
97
+ // customizer is bound to would become a progress bar.
98
+ Decimal: (props, params) => {
99
+ const name = columnName(params);
100
+ if (!name || !progressColumns.includes(name)) return undefined;
101
+ const value = toNumber(props.value);
102
+ if (value === undefined) return undefined;
103
+ return <ProgressCell value={value} />;
104
+ },
105
+ };
106
+ }
107
+
108
+ /** What the control registers with the grid — see `index.ts`. */
109
+ export const cellRendererOverrides: CellRendererOverrides = createCellRendererOverrides();
@@ -0,0 +1,137 @@
1
+ import * as React from 'react';
2
+ // ★ Root import, not a deep one (`@fluentui/react/lib/Label`). pcf-scripts externalises the
3
+ // module specifier `@fluentui/react` to the platform global; a deep path is a DIFFERENT
4
+ // specifier and gets bundled instead — which reintroduces exactly the compiled-version ≠
5
+ // served-version skew the exact pin in package.json exists to prevent.
6
+ import { ProgressIndicator } from '@fluentui/react';
7
+ import {
8
+ CURRENCY_NEGATIVE,
9
+ CURRENCY_POSITIVE,
10
+ PROGRESS_MAX,
11
+ getStatusColor,
12
+ } from './config';
13
+
14
+ /**
15
+ * The visual half of the customizer: presentational cells with no knowledge of the grid
16
+ * contract. `CellRendererOverrides.tsx` decides WHICH cell renders WHERE; this file decides
17
+ * what each one looks like. Split that way so a renderer can be unit-tested and reused from
18
+ * the dev harness without a PCF context.
19
+ *
20
+ * All of these are Fluent v8 / inline styles, matching the host grid — see the manifest
21
+ * comment on why a customizer is v8 even though the platform also serves Fluent v9.
22
+ */
23
+
24
+ /** Cells sit inside a grid row of fixed height; fill it rather than adding vertical padding. */
25
+ const fill: React.CSSProperties = {
26
+ display: 'flex',
27
+ alignItems: 'center',
28
+ height: '100%',
29
+ width: '100%',
30
+ overflow: 'hidden',
31
+ };
32
+
33
+ /**
34
+ * An option-set / status value as a coloured pill.
35
+ *
36
+ * Assumes a non-empty label — the caller falls through to the grid's own renderer for an
37
+ * unset value rather than asking this component to draw "nothing", which would blank the
38
+ * cell instead of leaving it to the platform.
39
+ */
40
+ export const StatusBadgeCell: React.FC<{ label: string }> = ({ label }) => {
41
+ return (
42
+ <div style={fill}>
43
+ <span
44
+ title={label}
45
+ style={{
46
+ backgroundColor: getStatusColor(label),
47
+ color: '#ffffff',
48
+ borderRadius: 10,
49
+ padding: '2px 10px',
50
+ fontSize: 12,
51
+ fontWeight: 600,
52
+ lineHeight: '16px',
53
+ maxWidth: '100%',
54
+ overflow: 'hidden',
55
+ textOverflow: 'ellipsis',
56
+ whiteSpace: 'nowrap',
57
+ }}
58
+ >
59
+ {label}
60
+ </span>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ /**
66
+ * A currency amount, right-aligned and coloured by sign.
67
+ *
68
+ * Renders the platform's `formattedValue` — it already carries the record's currency symbol
69
+ * and the user's locale separators, which a client-side `Intl.NumberFormat` would guess at
70
+ * (and get wrong for any transaction currency other than the user's own).
71
+ */
72
+ export const CurrencyCell: React.FC<{ amount: number; formatted: string }> = ({
73
+ amount,
74
+ formatted,
75
+ }) => (
76
+ <div style={{ ...fill, justifyContent: 'flex-end' }}>
77
+ <span
78
+ style={{
79
+ color: amount < 0 ? CURRENCY_NEGATIVE : CURRENCY_POSITIVE,
80
+ fontWeight: 600,
81
+ fontVariantNumeric: 'tabular-nums',
82
+ }}
83
+ >
84
+ {formatted}
85
+ </span>
86
+ </div>
87
+ );
88
+
89
+ /** A 0–PROGRESS_MAX number as a labelled bar. */
90
+ export const ProgressCell: React.FC<{ value: number }> = ({ value }) => {
91
+ const clamped = Math.max(0, Math.min(PROGRESS_MAX, value));
92
+ return (
93
+ <div style={{ ...fill, gap: 8 }}>
94
+ <div style={{ flex: '1 1 auto', minWidth: 40 }}>
95
+ <ProgressIndicator
96
+ percentComplete={clamped / PROGRESS_MAX}
97
+ barHeight={6}
98
+ // The grid row supplies the label; a second one here would double the row height.
99
+ styles={{ root: { margin: 0 }, itemProgress: { padding: 0 } }}
100
+ />
101
+ </div>
102
+ <span style={{ flex: '0 0 auto', fontSize: 12, fontVariantNumeric: 'tabular-nums' }}>
103
+ {Math.round(clamped)}%
104
+ </span>
105
+ </div>
106
+ );
107
+ };
108
+
109
+ /**
110
+ * A yes/no value as a tick or cross, so the column scans at a glance.
111
+ *
112
+ * ★ Inline SVG, NOT Fluent's `<Icon iconName="CheckMark">`. Fluent v8 icons are FONT icons
113
+ * that must be registered by `initializeIcons()` before use; an unregistered name renders as
114
+ * nothing and only warns ("The icon 'checkmark' was used but not registered"). A customizer
115
+ * does not own the page, so whether registration has happened against the platform's shared
116
+ * Fluent instance by the time a cell paints is not ours to guarantee — and the failure is a
117
+ * silently blank cell, not an error. An SVG has no such dependency and looks the same in the
118
+ * dev harness and in the org.
119
+ */
120
+ export const BooleanCell: React.FC<{ on: boolean; label: string }> = ({ on, label }) => (
121
+ <div style={{ ...fill, gap: 6 }}>
122
+ <svg
123
+ width="14"
124
+ height="14"
125
+ viewBox="0 0 16 16"
126
+ aria-hidden="true"
127
+ focusable="false"
128
+ style={{ flex: '0 0 auto' }}
129
+ >
130
+ <path
131
+ d={on ? 'M6.2 12.4 2 8.2l1.3-1.3 2.9 2.9 6.5-6.5L14 4.6z' : 'M12.7 4.3 11.3 2.9 8 6.2 4.7 2.9 3.3 4.3 6.6 7.6 3.3 10.9l1.4 1.4L8 9l3.3 3.3 1.4-1.4-3.3-3.3z'}
132
+ fill={on ? '#107c10' : '#a19f9d'}
133
+ />
134
+ </svg>
135
+ <span>{label}</span>
136
+ </div>
137
+ );
@@ -0,0 +1,84 @@
1
+ /**
2
+ * ★ THE ONE FILE YOU EDIT to retarget this customizer at your own columns.
3
+ *
4
+ * It is plain data, deliberately: a grid customizer cannot be configured by a maker at
5
+ * bind time. The grid's "Customizer control" field references this control by NAME only,
6
+ * so any `usage="input"` manifest property arrives `undefined` at runtime. Changing
7
+ * behaviour means editing this file and redeploying with a bumped manifest version.
8
+ *
9
+ * If you need runtime-changeable rules instead, read them from a Dataverse environment
10
+ * variable in `index.ts` — see the README. (If you have the `claude-workspace` checkout,
11
+ * `shared/pcf-gallery/controls/color-grid` is a full worked implementation, including the
12
+ * `<uses-feature name="WebAPI">` declaration that read requires.)
13
+ */
14
+
15
+ /**
16
+ * Columns rendered as a progress bar instead of a plain number. Empty by default: without
17
+ * an explicit list, EVERY numeric column in every grid this customizer touches would turn
18
+ * into a progress bar, which is almost never what you want.
19
+ *
20
+ * Use column LOGICAL names, e.g. `['dvk_percentcomplete']`.
21
+ */
22
+ export const PROGRESS_COLUMNS: readonly string[] = [];
23
+
24
+ /** Values a progress column is scaled against. A value of `PROGRESS_MAX` fills the bar. */
25
+ export const PROGRESS_MAX = 100;
26
+
27
+ /**
28
+ * Colour for a status / option-set label, matched case-insensitively against the
29
+ * platform-formatted TEXT of the cell (not the numeric option value) — so the same map
30
+ * works across tables whose option values differ but whose labels agree.
31
+ *
32
+ * Anything not listed falls back to a stable colour derived from the label itself, so a
33
+ * new option is still visually distinct without a code change.
34
+ */
35
+ export const STATUS_COLORS: Readonly<Record<string, string>> = {
36
+ active: '#107c10',
37
+ inactive: '#605e5c',
38
+ draft: '#0078d4',
39
+ new: '#0078d4',
40
+ pending: '#c19c00',
41
+ inprogress: '#0078d4',
42
+ approved: '#107c10',
43
+ completed: '#107c10',
44
+ done: '#107c10',
45
+ rejected: '#a4262c',
46
+ failed: '#a4262c',
47
+ cancelled: '#605e5c',
48
+ canceled: '#605e5c',
49
+ blocked: '#a4262c',
50
+ };
51
+
52
+ /** Colour for a positive currency amount. */
53
+ export const CURRENCY_POSITIVE = '#107c10';
54
+ /** Colour for a negative currency amount — the reason Currency is overridden at all. */
55
+ export const CURRENCY_NEGATIVE = '#a4262c';
56
+
57
+ /**
58
+ * Deterministic fallback colour for an unmapped status label. Same input → same colour for
59
+ * the life of the label, so the grid does not reshuffle between renders.
60
+ */
61
+ export function stringToColor(str: string): string {
62
+ let hash = 0;
63
+ for (let i = 0; i < str.length; i++) {
64
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
65
+ hash |= 0; // keep it a 32-bit int so long labels don't drift into float imprecision
66
+ }
67
+ return `hsl(${Math.abs(hash) % 360}, 55%, 35%)`;
68
+ }
69
+
70
+ /** Resolve the badge colour for a formatted status label. */
71
+ export function getStatusColor(label: string): string {
72
+ const normalized = label.toLowerCase().replace(/[\s_-]+/g, '');
73
+ return STATUS_COLORS[normalized] ?? stringToColor(normalized || 'unknown');
74
+ }
75
+
76
+ /** Coerce a raw grid cell value to a number, or `undefined` when it isn't one. */
77
+ export function toNumber(value: unknown): number | undefined {
78
+ if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
79
+ if (typeof value === 'string' && value.trim() !== '') {
80
+ const n = Number(value);
81
+ return Number.isFinite(n) ? n : undefined;
82
+ }
83
+ return undefined;
84
+ }
@@ -0,0 +1,81 @@
1
+ import type { IInputs, IOutputs } from './generated/ManifestTypes';
2
+ import * as React from 'react';
3
+ import { cellRendererOverrides } from './customizers/CellRendererOverrides';
4
+ import { cellEditorOverrides } from './customizers/CellEditorOverrides';
5
+ import type { PAOneGridCustomizer } from './types';
6
+
7
+ /**
8
+ * `context.factory` carries the grid's event channel but is absent from the typed
9
+ * `ComponentFramework.Context`, so it is declared structurally here rather than cast to
10
+ * `any` — the cast would also silence a genuine typo in the call.
11
+ */
12
+ interface CustomizerFactory {
13
+ fireEvent(eventName: string, payload: PAOneGridCustomizer): void;
14
+ }
15
+
16
+ export class {{componentName}} implements ComponentFramework.ReactControl<IInputs, IOutputs> {
17
+ // No constructor: the platform instantiates the control with no arguments and there is
18
+ // nothing to set up before init(). An explicit empty one would need an
19
+ // `@typescript-eslint/no-empty-function` disable, and that rule is not defined by the
20
+ // parser-only .eslintrc this template ships — `pcf-scripts build` fails on an unknown
21
+ // rule in a disable comment, not just on a rule violation.
22
+
23
+ /**
24
+ * Register the customizer with the host grid.
25
+ *
26
+ * This is the whole control. The grid passes an event name through the bound `EventName`
27
+ * property and then waits; firing that event with the override maps is what makes the
28
+ * grid paint custom cells. A customizer is initialised ONCE per grid, and the grid latches
29
+ * the FIRST payload it receives — later fires are ignored — so everything the customizer
30
+ * needs must be ready by the time this returns.
31
+ */
32
+ public init(
33
+ context: ComponentFramework.Context<IInputs>,
34
+ notifyOutputChanged: () => void,
35
+ state: ComponentFramework.Dictionary,
36
+ ): void {
37
+ // ★ The host SWALLOWS a throw from init: it tears the customizer down through a
38
+ // telemetry path that itself errors, which masks the original stack. The symptom is a
39
+ // grid that renders perfectly with default cells and no console error at all. Catch and
40
+ // log here or you will debug this blind.
41
+ try {
42
+ // ★ `?.` is load-bearing. A grid customizer receives ONLY the bound EventName; any
43
+ // other manifest property is undefined at runtime, and so is this one on a host that
44
+ // instantiates the control outside a grid. Reading `.raw` off undefined throws — see
45
+ // above for why that throw is invisible.
46
+ const eventName = context.parameters.EventName?.raw;
47
+ if (!eventName) return;
48
+
49
+ const customizer: PAOneGridCustomizer = {
50
+ cellRendererOverrides,
51
+ cellEditorOverrides,
52
+ };
53
+
54
+ (context as unknown as { factory: CustomizerFactory }).factory.fireEvent(
55
+ eventName,
56
+ customizer,
57
+ );
58
+ } catch (e) {
59
+ // eslint-disable-next-line no-console
60
+ console.error('[{{componentName}}] init failed', e);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * A customizer has no surface of its own — the grid it decorates owns the DOM. Returning
66
+ * an empty Fragment satisfies the virtual-control contract without adding a wrapper
67
+ * element to the grid's layout.
68
+ */
69
+ public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement {
70
+ return React.createElement(React.Fragment);
71
+ }
72
+
73
+ public getOutputs(): IOutputs {
74
+ return {};
75
+ }
76
+
77
+ public destroy(): void {
78
+ // Nothing to release: no timers, no listeners, no subscriptions. If you add a runtime
79
+ // config read (see README), cancel it here.
80
+ }
81
+ }
@@ -0,0 +1,56 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <root>
3
+ <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
4
+ <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
5
+ <xsd:element name="root" msdata:IsDataSet="true">
6
+ <xsd:complexType>
7
+ <xsd:choice maxOccurs="unbounded">
8
+ <xsd:element name="data">
9
+ <xsd:complexType>
10
+ <xsd:sequence>
11
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
12
+ <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
13
+ </xsd:sequence>
14
+ <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
15
+ <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
16
+ <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
17
+ <xsd:attribute ref="xml:space" />
18
+ </xsd:complexType>
19
+ </xsd:element>
20
+ <xsd:element name="resheader">
21
+ <xsd:complexType>
22
+ <xsd:sequence>
23
+ <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
24
+ </xsd:sequence>
25
+ <xsd:attribute name="name" type="xsd:string" use="required" />
26
+ </xsd:complexType>
27
+ </xsd:element>
28
+ </xsd:choice>
29
+ </xsd:complexType>
30
+ </xsd:element>
31
+ </xsd:schema>
32
+ <resheader name="resmimetype">
33
+ <value>text/microsoft-resx</value>
34
+ </resheader>
35
+ <resheader name="version">
36
+ <value>2.0</value>
37
+ </resheader>
38
+ <resheader name="reader">
39
+ <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
40
+ </resheader>
41
+ <resheader name="writer">
42
+ <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
43
+ </resheader>
44
+ <data name="{{namespace}}_{{componentName}}" xml:space="preserve">
45
+ <value>{{componentName}}</value>
46
+ </data>
47
+ <data name="{{namespace}}_{{componentName}}_Desc" xml:space="preserve">
48
+ <value>Editable-grid cell renderer overrides for Power Apps OneGrid.</value>
49
+ </data>
50
+ <data name="EventName" xml:space="preserve">
51
+ <value>Event Name</value>
52
+ </data>
53
+ <data name="EventName_Desc" xml:space="preserve">
54
+ <value>Bound by the host grid. The customizer is published back to the grid through this event.</value>
55
+ </data>
56
+ </root>
@@ -0,0 +1,168 @@
1
+ import type * as React from 'react';
2
+
3
+ /**
4
+ * The Power Apps OneGrid customizer contract.
5
+ *
6
+ * Microsoft publishes no npm package for these types, so every grid customizer vendors
7
+ * them. This copy matches Microsoft's documented shape. (If you have the `claude-workspace`
8
+ * checkout, `shared/pcf-gallery/controls/color-grid` vendors the same contract.)
9
+ *
10
+ * Reference:
11
+ * https://learn.microsoft.com/power-apps/developer/component-framework/customize-editable-grid-control
12
+ */
13
+
14
+ /** What `context.factory.fireEvent(eventName, …)` is handed. */
15
+ export interface PAOneGridCustomizer {
16
+ /** Full-surface customization. Prefer the two override maps below for cell-level work. */
17
+ gridCustomizer?: GridCustomizer;
18
+ /** Overrides for the grid's built-in cell RENDERERS (read mode). */
19
+ cellRendererOverrides?: CellRendererOverrides;
20
+ /** Overrides for the grid's built-in cell EDITORS (edit mode). */
21
+ cellEditorOverrides?: CellEditorOverrides;
22
+ }
23
+
24
+ /**
25
+ * ★ Keyed by COLUMN DATA TYPE, never by column logical name.
26
+ *
27
+ * A `{ statuscode: renderer }` entry silently never fires — `statuscode` is not a
28
+ * `ColumnDataType`. To scope a renderer to one column, register under its data type and
29
+ * narrow inside the handler with `params.colDefs[params.columnIndex].name`.
30
+ *
31
+ * Returning `undefined` (or `null`) falls through to the grid's own renderer for that cell,
32
+ * which is how a handler opts out per row/column.
33
+ */
34
+ export type CellRendererOverrides = {
35
+ [dataType in ColumnDataType]?: (
36
+ props: CellRendererProps,
37
+ rendererParams: GetRendererParams,
38
+ ) => React.ReactElement | null | undefined;
39
+ };
40
+
41
+ /** Same keying + fall-through rules as CellRendererOverrides. */
42
+ export type CellEditorOverrides = {
43
+ [dataType in ColumnDataType]?: (
44
+ defaultProps: CellEditorProps,
45
+ editorParams: GetEditorParams,
46
+ ) => React.ReactElement | null | undefined;
47
+ };
48
+
49
+ export interface GridCustomizer {
50
+ GetHeaderRenderer?(params: GetHeaderParams): React.ReactElement;
51
+ GetCellRenderer?(params: GetRendererParams): React.ReactElement;
52
+ GetLoadingRowRenderer?(): React.ReactElement;
53
+ GetCellEditor?(params: GetEditorParams): React.ReactElement;
54
+ GetNoRowsOverlayConfiguration?(): NoRowsOverlayConfig;
55
+ }
56
+
57
+ export interface CellRendererProps {
58
+ onValueChange(checked: boolean): void;
59
+ /** Raw cell value — a number for Currency/Decimal, an option value for OptionSet, … */
60
+ readonly value: unknown;
61
+ readonly isRTLMode?: boolean;
62
+ readonly onCellClicked?: (
63
+ event?: React.MouseEvent<HTMLElement, MouseEvent> | MouseEvent,
64
+ ) => void;
65
+ readonly validationError?: Error | null;
66
+ readonly isRightAligned?: boolean;
67
+ readonly startEditing?: (editorInitValue?: unknown) => void;
68
+ readonly isLastRow?: boolean;
69
+ readonly rowHeight?: number;
70
+ readonly columnDataType?: ColumnDataType;
71
+ /** Platform-formatted display text — currency symbols, option labels, localized dates. */
72
+ readonly formattedValue?: string;
73
+ readonly cellContainerElement?: HTMLElement;
74
+ readonly cellErrorLabelId?: string;
75
+ readonly columnEditable?: boolean;
76
+ }
77
+
78
+ export interface GetRendererParams {
79
+ /** Definitions for every visible column, in visual order. */
80
+ readonly colDefs: ColumnDefinition[];
81
+ /** Index into `colDefs` for the cell being rendered. */
82
+ readonly columnIndex: number;
83
+ readonly rowData?: RowData;
84
+ readonly allowTabKeyNavigation?: boolean;
85
+ }
86
+
87
+ export interface CellEditorProps {
88
+ readonly secured?: boolean;
89
+ readonly value: unknown;
90
+ readonly isRTLMode?: boolean;
91
+ readonly rowHeight?: number;
92
+ readonly isRequired?: boolean;
93
+ readonly charPress?: string | null;
94
+ readonly columnDataType?: ColumnDataType;
95
+ readonly onChange: (newValue: unknown) => void;
96
+ }
97
+
98
+ export interface GetEditorParams {
99
+ readonly colDefs: ColumnDefinition[];
100
+ readonly columnIndex: number;
101
+ readonly onCellValueChanged: (newValue: unknown) => void;
102
+ readonly rowData?: RowData;
103
+ readonly stopEditing: (cancel?: boolean) => void;
104
+ }
105
+
106
+ export interface GetHeaderParams {
107
+ readonly colDefs: ColumnDefinition[];
108
+ readonly columnIndex: number;
109
+ readonly isFirstVisualColumn?: boolean;
110
+ readonly isLastVisualColumn?: boolean;
111
+ readonly rowData?: RowData;
112
+ readonly isRTLMode?: boolean;
113
+ readonly allowTabKeyNavigation?: boolean;
114
+ }
115
+
116
+ export interface NoRowsOverlayConfig {
117
+ component: React.ComponentClass | undefined;
118
+ props: unknown | undefined;
119
+ }
120
+
121
+ export const RECID = '__rec_id';
122
+
123
+ export interface RowData {
124
+ readonly [RECID]: string;
125
+ }
126
+
127
+ export interface ColumnDefinition {
128
+ /** Column LOGICAL name — what you match on to scope a renderer to one column. */
129
+ readonly name: string;
130
+ readonly displayName?: string;
131
+ readonly colId?: string;
132
+ readonly dataType: string;
133
+ readonly CustomRendererType?: string;
134
+ readonly width?: number;
135
+ readonly minWidth?: number;
136
+ readonly maxWidth?: number;
137
+ readonly hide?: boolean;
138
+ readonly isPrimary: boolean;
139
+ }
140
+
141
+ /** The complete set of override keys the grid recognises. */
142
+ export type ColumnDataType =
143
+ | 'Text'
144
+ | 'Email'
145
+ | 'Phone'
146
+ | 'Ticker'
147
+ | 'URL'
148
+ | 'TextArea'
149
+ | 'Lookup'
150
+ | 'Customer'
151
+ | 'Owner'
152
+ | 'MultiSelectPicklist'
153
+ | 'OptionSet'
154
+ | 'TwoOptions'
155
+ | 'Duration'
156
+ | 'Language'
157
+ | 'Multiple'
158
+ | 'TimeZone'
159
+ | 'Integer'
160
+ | 'Currency'
161
+ | 'Decimal'
162
+ | 'FloatingPoint'
163
+ | 'AutoNumber'
164
+ | 'DateOnly'
165
+ | 'DateAndTime'
166
+ | 'Image'
167
+ | 'File'
168
+ | 'Persona';