@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
|
@@ -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';
|