@dataverse-kit/export-engine 1.6.0 → 1.6.2
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/dist/index.cjs +55 -179
- package/dist/index.mjs +55 -179
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -11767,7 +11767,9 @@ function DetailPaneChildren<T extends Record<string, unknown>>(props: {
|
|
|
11767
11767
|
return () => {
|
|
11768
11768
|
alive = false;
|
|
11769
11769
|
};
|
|
11770
|
-
|
|
11770
|
+
// \`nested.reloadToken\` makes the refetch explicit on a child mutation (e.g. addExisting)
|
|
11771
|
+
// rather than relying on \`nested\`'s inline-literal identity changing each render.
|
|
11772
|
+
}, [parent, nested, nested.reloadToken]);
|
|
11771
11773
|
|
|
11772
11774
|
if (rows === null) return <Text variant="small">Loading\u2026</Text>;
|
|
11773
11775
|
return (
|
|
@@ -12259,158 +12261,12 @@ export function ReadOnlyGrid<T extends Record<string, unknown>>(props: ReadOnlyG
|
|
|
12259
12261
|
"src/lib/grid-kit/grid-kit/hosts/cardLayout.tsx": "import { mergeStyles } from '@fluentui/react';\nimport type { CardConfig, ColumnDef } from '../types';\n\n// Shared card-layout primitives for the card hosts (`<CardGrid>` flat cards +\n// `<NestedCardParent>` expandable parent cards). The derivation is a PURE function\n// (no hooks) so it's unit-testable without a DOM; the mergeStyles classes are shared\n// so the two hosts can't drift apart visually.\n\nexport const cardClass = mergeStyles({\n border: '1px solid #edebe9',\n borderRadius: 4,\n padding: 12,\n background: '#fff',\n display: 'flex',\n flexDirection: 'column',\n gap: 6,\n boxShadow: '0 1px 2px rgba(0,0,0,0.06)',\n});\n\nexport const cardImageClass = mergeStyles({\n width: '100%',\n height: 96,\n objectFit: 'cover',\n borderRadius: 2,\n marginBottom: 4,\n});\n\nexport const bodyRowClass = mergeStyles({\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n gap: 8,\n fontSize: 13,\n});\n\n/** Resolved card layout: which fields drive title/subtitle/image + the body columns. */\nexport interface ResolvedCardLayout<T> {\n cardsPerRow: number;\n titleField?: string;\n subtitleField?: string;\n imageField?: string;\n cardHeight?: number;\n byField: Map<string, ColumnDef<T>>;\n bodyColumns: ColumnDef<T>[];\n}\n\n/**\n * PURE derivation of a card layout from columns + config \u2014 no React hooks, so it's\n * directly unit-testable. Hosts wrap the call in their own `useMemo`. titleField\n * defaults to the first column; bodyColumns defaults to all columns except the\n * title/subtitle/image fields (explicit `bodyFields` are honored verbatim).\n */\nexport function resolveCardLayout<T>(\n columns: ColumnDef<T>[],\n card: CardConfig | undefined\n): ResolvedCardLayout<T> {\n const cardsPerRow = card?.cardsPerRow ?? 3;\n const titleField = card?.titleField ?? columns[0]?.fieldName;\n const { subtitleField, imageField, cardHeight } = card ?? {};\n\n const byField = new Map<string, ColumnDef<T>>();\n columns.forEach((c) => byField.set(c.fieldName, c));\n\n const explicit = card?.bodyFields;\n const bodyColumns = explicit\n ? (explicit.map((f) => byField.get(f)).filter(Boolean) as ColumnDef<T>[])\n : columns.filter(\n (c) =>\n !new Set([titleField, subtitleField, imageField].filter(Boolean) as string[]).has(\n c.fieldName\n )\n );\n\n return { cardsPerRow, titleField, subtitleField, imageField, cardHeight, byField, bodyColumns };\n}\n",
|
|
12260
12262
|
"src/lib/grid-kit/grid-kit/hosts/fill.tsx": "import React from 'react';\nimport type { IStackProps } from '@fluentui/react';\n\n// Shared helpers for the additive, default-off `fill`/`height` grid props (v1b).\n// `fill` makes a host fill its container (flex column at `height`, default 100%) and\n// scroll its list/cards region internally \u2014 so a grid dropped into a surface-kit\n// Section / Dialog / Panel scrolls inside the surface while the toolbar + footers\n// (and the surface's own footer) stay fixed. All default-off \u2192 unchanged when unset.\n//\n// Consumer contract: the container MUST have a resolved height (a flex/grid parent or\n// an explicit height) \u2014 `height: '100%'` against an auto-height parent renders nothing.\n// v1 limitation: the DetailsList column header scrolls with the body (not pinned via\n// ScrollablePane/Sticky); the surface-integration goal (grid scrolls so the surface\n// footer stays put) is still met.\n\n/** Stack root styles for a host that should fill its container. `undefined` when not filling. */\nexport function fillStackStyles(fill?: boolean, height?: number | string): IStackProps['styles'] {\n return fill ? { root: { height: height ?? '100%', display: 'flex', flexDirection: 'column', minHeight: 0 } } : undefined;\n}\n\n/** Wraps the scrollable region (list/cards) so it grows + scrolls inside a `fill` host; passthrough otherwise. */\nexport const FillRegion: React.FC<{ fill?: boolean; children: React.ReactNode }> = ({ fill, children }) =>\n fill ? <div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>{children}</div> : <>{children}</>;\n",
|
|
12261
12263
|
"src/lib/grid-kit/grid-kit/hosts/index.ts": "export { DataGrid } from './DataGrid';\nexport { DetailsGrid } from './DetailsGrid';\nexport { ReadOnlyGrid } from './ReadOnlyGrid';\nexport { CardGrid } from './CardGrid';\nexport { GroupedGrid } from './GroupedGrid';\nexport { buildGroups } from './buildGroups';\nexport { NestedGrid } from './nested/NestedGrid';\nexport { FocusedViewGrid } from './FocusedViewGrid';\nexport { useChildren } from './nested/useChildren';\nexport { GridToolbar } from './toolbar/GridToolbar';\nexport { GridColumnChooser, orderVisibleColumns } from './GridColumnChooser';\nexport type { GridColumnChooserProps } from './GridColumnChooser';\nexport { GridFilterBuilder, getOperatorsForFieldType, fieldTypeForRenderer } from './GridFilterBuilder';\nexport type { GridFilterBuilderProps, FilterFieldType } from './GridFilterBuilder';\nexport { GridPaginationFooter } from './GridPaginationFooter';\nexport { useEditState } from './useEditState';\nexport type { EditState } from './useEditState';\nexport { useGridContext } from './useGridContext';\n",
|
|
12262
|
-
"src/lib/grid-kit/grid-kit/hosts/nested/NestedCardParent.tsx": "import React, { useCallback, useMemo, useState } from 'react';\nimport { Checkbox, IconButton, Spinner, Stack, Text } from '@fluentui/react';\nimport type { ColumnDef, NestedGridProps } from '../../types';\nimport { buildCellProps, GridRenderContext } from '../../adapters';\nimport { DataGrid } from '../DataGrid';\nimport { GridToolbar } from '../toolbar/GridToolbar';\nimport { GridAggregateFooter } from '../GridAggregateFooter';\nimport { GridPaginationFooter } from '../GridPaginationFooter';\nimport { fillStackStyles, FillRegion } from '../fill';\nimport { cardClass, cardImageClass, bodyRowClass, resolveCardLayout } from '../cardLayout';\nimport { useRowContextMenu } from '../useRowContextMenu';\nimport { useChildren } from './useChildren';\n\n/**\n * Card-parent nested grid (`parentLayout: 'cards'`): each parent row renders as a\n * card that expands IN PLACE to reveal its child grid below the card fields. Reuses\n * the shared card layout (`resolveCardLayout` + classes) for the card body and the\n * lazy `useChildren` hook for child resolution (grid-kit never fetches). v8-only.\n *\n * `nested.mode` does NOT apply to card parents \u2014 `<NestedGrid>` routes here on\n * `parentLayout === 'cards'` BEFORE the mode discriminator, so the side-panel /\n * hover-callout trigger machinery is bypassed.\n *\n * v1 limitation: `childSelectionGating: 'requires-parent'` is unsupported here \u2014 cards\n * use a manual checkbox `Set`, not a Fluent DetailsList `Selection` to gate against \u2014\n * so it's treated as `'independent'` with a one-time `console.warn`.\n */\nexport function NestedCardParent<\n T extends Record<string, unknown>,\n C extends Record<string, unknown>,\n>(props: {\n parentProps: NestedGridProps<T, C>;\n ctx: GridRenderContext<T>;\n getKeyFn: (it: T) => string;\n}): JSX.Element {\n const { parentProps, ctx, getKeyFn } = props;\n const {\n items,\n columns,\n nested,\n selectionMode = 'none',\n onSelectionChanged,\n toolbar,\n pagination,\n onPageChange,\n aggregateItems,\n fill,\n height,\n rowCommands,\n } = parentProps;\n\n // Per-row right-click menu (parity with DataGrid/NestedInline). For these raw-div\n // cards we invoke the hook fn from a native onContextMenu and suppress the browser\n // menu with e.preventDefault() ourselves (the hook's `return false` only matters to\n // Fluent's DetailsList onItemContextMenu contract).\n const { onItemContextMenu, contextMenuElement } = useRowContextMenu<T>(rowCommands);\n\n const { cardsPerRow, titleField, subtitleField, imageField, cardHeight, byField, bodyColumns } =\n useMemo(() => resolveCardLayout(columns, nested.parentCard), [columns, nested.parentCard]);\n\n // Parent footers (parity with NestedInline / CardGrid): aggregate row when any\n // parent column declares an `aggregate`, then the pager. No DetailsList leading\n // control columns on a card grid, so the aggregate footer has no leading spacer.\n const hasAggregate = columns.some((c) => c.aggregate);\n\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const { get, ensure } = useChildren<T, C>(nested.getChildren);\n\n // requires-parent gating needs a Fluent Selection over the parent rows; cards have\n // none, so surface the unsupported config and fall back to independent selection.\n React.useEffect(() => {\n if (nested.childSelectionGating === 'requires-parent' && typeof console !== 'undefined') {\n console.warn(\n \"[grid-kit] childSelectionGating 'requires-parent' is unsupported for card parents (cards have no Fluent Selection); treating as 'independent'.\"\n );\n }\n }, [nested.childSelectionGating]);\n\n const renderField = (col: ColumnDef<T> | undefined, item: T): React.ReactNode => {\n if (!col) return null;\n const cellProps = buildCellProps(col, item, ctx);\n const Renderer = ctx.registry.resolve(col, false); // cards render read-only\n return <Renderer {...cellProps} />;\n };\n\n const rawValue = (item: T, field?: string): string => {\n if (!field) return '';\n const v = (item as Record<string, unknown>)[field];\n return v == null ? '' : String(v);\n };\n\n const toggleSelect = (item: T, checked?: boolean) => {\n const key = getKeyFn(item);\n setSelected((prev) => {\n const next = new Set(selectionMode === 'single' ? [] : prev);\n if (checked) next.add(key);\n else next.delete(key);\n onSelectionChanged?.(items.filter((it) => next.has(getKeyFn(it))));\n return next;\n });\n };\n\n const toggleExpand = useCallback(\n (key: string, item: T) => {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else {\n next.add(key);\n ensure(key, item);\n }\n return next;\n });\n },\n [ensure]\n );\n\n return (\n <Stack styles={fillStackStyles(fill, height)}>\n {toolbar && <GridToolbar config={toolbar} />}\n <FillRegion fill={fill}>\n <div\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${cardsPerRow}, minmax(0, 1fr))`,\n gap: 12,\n paddingTop: 4,\n }}\n >\n {items.map((item) => {\n const key = getKeyFn(item);\n const open = expanded.has(key);\n const entry = open ? get(key) : undefined;\n return (\n <div\n key={key}\n className={cardClass}\n style={cardHeight ? { height: cardHeight } : undefined}\n onClick={() => toggleExpand(key, item)}\n // Right-click anywhere on the card opens its row menu. The card is the\n // row-equivalent target, so the title/body cells, chevron, and checkbox\n // below keep only their onClick stopPropagation \u2014 a right-click on them\n // intentionally bubbles here (preventDefault suppresses the browser/anchor\n // menu). Do NOT add onContextMenu guards there or the menu dies on cells.\n // The expanded child region DOES stop it (see below).\n onContextMenu={\n onItemContextMenu\n ? (e) => {\n e.preventDefault();\n onItemContextMenu(item, undefined, e.nativeEvent);\n }\n : undefined\n }\n >\n <Stack horizontal horizontalAlign=\"space-between\" verticalAlign=\"start\">\n <Stack horizontal verticalAlign=\"center\" tokens={{ childrenGap: 4 }}>\n <IconButton\n iconProps={{ iconName: open ? 'ChevronDown' : 'ChevronRight' }}\n ariaLabel={open ? 'Collapse' : 'Expand'}\n styles={{ root: { height: 24, width: 24 } }}\n onClick={(e) => {\n e.stopPropagation();\n toggleExpand(key, item);\n }}\n />\n <Text variant=\"mediumPlus\" styles={{ root: { fontWeight: 600 } }}>\n {/* An interactive title (e.g. a link/lookup primary column) shouldn't\n ALSO toggle the card \u2014 guard it like the body cells. */}\n <span onClick={(e) => e.stopPropagation()}>\n {renderField(byField.get(titleField ?? ''), item) ?? rawValue(item, titleField)}\n </span>\n </Text>\n </Stack>\n {selectionMode !== 'none' && (\n // Stop the click from bubbling to the card's expand toggle.\n <span onClick={(e) => e.stopPropagation()}>\n <Checkbox\n checked={selected.has(key)}\n onChange={(_, checked) => toggleSelect(item, checked)}\n ariaLabel=\"Select card\"\n />\n </span>\n )}\n </Stack>\n {imageField && rawValue(item, imageField) && (\n <img className={cardImageClass} src={rawValue(item, imageField)} alt=\"\" />\n )}\n {subtitleField && (\n <Text variant=\"small\" styles={{ root: { color: '#605e5c' } }}>\n {rawValue(item, subtitleField)}\n </Text>\n )}\n {bodyColumns.map((col) => (\n <div key={col.key} className={bodyRowClass}>\n <Text variant=\"small\" styles={{ root: { color: '#605e5c' } }}>\n {col.name}\n </Text>\n {/* Cell value clicks shouldn't toggle the card. */}\n <span onClick={(e) => e.stopPropagation()}>{renderField(col, item)}</span>\n </div>\n ))}\n {open && (\n <div\n style={{ paddingTop: 8, marginTop: 4, borderTop: '1px solid #edebe9' }}\n onClick={(e) => e.stopPropagation()}\n // The child <DataGrid> renders INSIDE this card's DOM, so a right-click\n // on a child row would otherwise bubble to the card's onContextMenu and\n // fire the PARENT card's menu for the wrong record (the #93 / 2a8ff381\n // lesson; mirrors NestedInline's inline child-region guard). Children\n // have no own menu in v1 \u2192 the native browser menu is left intact here.\n onContextMenu={(e) => e.stopPropagation()}\n >\n {!entry || entry.loading ? (\n <Spinner label=\"Loading related records\u2026\" />\n ) : (\n <DataGrid<C>\n items={entry.rows}\n columns={nested.childColumns}\n registry={nested.childRegistry}\n compact\n selectionMode={nested.childSelectionMode}\n onSelectionChanged={\n nested.onChildSelectionChanged\n ? (sel) => nested.onChildSelectionChanged!(item, sel)\n : undefined\n }\n editable={nested.childEditable}\n editTrigger={nested.childEditTrigger}\n onValueChange={\n nested.onChildValueChange\n ? (rk, fn, v, ov) => nested.onChildValueChange!(item, rk, fn, v, ov)\n : undefined\n }\n />\n )}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </FillRegion>\n {hasAggregate && (\n <GridAggregateFooter items={aggregateItems ?? items} columns={columns} leadingSpacer={0} />\n )}\n {pagination && <GridPaginationFooter pagination={pagination} onPageChange={onPageChange} />}\n {contextMenuElement}\n </Stack>\n );\n}\n",
|
|
12264
|
+
"src/lib/grid-kit/grid-kit/hosts/nested/NestedCardParent.tsx": "import React, { useCallback, useMemo, useState } from 'react';\nimport { Checkbox, IconButton, Spinner, Stack, Text } from '@fluentui/react';\nimport type { ColumnDef, NestedGridProps } from '../../types';\nimport { buildCellProps, GridRenderContext } from '../../adapters';\nimport { DataGrid } from '../DataGrid';\nimport { GridToolbar } from '../toolbar/GridToolbar';\nimport { GridAggregateFooter } from '../GridAggregateFooter';\nimport { GridPaginationFooter } from '../GridPaginationFooter';\nimport { fillStackStyles, FillRegion } from '../fill';\nimport { cardClass, cardImageClass, bodyRowClass, resolveCardLayout } from '../cardLayout';\nimport { useRowContextMenu } from '../useRowContextMenu';\nimport { useChildren } from './useChildren';\n\n/**\n * Card-parent nested grid (`parentLayout: 'cards'`): each parent row renders as a\n * card that expands IN PLACE to reveal its child grid below the card fields. Reuses\n * the shared card layout (`resolveCardLayout` + classes) for the card body and the\n * lazy `useChildren` hook for child resolution (grid-kit never fetches). v8-only.\n *\n * `nested.mode` does NOT apply to card parents \u2014 `<NestedGrid>` routes here on\n * `parentLayout === 'cards'` BEFORE the mode discriminator, so the side-panel /\n * hover-callout trigger machinery is bypassed.\n *\n * v1 limitation: `childSelectionGating: 'requires-parent'` is unsupported here \u2014 cards\n * use a manual checkbox `Set`, not a Fluent DetailsList `Selection` to gate against \u2014\n * so it's treated as `'independent'` with a one-time `console.warn`.\n */\nexport function NestedCardParent<\n T extends Record<string, unknown>,\n C extends Record<string, unknown>,\n>(props: {\n parentProps: NestedGridProps<T, C>;\n ctx: GridRenderContext<T>;\n getKeyFn: (it: T) => string;\n}): JSX.Element {\n const { parentProps, ctx, getKeyFn } = props;\n const {\n items,\n columns,\n nested,\n selectionMode = 'none',\n onSelectionChanged,\n toolbar,\n pagination,\n onPageChange,\n aggregateItems,\n fill,\n height,\n rowCommands,\n } = parentProps;\n\n // Per-row right-click menu (parity with DataGrid/NestedInline). For these raw-div\n // cards we invoke the hook fn from a native onContextMenu and suppress the browser\n // menu with e.preventDefault() ourselves (the hook's `return false` only matters to\n // Fluent's DetailsList onItemContextMenu contract).\n const { onItemContextMenu, contextMenuElement } = useRowContextMenu<T>(rowCommands);\n\n const { cardsPerRow, titleField, subtitleField, imageField, cardHeight, byField, bodyColumns } =\n useMemo(() => resolveCardLayout(columns, nested.parentCard), [columns, nested.parentCard]);\n\n // Parent footers (parity with NestedInline / CardGrid): aggregate row when any\n // parent column declares an `aggregate`, then the pager. No DetailsList leading\n // control columns on a card grid, so the aggregate footer has no leading spacer.\n const hasAggregate = columns.some((c) => c.aggregate);\n\n const [selected, setSelected] = useState<Set<string>>(new Set());\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const { get, ensure } = useChildren<T, C>(nested.getChildren, nested.reloadToken);\n\n // requires-parent gating needs a Fluent Selection over the parent rows; cards have\n // none, so surface the unsupported config and fall back to independent selection.\n React.useEffect(() => {\n if (nested.childSelectionGating === 'requires-parent' && typeof console !== 'undefined') {\n console.warn(\n \"[grid-kit] childSelectionGating 'requires-parent' is unsupported for card parents (cards have no Fluent Selection); treating as 'independent'.\"\n );\n }\n }, [nested.childSelectionGating]);\n\n const renderField = (col: ColumnDef<T> | undefined, item: T): React.ReactNode => {\n if (!col) return null;\n const cellProps = buildCellProps(col, item, ctx);\n const Renderer = ctx.registry.resolve(col, false); // cards render read-only\n return <Renderer {...cellProps} />;\n };\n\n const rawValue = (item: T, field?: string): string => {\n if (!field) return '';\n const v = (item as Record<string, unknown>)[field];\n return v == null ? '' : String(v);\n };\n\n const toggleSelect = (item: T, checked?: boolean) => {\n const key = getKeyFn(item);\n setSelected((prev) => {\n const next = new Set(selectionMode === 'single' ? [] : prev);\n if (checked) next.add(key);\n else next.delete(key);\n onSelectionChanged?.(items.filter((it) => next.has(getKeyFn(it))));\n return next;\n });\n };\n\n const toggleExpand = useCallback(\n (key: string, item: T) => {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else {\n next.add(key);\n ensure(key, item);\n }\n return next;\n });\n },\n [ensure]\n );\n\n return (\n <Stack styles={fillStackStyles(fill, height)}>\n {toolbar && <GridToolbar config={toolbar} />}\n <FillRegion fill={fill}>\n <div\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${cardsPerRow}, minmax(0, 1fr))`,\n gap: 12,\n paddingTop: 4,\n }}\n >\n {items.map((item) => {\n const key = getKeyFn(item);\n const open = expanded.has(key);\n const entry = open ? get(key) : undefined;\n return (\n <div\n key={key}\n className={cardClass}\n style={cardHeight ? { height: cardHeight } : undefined}\n onClick={() => toggleExpand(key, item)}\n // Right-click anywhere on the card opens its row menu. The card is the\n // row-equivalent target, so the title/body cells, chevron, and checkbox\n // below keep only their onClick stopPropagation \u2014 a right-click on them\n // intentionally bubbles here (preventDefault suppresses the browser/anchor\n // menu). Do NOT add onContextMenu guards there or the menu dies on cells.\n // The expanded child region DOES stop it (see below).\n onContextMenu={\n onItemContextMenu\n ? (e) => {\n e.preventDefault();\n onItemContextMenu(item, undefined, e.nativeEvent);\n }\n : undefined\n }\n >\n <Stack horizontal horizontalAlign=\"space-between\" verticalAlign=\"start\">\n <Stack horizontal verticalAlign=\"center\" tokens={{ childrenGap: 4 }}>\n <IconButton\n iconProps={{ iconName: open ? 'ChevronDown' : 'ChevronRight' }}\n ariaLabel={open ? 'Collapse' : 'Expand'}\n styles={{ root: { height: 24, width: 24 } }}\n onClick={(e) => {\n e.stopPropagation();\n toggleExpand(key, item);\n }}\n />\n <Text variant=\"mediumPlus\" styles={{ root: { fontWeight: 600 } }}>\n {/* An interactive title (e.g. a link/lookup primary column) shouldn't\n ALSO toggle the card \u2014 guard it like the body cells. */}\n <span onClick={(e) => e.stopPropagation()}>\n {renderField(byField.get(titleField ?? ''), item) ?? rawValue(item, titleField)}\n </span>\n </Text>\n </Stack>\n {selectionMode !== 'none' && (\n // Stop the click from bubbling to the card's expand toggle.\n <span onClick={(e) => e.stopPropagation()}>\n <Checkbox\n checked={selected.has(key)}\n onChange={(_, checked) => toggleSelect(item, checked)}\n ariaLabel=\"Select card\"\n />\n </span>\n )}\n </Stack>\n {imageField && rawValue(item, imageField) && (\n <img className={cardImageClass} src={rawValue(item, imageField)} alt=\"\" />\n )}\n {subtitleField && (\n <Text variant=\"small\" styles={{ root: { color: '#605e5c' } }}>\n {rawValue(item, subtitleField)}\n </Text>\n )}\n {bodyColumns.map((col) => (\n <div key={col.key} className={bodyRowClass}>\n <Text variant=\"small\" styles={{ root: { color: '#605e5c' } }}>\n {col.name}\n </Text>\n {/* Cell value clicks shouldn't toggle the card. */}\n <span onClick={(e) => e.stopPropagation()}>{renderField(col, item)}</span>\n </div>\n ))}\n {open && (\n <div\n style={{ paddingTop: 8, marginTop: 4, borderTop: '1px solid #edebe9' }}\n onClick={(e) => e.stopPropagation()}\n // The child <DataGrid> renders INSIDE this card's DOM, so a right-click\n // on a child row would otherwise bubble to the card's onContextMenu and\n // fire the PARENT card's menu for the wrong record (the #93 / 2a8ff381\n // lesson; mirrors NestedInline's inline child-region guard). Children\n // have no own menu in v1 \u2192 the native browser menu is left intact here.\n onContextMenu={(e) => e.stopPropagation()}\n >\n {!entry || entry.loading ? (\n <Spinner label=\"Loading related records\u2026\" />\n ) : (\n <DataGrid<C>\n items={entry.rows}\n columns={nested.childColumns}\n registry={nested.childRegistry}\n compact\n selectionMode={nested.childSelectionMode}\n onSelectionChanged={\n nested.onChildSelectionChanged\n ? (sel) => nested.onChildSelectionChanged!(item, sel)\n : undefined\n }\n editable={nested.childEditable}\n editTrigger={nested.childEditTrigger}\n onValueChange={\n nested.onChildValueChange\n ? (rk, fn, v, ov) => nested.onChildValueChange!(item, rk, fn, v, ov)\n : undefined\n }\n />\n )}\n </div>\n )}\n </div>\n );\n })}\n </div>\n </FillRegion>\n {hasAggregate && (\n <GridAggregateFooter items={aggregateItems ?? items} columns={columns} leadingSpacer={0} />\n )}\n {pagination && <GridPaginationFooter pagination={pagination} onPageChange={onPageChange} />}\n {contextMenuElement}\n </Stack>\n );\n}\n",
|
|
12263
12265
|
"src/lib/grid-kit/grid-kit/hosts/nested/NestedGrid.tsx": "import React, { useMemo } from 'react';\nimport type { ColumnDef, NestedGridProps } from '../../types';\nimport { useEditState } from '../useEditState';\nimport { useGridContext } from '../useGridContext';\nimport { DataGrid } from '../DataGrid';\nimport { NestedInline } from './NestedInline';\nimport { NestedCardParent } from './NestedCardParent';\nimport { NestedTriggerCell } from './NestedTriggerCell';\n\n/**\n * Nested / expandable grid host. Dispatch order:\n * 1. `parentLayout === 'cards'` \u2014 each parent is a card that expands in place\n * (`<NestedCardParent>`); `mode` is ignored.\n * Otherwise, parents are rows and child records surface per `nested.mode`:\n * - `inline` \u2014 chevron expands the child grid under the row\n * - `side-panel` \u2014 a trigger opens a Fluent Panel with the child grid\n * - `hover-callout` \u2014 a count badge shows a compact callout of top-N rows\n * - `detail-pane` \u2014 falls back to `side-panel` here (use `<FocusedViewGrid>`\n * for true master-detail with a detail-pane child)\n *\n * grid-kit never fetches \u2014 `nested.getChildren(parent)` (sync or async) owns it.\n */\nexport function NestedGrid<\n T extends Record<string, unknown> = Record<string, unknown>,\n C extends Record<string, unknown> = Record<string, unknown>,\n>(props: NestedGridProps<T, C>): JSX.Element {\n const { columns, nested } = props;\n const edit = useEditState();\n const { getKeyFn, ctx } = useGridContext<T>(props, edit);\n\n // Append a trigger column for the non-inline modes (its cell manages its own\n // panel/callout). Built before any early return to keep hook order stable.\n const triggeredColumns = useMemo<ColumnDef<T>[]>(() => {\n const trigger: ColumnDef<T> = {\n key: '__nested',\n fieldName: '__nested',\n name: 'Related',\n rendererType: 'text',\n minWidth: 160,\n onRender: (item) => <NestedTriggerCell<T, C> parent={item} nested={nested} />,\n };\n return [...columns, trigger];\n }, [columns, nested]);\n\n // Card parents win over `mode`: each parent renders as a card that expands in place.\n if (nested.parentLayout === 'cards') {\n return <NestedCardParent<T, C> parentProps={props} ctx={ctx} getKeyFn={getKeyFn} />;\n }\n\n if (nested.mode === 'inline') {\n return <NestedInline<T, C> parentProps={props} ctx={ctx} getKeyFn={getKeyFn} />;\n }\n\n // side-panel, hover-callout (and detail-pane fallback) \u2192 DataGrid + trigger column\n return <DataGrid<T> {...props} columns={triggeredColumns} />;\n}\n",
|
|
12264
|
-
"src/lib/grid-kit/grid-kit/hosts/nested/NestedInline.tsx": "import React, { useCallback, useMemo, useRef, useState } from 'react';\nimport {\n DetailsListLayoutMode,\n type IColumn,\n type IDetailsRowProps,\n type IObjectWithKey,\n IconButton,\n Selection,\n SelectionMode,\n ShimmeredDetailsList,\n Spinner,\n} from '@fluentui/react';\nimport { getDetailsListStyles } from '../../../cell-renderers';\nimport type { NestedGridProps } from '../../types';\nimport { toDetailsListColumns, GridRenderContext } from '../../adapters';\nimport { DataGrid } from '../DataGrid';\nimport { useRowContextMenu } from '../useRowContextMenu';\nimport { GridAggregateFooter } from '../GridAggregateFooter';\nimport { GridPaginationFooter } from '../GridPaginationFooter';\nimport { useChildren } from './useChildren';\n\n// Leading control-column widths the parent footer must skip to line up its cells\n// with the data columns: the selection checkbox (Fluent default ~48px, present\n// only when the parent grid is selectable) and the always-present expand chevron.\nconst SELECTION_CHECKBOX_WIDTH = 48;\nconst EXPAND_CHEVRON_WIDTH = 32;\n\nfunction mapSelectionMode(mode?: string): SelectionMode {\n return mode === 'multiple' ? SelectionMode.multiple : mode === 'single' ? SelectionMode.single : SelectionMode.none;\n}\n\n/**\n * Inline nested grid: a chevron column expands each parent row to reveal its\n * child grid indented beneath (Fluent `onRenderRow`). Children resolve lazily on\n * first expand.\n *\n * Known limitation: an expanded row's height varies with its child grid, which\n * defeats Fluent v8 DetailsList row-height virtualization \u2014 fine for typical\n * subgrid sizes; for very large parent lists prefer side-panel / hover-callout.\n */\nexport function NestedInline<T extends Record<string, unknown>, C extends Record<string, unknown>>(props: {\n parentProps: NestedGridProps<T, C>;\n ctx: GridRenderContext<T>;\n getKeyFn: (it: T) => string;\n}): JSX.Element {\n const { parentProps, ctx, getKeyFn } = props;\n const {\n items,\n columns,\n nested,\n selectionMode,\n isLoading,\n compact,\n alternateRowColors,\n pagination,\n onPageChange,\n aggregateItems,\n onSelectionChanged,\n rowCommands,\n } = parentProps;\n const { onItemContextMenu, contextMenuElement } = useRowContextMenu<T>(rowCommands);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const { get, ensure } = useChildren<T, C>(nested.getChildren);\n // Forward parent-row selection to the consumer (DataGrid does this; inline did\n // not). A ref keeps the once-created Selection's callback from going stale when\n // the prop changes (mirrors DataGrid's onSelectionChangedRef).\n const onSelectionChangedRef = useRef(onSelectionChanged);\n onSelectionChangedRef.current = onSelectionChanged;\n\n // 'requires-parent' gating: a parent's child grid is only selectable while the\n // parent row is selected. Needs a managed parent Selection (the parent grid\n // must be selectable). Tracks selected parent keys to re-gate children on change.\n const gating = nested.childSelectionGating ?? 'independent';\n const parentSelectable = mapSelectionMode(selectionMode) !== SelectionMode.none;\n const [selectedParentKeys, setSelectedParentKeys] = useState<Set<string>>(new Set());\n const selectionRef = useRef<Selection>();\n if (!selectionRef.current) {\n selectionRef.current = new Selection({\n getKey: (it: IObjectWithKey, index?: number) =>\n it == null ? `__shimmer_${index ?? 0}` : getKeyFn(it as unknown as T),\n onSelectionChanged: () => {\n const sel = selectionRef.current!.getSelection() as unknown as T[];\n setSelectedParentKeys(new Set(sel.map((it) => getKeyFn(it))));\n onSelectionChangedRef.current?.(sel);\n },\n });\n }\n // Surface the silent misconfiguration: gating needs a selectable parent grid.\n React.useEffect(() => {\n if (gating === 'requires-parent' && !parentSelectable && typeof console !== 'undefined') {\n console.warn(\n \"[grid-kit] childSelectionGating 'requires-parent' has no effect: the parent grid is not selectable. Set the nested grid's selectionMode to 'single' or 'multiple'.\"\n );\n }\n }, [gating, parentSelectable]);\n\n const toggle = useCallback(\n (key: string, item: T) => {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else {\n next.add(key);\n ensure(key, item);\n }\n return next;\n });\n },\n [ensure]\n );\n\n const dlColumns = useMemo<IColumn[]>(() => {\n const chevron: IColumn = {\n key: '__expand',\n name: '',\n fieldName: '__expand',\n minWidth: 32,\n maxWidth: 32,\n isResizable: false,\n onRender: (item?: T) => {\n if (!item) return null;\n const key = getKeyFn(item);\n const open = expanded.has(key);\n return (\n <IconButton\n iconProps={{ iconName: open ? 'ChevronDown' : 'ChevronRight' }}\n ariaLabel={open ? 'Collapse' : 'Expand'}\n styles={{ root: { height: 28, width: 28 } }}\n onClick={(e) => {\n e.stopPropagation();\n toggle(key, item);\n }}\n />\n );\n },\n };\n return [chevron, ...toDetailsListColumns(columns, ctx)];\n }, [columns, ctx, expanded, getKeyFn, toggle]);\n\n const onRenderRow = useCallback(\n (rowProps?: IDetailsRowProps, defaultRender?: (p?: IDetailsRowProps) => JSX.Element | null) => {\n if (!rowProps || !defaultRender) return null;\n const parent = rowProps.item as T;\n const key = getKeyFn(parent);\n // Tint the parent row only (not the expansion wrapper) \u2014 pass fresh styles.\n const row = alternateRowColors\n ? defaultRender({\n ...rowProps,\n styles: {\n root: {\n backgroundColor:\n rowProps.itemIndex % 2 === 0 ? alternateRowColors.even : alternateRowColors.odd,\n },\n },\n })\n : defaultRender(rowProps);\n if (!expanded.has(key)) return row;\n const entry = get(key);\n // requires-parent: child selectable only while THIS parent row is selected.\n const childMode =\n gating === 'requires-parent' && !selectedParentKeys.has(key) ? 'none' : nested.childSelectionMode;\n return (\n <div>\n {row}\n {/*\n Stop right-click events from the inline child region bubbling to the parent\n grid's SelectionZone. The child <DataGrid> renders INSIDE the parent rows' DOM\n (not a portal), and its rows carry `data-selection-index` too. Without this, a\n right-click on child row N would bubble up, the parent SelectionZone's\n _findItemRoot would match the child row's index first, and fire the PARENT's\n rowCommands menu for the wrong parent (parentItems[N]) + select it \u2014 a wrong-record\n hazard for a destructive command. Children have no own context menu in v1, so the\n native browser menu is left intact for the child region.\n */}\n <div\n onContextMenu={(e) => e.stopPropagation()}\n style={{ padding: '8px 8px 8px 48px', background: '#faf9f8', borderBottom: '1px solid #edebe9' }}\n >\n {!entry || entry.loading ? (\n <Spinner label=\"Loading related records\u2026\" />\n ) : (\n <DataGrid<C>\n items={entry.rows}\n columns={nested.childColumns}\n registry={nested.childRegistry}\n compact\n selectionMode={childMode}\n onSelectionChanged={\n nested.onChildSelectionChanged\n ? (sel) => nested.onChildSelectionChanged!(parent, sel)\n : undefined\n }\n editable={nested.childEditable}\n editTrigger={nested.childEditTrigger}\n onValueChange={\n nested.onChildValueChange\n ? (rk, fn, v, ov) => nested.onChildValueChange!(parent, rk, fn, v, ov)\n : undefined\n }\n />\n )}\n </div>\n </div>\n );\n },\n [expanded, get, getKeyFn, nested, alternateRowColors, gating, selectedParentKeys]\n );\n\n // Parent-level footers (mirrors DataGrid): aggregate row when any parent column\n // declares an `aggregate`, then the pager. Aggregates over `aggregateItems`\n // (cross-page grand totals) when given, else the displayed parent rows. The\n // footer cells skip the selection checkbox + expand chevron so they line up\n // with the data columns.\n const hasAggregate = columns.some((c) => c.aggregate);\n const footerLeadingSpacer =\n (parentSelectable ? SELECTION_CHECKBOX_WIDTH : 0) + EXPAND_CHEVRON_WIDTH;\n\n return (\n <>\n <ShimmeredDetailsList\n items={items}\n columns={dlColumns}\n onRenderRow={onRenderRow}\n selectionMode={mapSelectionMode(selectionMode)}\n selection={parentSelectable ? selectionRef.current : undefined}\n enableShimmer={isLoading}\n layoutMode={DetailsListLayoutMode.justified}\n compact={compact}\n setKey=\"grid-kit-nested-inline\"\n getKey={(it, index) => (it == null ? `__shimmer_${index ?? 0}` : getKeyFn(it as T))}\n onItemContextMenu={onItemContextMenu as (item?: unknown, index?: number, ev?: Event) => boolean | void}\n styles={getDetailsListStyles()}\n ariaLabelForGrid=\"Nested grid\"\n />\n {contextMenuElement}\n {hasAggregate && (\n <GridAggregateFooter\n items={aggregateItems ?? items}\n columns={columns}\n leadingSpacer={footerLeadingSpacer}\n />\n )}\n {pagination && <GridPaginationFooter pagination={pagination} onPageChange={onPageChange} />}\n </>\n );\n}\n",
|
|
12265
|
-
"src/lib/grid-kit/grid-kit/hosts/nested/NestedTriggerCell.tsx":
|
|
12266
|
-
import {
|
|
12267
|
-
ActionButton,
|
|
12268
|
-
Callout,
|
|
12269
|
-
DirectionalHint,
|
|
12270
|
-
Panel,
|
|
12271
|
-
Spinner,
|
|
12272
|
-
Text,
|
|
12273
|
-
} from '@fluentui/react';
|
|
12274
|
-
import type { NestedConfig } from '../../types';
|
|
12275
|
-
import { DataGrid } from '../DataGrid';
|
|
12276
|
-
import { ReadOnlyGrid } from '../ReadOnlyGrid';
|
|
12277
|
-
import { formatTriggerLabel, panelTypeFor } from './labels';
|
|
12278
|
-
|
|
12279
|
-
interface Entry<C> {
|
|
12280
|
-
loading: boolean;
|
|
12281
|
-
rows: C[];
|
|
12282
|
-
}
|
|
12283
|
-
|
|
12284
|
-
/**
|
|
12285
|
-
* Per-parent-row trigger cell for \`side-panel\` and \`hover-callout\` nested modes.
|
|
12286
|
-
* Children are resolved **lazily** \u2014 on click (panel) or first hover (callout) \u2014
|
|
12287
|
-
* so a 50-row page doesn't fire 50 fetches up front.
|
|
12288
|
-
*/
|
|
12289
|
-
export function NestedTriggerCell<T extends Record<string, unknown>, C extends Record<string, unknown>>(props: {
|
|
12290
|
-
parent: T;
|
|
12291
|
-
nested: NestedConfig<T, C>;
|
|
12292
|
-
}): JSX.Element {
|
|
12293
|
-
const { parent, nested } = props;
|
|
12294
|
-
const [entry, setEntry] = useState<Entry<C> | null>(null);
|
|
12295
|
-
const [open, setOpen] = useState(false);
|
|
12296
|
-
const [hovered, setHovered] = useState(false);
|
|
12297
|
-
const hostRef = useRef<HTMLDivElement>(null);
|
|
12298
|
-
const hoverTimer = useRef<ReturnType<typeof setTimeout>>();
|
|
12299
|
-
const mounted = useRef(true);
|
|
12300
|
-
useEffect(() => {
|
|
12301
|
-
mounted.current = true;
|
|
12302
|
-
return () => {
|
|
12303
|
-
mounted.current = false;
|
|
12304
|
-
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
|
12305
|
-
};
|
|
12306
|
-
}, []);
|
|
12307
|
-
|
|
12308
|
-
// Resolve children once, on demand.
|
|
12309
|
-
const load = useCallback(() => {
|
|
12310
|
-
setEntry((prev) => {
|
|
12311
|
-
if (prev) return prev;
|
|
12312
|
-
const result = nested.getChildren(parent);
|
|
12313
|
-
if (Array.isArray(result)) return { loading: false, rows: result };
|
|
12314
|
-
Promise.resolve(result).then((rows) => mounted.current && setEntry({ loading: false, rows }));
|
|
12315
|
-
return { loading: true, rows: [] };
|
|
12316
|
-
});
|
|
12317
|
-
}, [parent, nested]);
|
|
12318
|
-
|
|
12319
|
-
const count = entry && !entry.loading ? entry.rows.length : undefined;
|
|
12320
|
-
|
|
12321
|
-
if (nested.mode === 'hover-callout') {
|
|
12322
|
-
const max = nested.calloutMaxRows ?? 5;
|
|
12323
|
-
const preview = entry ? entry.rows.slice(0, max) : [];
|
|
12324
|
-
return (
|
|
12325
|
-
<div
|
|
12326
|
-
ref={hostRef}
|
|
12327
|
-
style={{ display: 'inline-block' }}
|
|
12328
|
-
onMouseEnter={() => {
|
|
12329
|
-
load();
|
|
12330
|
-
hoverTimer.current = setTimeout(() => mounted.current && setHovered(true), nested.hoverDelay ?? 300);
|
|
12331
|
-
}}
|
|
12332
|
-
onMouseLeave={() => {
|
|
12333
|
-
if (hoverTimer.current) clearTimeout(hoverTimer.current);
|
|
12334
|
-
setHovered(false);
|
|
12335
|
-
}}
|
|
12336
|
-
>
|
|
12337
|
-
<Text
|
|
12338
|
-
variant="small"
|
|
12339
|
-
styles={{ root: { background: '#edebe9', borderRadius: 10, padding: '2px 10px', cursor: 'default' } }}
|
|
12340
|
-
>
|
|
12341
|
-
{entry?.loading ? '\u2026' : count !== undefined ? \`\${count} related\` : 'related'}
|
|
12342
|
-
</Text>
|
|
12343
|
-
{hovered && entry && !entry.loading && entry.rows.length > 0 && (
|
|
12344
|
-
<Callout
|
|
12345
|
-
target={hostRef.current}
|
|
12346
|
-
onDismiss={() => setHovered(false)}
|
|
12347
|
-
directionalHint={DirectionalHint.bottomLeftEdge}
|
|
12348
|
-
isBeakVisible={false}
|
|
12349
|
-
gapSpace={4}
|
|
12350
|
-
>
|
|
12351
|
-
<div style={{ padding: 8, minWidth: 360, maxWidth: 560 }}>
|
|
12352
|
-
<ReadOnlyGrid<C> items={preview} columns={nested.childColumns} registry={nested.childRegistry} compact />
|
|
12353
|
-
{entry.rows.length > max && (
|
|
12354
|
-
<Text variant="small" styles={{ root: { color: '#605e5c', paddingTop: 4 } }}>
|
|
12355
|
-
+{entry.rows.length - max} more
|
|
12356
|
-
</Text>
|
|
12357
|
-
)}
|
|
12358
|
-
</div>
|
|
12359
|
-
</Callout>
|
|
12360
|
-
)}
|
|
12361
|
-
</div>
|
|
12362
|
-
);
|
|
12363
|
-
}
|
|
12364
|
-
|
|
12365
|
-
// side-panel (default for triggered modes) \u2014 fetch on open.
|
|
12366
|
-
const label = formatTriggerLabel(nested.triggerLabel, count);
|
|
12367
|
-
return (
|
|
12368
|
-
<>
|
|
12369
|
-
<ActionButton
|
|
12370
|
-
iconProps={{ iconName: nested.triggerIcon ?? 'OpenPaneMirrored' }}
|
|
12371
|
-
onClick={() => {
|
|
12372
|
-
load();
|
|
12373
|
-
setOpen(true);
|
|
12374
|
-
}}
|
|
12375
|
-
styles={{ root: { height: 28 } }}
|
|
12376
|
-
>
|
|
12377
|
-
{label}
|
|
12378
|
-
</ActionButton>
|
|
12379
|
-
<Panel
|
|
12380
|
-
isOpen={open}
|
|
12381
|
-
onDismiss={() => setOpen(false)}
|
|
12382
|
-
type={panelTypeFor(nested.panelSize)}
|
|
12383
|
-
headerText={label}
|
|
12384
|
-
closeButtonAriaLabel="Close"
|
|
12385
|
-
>
|
|
12386
|
-
{!entry || entry.loading ? (
|
|
12387
|
-
<Spinner label="Loading related records\u2026" />
|
|
12388
|
-
) : (
|
|
12389
|
-
<DataGrid<C>
|
|
12390
|
-
items={entry.rows}
|
|
12391
|
-
columns={nested.childColumns}
|
|
12392
|
-
registry={nested.childRegistry}
|
|
12393
|
-
compact
|
|
12394
|
-
selectionMode={nested.childSelectionMode}
|
|
12395
|
-
onSelectionChanged={
|
|
12396
|
-
nested.onChildSelectionChanged ? (sel) => nested.onChildSelectionChanged!(parent, sel) : undefined
|
|
12397
|
-
}
|
|
12398
|
-
editable={nested.childEditable}
|
|
12399
|
-
editTrigger={nested.childEditTrigger}
|
|
12400
|
-
onValueChange={
|
|
12401
|
-
nested.onChildValueChange
|
|
12402
|
-
? (rk, fn, v, ov) => nested.onChildValueChange!(parent, rk, fn, v, ov)
|
|
12403
|
-
: undefined
|
|
12404
|
-
}
|
|
12405
|
-
/>
|
|
12406
|
-
)}
|
|
12407
|
-
</Panel>
|
|
12408
|
-
</>
|
|
12409
|
-
);
|
|
12410
|
-
}
|
|
12411
|
-
`,
|
|
12266
|
+
"src/lib/grid-kit/grid-kit/hosts/nested/NestedInline.tsx": "import React, { useCallback, useMemo, useRef, useState } from 'react';\nimport {\n DetailsListLayoutMode,\n type IColumn,\n type IDetailsRowProps,\n type IObjectWithKey,\n IconButton,\n Selection,\n SelectionMode,\n ShimmeredDetailsList,\n Spinner,\n} from '@fluentui/react';\nimport { getDetailsListStyles } from '../../../cell-renderers';\nimport type { NestedGridProps } from '../../types';\nimport { toDetailsListColumns, GridRenderContext } from '../../adapters';\nimport { DataGrid } from '../DataGrid';\nimport { useRowContextMenu } from '../useRowContextMenu';\nimport { GridAggregateFooter } from '../GridAggregateFooter';\nimport { GridPaginationFooter } from '../GridPaginationFooter';\nimport { useChildren } from './useChildren';\n\n// Leading control-column widths the parent footer must skip to line up its cells\n// with the data columns: the selection checkbox (Fluent default ~48px, present\n// only when the parent grid is selectable) and the always-present expand chevron.\nconst SELECTION_CHECKBOX_WIDTH = 48;\nconst EXPAND_CHEVRON_WIDTH = 32;\n\nfunction mapSelectionMode(mode?: string): SelectionMode {\n return mode === 'multiple' ? SelectionMode.multiple : mode === 'single' ? SelectionMode.single : SelectionMode.none;\n}\n\n/**\n * Inline nested grid: a chevron column expands each parent row to reveal its\n * child grid indented beneath (Fluent `onRenderRow`). Children resolve lazily on\n * first expand.\n *\n * Known limitation: an expanded row's height varies with its child grid, which\n * defeats Fluent v8 DetailsList row-height virtualization \u2014 fine for typical\n * subgrid sizes; for very large parent lists prefer side-panel / hover-callout.\n */\nexport function NestedInline<T extends Record<string, unknown>, C extends Record<string, unknown>>(props: {\n parentProps: NestedGridProps<T, C>;\n ctx: GridRenderContext<T>;\n getKeyFn: (it: T) => string;\n}): JSX.Element {\n const { parentProps, ctx, getKeyFn } = props;\n const {\n items,\n columns,\n nested,\n selectionMode,\n isLoading,\n compact,\n alternateRowColors,\n pagination,\n onPageChange,\n aggregateItems,\n onSelectionChanged,\n rowCommands,\n } = parentProps;\n const { onItemContextMenu, contextMenuElement } = useRowContextMenu<T>(rowCommands);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const { get, ensure } = useChildren<T, C>(nested.getChildren, nested.reloadToken);\n // Forward parent-row selection to the consumer (DataGrid does this; inline did\n // not). A ref keeps the once-created Selection's callback from going stale when\n // the prop changes (mirrors DataGrid's onSelectionChangedRef).\n const onSelectionChangedRef = useRef(onSelectionChanged);\n onSelectionChangedRef.current = onSelectionChanged;\n\n // 'requires-parent' gating: a parent's child grid is only selectable while the\n // parent row is selected. Needs a managed parent Selection (the parent grid\n // must be selectable). Tracks selected parent keys to re-gate children on change.\n const gating = nested.childSelectionGating ?? 'independent';\n const parentSelectable = mapSelectionMode(selectionMode) !== SelectionMode.none;\n const [selectedParentKeys, setSelectedParentKeys] = useState<Set<string>>(new Set());\n const selectionRef = useRef<Selection>();\n if (!selectionRef.current) {\n selectionRef.current = new Selection({\n getKey: (it: IObjectWithKey, index?: number) =>\n it == null ? `__shimmer_${index ?? 0}` : getKeyFn(it as unknown as T),\n onSelectionChanged: () => {\n const sel = selectionRef.current!.getSelection() as unknown as T[];\n setSelectedParentKeys(new Set(sel.map((it) => getKeyFn(it))));\n onSelectionChangedRef.current?.(sel);\n },\n });\n }\n // Surface the silent misconfiguration: gating needs a selectable parent grid.\n React.useEffect(() => {\n if (gating === 'requires-parent' && !parentSelectable && typeof console !== 'undefined') {\n console.warn(\n \"[grid-kit] childSelectionGating 'requires-parent' has no effect: the parent grid is not selectable. Set the nested grid's selectionMode to 'single' or 'multiple'.\"\n );\n }\n }, [gating, parentSelectable]);\n\n const toggle = useCallback(\n (key: string, item: T) => {\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(key)) next.delete(key);\n else {\n next.add(key);\n ensure(key, item);\n }\n return next;\n });\n },\n [ensure]\n );\n\n const dlColumns = useMemo<IColumn[]>(() => {\n const chevron: IColumn = {\n key: '__expand',\n name: '',\n fieldName: '__expand',\n minWidth: 32,\n maxWidth: 32,\n isResizable: false,\n onRender: (item?: T) => {\n if (!item) return null;\n const key = getKeyFn(item);\n const open = expanded.has(key);\n return (\n <IconButton\n iconProps={{ iconName: open ? 'ChevronDown' : 'ChevronRight' }}\n ariaLabel={open ? 'Collapse' : 'Expand'}\n styles={{ root: { height: 28, width: 28 } }}\n onClick={(e) => {\n e.stopPropagation();\n toggle(key, item);\n }}\n />\n );\n },\n };\n return [chevron, ...toDetailsListColumns(columns, ctx)];\n }, [columns, ctx, expanded, getKeyFn, toggle]);\n\n const onRenderRow = useCallback(\n (rowProps?: IDetailsRowProps, defaultRender?: (p?: IDetailsRowProps) => JSX.Element | null) => {\n if (!rowProps || !defaultRender) return null;\n const parent = rowProps.item as T;\n const key = getKeyFn(parent);\n // Tint the parent row only (not the expansion wrapper) \u2014 pass fresh styles.\n const row = alternateRowColors\n ? defaultRender({\n ...rowProps,\n styles: {\n root: {\n backgroundColor:\n rowProps.itemIndex % 2 === 0 ? alternateRowColors.even : alternateRowColors.odd,\n },\n },\n })\n : defaultRender(rowProps);\n if (!expanded.has(key)) return row;\n const entry = get(key);\n // requires-parent: child selectable only while THIS parent row is selected.\n const childMode =\n gating === 'requires-parent' && !selectedParentKeys.has(key) ? 'none' : nested.childSelectionMode;\n return (\n <div>\n {row}\n {/*\n Stop right-click events from the inline child region bubbling to the parent\n grid's SelectionZone. The child <DataGrid> renders INSIDE the parent rows' DOM\n (not a portal), and its rows carry `data-selection-index` too. Without this, a\n right-click on child row N would bubble up, the parent SelectionZone's\n _findItemRoot would match the child row's index first, and fire the PARENT's\n rowCommands menu for the wrong parent (parentItems[N]) + select it \u2014 a wrong-record\n hazard for a destructive command. Children have no own context menu in v1, so the\n native browser menu is left intact for the child region.\n */}\n <div\n onContextMenu={(e) => e.stopPropagation()}\n style={{ padding: '8px 8px 8px 48px', background: '#faf9f8', borderBottom: '1px solid #edebe9' }}\n >\n {!entry || entry.loading ? (\n <Spinner label=\"Loading related records\u2026\" />\n ) : (\n <DataGrid<C>\n items={entry.rows}\n columns={nested.childColumns}\n registry={nested.childRegistry}\n compact\n selectionMode={childMode}\n onSelectionChanged={\n nested.onChildSelectionChanged\n ? (sel) => nested.onChildSelectionChanged!(parent, sel)\n : undefined\n }\n editable={nested.childEditable}\n editTrigger={nested.childEditTrigger}\n onValueChange={\n nested.onChildValueChange\n ? (rk, fn, v, ov) => nested.onChildValueChange!(parent, rk, fn, v, ov)\n : undefined\n }\n />\n )}\n </div>\n </div>\n );\n },\n [expanded, get, getKeyFn, nested, alternateRowColors, gating, selectedParentKeys]\n );\n\n // Parent-level footers (mirrors DataGrid): aggregate row when any parent column\n // declares an `aggregate`, then the pager. Aggregates over `aggregateItems`\n // (cross-page grand totals) when given, else the displayed parent rows. The\n // footer cells skip the selection checkbox + expand chevron so they line up\n // with the data columns.\n const hasAggregate = columns.some((c) => c.aggregate);\n const footerLeadingSpacer =\n (parentSelectable ? SELECTION_CHECKBOX_WIDTH : 0) + EXPAND_CHEVRON_WIDTH;\n\n return (\n <>\n <ShimmeredDetailsList\n items={items}\n columns={dlColumns}\n onRenderRow={onRenderRow}\n selectionMode={mapSelectionMode(selectionMode)}\n selection={parentSelectable ? selectionRef.current : undefined}\n enableShimmer={isLoading}\n layoutMode={DetailsListLayoutMode.justified}\n compact={compact}\n setKey=\"grid-kit-nested-inline\"\n getKey={(it, index) => (it == null ? `__shimmer_${index ?? 0}` : getKeyFn(it as T))}\n onItemContextMenu={onItemContextMenu as (item?: unknown, index?: number, ev?: Event) => boolean | void}\n styles={getDetailsListStyles()}\n ariaLabelForGrid=\"Nested grid\"\n />\n {contextMenuElement}\n {hasAggregate && (\n <GridAggregateFooter\n items={aggregateItems ?? items}\n columns={columns}\n leadingSpacer={footerLeadingSpacer}\n />\n )}\n {pagination && <GridPaginationFooter pagination={pagination} onPageChange={onPageChange} />}\n </>\n );\n}\n",
|
|
12267
|
+
"src/lib/grid-kit/grid-kit/hosts/nested/NestedTriggerCell.tsx": "import React, { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n ActionButton,\n Callout,\n DirectionalHint,\n Panel,\n Spinner,\n Text,\n} from '@fluentui/react';\nimport type { NestedConfig } from '../../types';\nimport { DataGrid } from '../DataGrid';\nimport { ReadOnlyGrid } from '../ReadOnlyGrid';\nimport { formatTriggerLabel, panelTypeFor } from './labels';\n\ninterface Entry<C> {\n loading: boolean;\n rows: C[];\n}\n\n/**\n * Per-parent-row trigger cell for `side-panel` and `hover-callout` nested modes.\n * Children are resolved **lazily** \u2014 on click (panel) or first hover (callout) \u2014\n * so a 50-row page doesn't fire 50 fetches up front.\n */\nexport function NestedTriggerCell<T extends Record<string, unknown>, C extends Record<string, unknown>>(props: {\n parent: T;\n nested: NestedConfig<T, C>;\n}): JSX.Element {\n const { parent, nested } = props;\n const [entry, setEntry] = useState<Entry<C> | null>(null);\n const [open, setOpen] = useState(false);\n const [hovered, setHovered] = useState(false);\n const hostRef = useRef<HTMLDivElement>(null);\n const hoverTimer = useRef<ReturnType<typeof setTimeout>>();\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n if (hoverTimer.current) clearTimeout(hoverTimer.current);\n };\n }, []);\n\n // Invalidate the on-demand cache when `reloadToken` changes (e.g. after associating a\n // new child): drop the loaded `entry` so the next open/hover re-fetches via `load()`\n // (which early-returns on a non-null `entry`). Value-compared so it's StrictMode-safe.\n // Edge: if the panel/callout is OPEN at bump time it shows the spinner until reopened \u2014\n // acceptable, these modes are on-demand and usually closed during a toolbar action.\n const prevReloadToken = useRef(nested.reloadToken);\n useEffect(() => {\n if (nested.reloadToken === prevReloadToken.current) return;\n prevReloadToken.current = nested.reloadToken;\n setEntry(null);\n }, [nested.reloadToken]);\n\n // Resolve children once, on demand.\n const load = useCallback(() => {\n setEntry((prev) => {\n if (prev) return prev;\n const result = nested.getChildren(parent);\n if (Array.isArray(result)) return { loading: false, rows: result };\n Promise.resolve(result).then((rows) => mounted.current && setEntry({ loading: false, rows }));\n return { loading: true, rows: [] };\n });\n }, [parent, nested]);\n\n const count = entry && !entry.loading ? entry.rows.length : undefined;\n\n if (nested.mode === 'hover-callout') {\n const max = nested.calloutMaxRows ?? 5;\n const preview = entry ? entry.rows.slice(0, max) : [];\n return (\n <div\n ref={hostRef}\n style={{ display: 'inline-block' }}\n onMouseEnter={() => {\n load();\n hoverTimer.current = setTimeout(() => mounted.current && setHovered(true), nested.hoverDelay ?? 300);\n }}\n onMouseLeave={() => {\n if (hoverTimer.current) clearTimeout(hoverTimer.current);\n setHovered(false);\n }}\n >\n <Text\n variant=\"small\"\n styles={{ root: { background: '#edebe9', borderRadius: 10, padding: '2px 10px', cursor: 'default' } }}\n >\n {entry?.loading ? '\u2026' : count !== undefined ? `${count} related` : 'related'}\n </Text>\n {hovered && entry && !entry.loading && entry.rows.length > 0 && (\n <Callout\n target={hostRef.current}\n onDismiss={() => setHovered(false)}\n directionalHint={DirectionalHint.bottomLeftEdge}\n isBeakVisible={false}\n gapSpace={4}\n >\n <div style={{ padding: 8, minWidth: 360, maxWidth: 560 }}>\n <ReadOnlyGrid<C> items={preview} columns={nested.childColumns} registry={nested.childRegistry} compact />\n {entry.rows.length > max && (\n <Text variant=\"small\" styles={{ root: { color: '#605e5c', paddingTop: 4 } }}>\n +{entry.rows.length - max} more\n </Text>\n )}\n </div>\n </Callout>\n )}\n </div>\n );\n }\n\n // side-panel (default for triggered modes) \u2014 fetch on open.\n const label = formatTriggerLabel(nested.triggerLabel, count);\n return (\n <>\n <ActionButton\n iconProps={{ iconName: nested.triggerIcon ?? 'OpenPaneMirrored' }}\n onClick={() => {\n load();\n setOpen(true);\n }}\n styles={{ root: { height: 28 } }}\n >\n {label}\n </ActionButton>\n <Panel\n isOpen={open}\n onDismiss={() => setOpen(false)}\n type={panelTypeFor(nested.panelSize)}\n headerText={label}\n closeButtonAriaLabel=\"Close\"\n >\n {!entry || entry.loading ? (\n <Spinner label=\"Loading related records\u2026\" />\n ) : (\n <DataGrid<C>\n items={entry.rows}\n columns={nested.childColumns}\n registry={nested.childRegistry}\n compact\n selectionMode={nested.childSelectionMode}\n onSelectionChanged={\n nested.onChildSelectionChanged ? (sel) => nested.onChildSelectionChanged!(parent, sel) : undefined\n }\n editable={nested.childEditable}\n editTrigger={nested.childEditTrigger}\n onValueChange={\n nested.onChildValueChange\n ? (rk, fn, v, ov) => nested.onChildValueChange!(parent, rk, fn, v, ov)\n : undefined\n }\n />\n )}\n </Panel>\n </>\n );\n}\n",
|
|
12412
12268
|
"src/lib/grid-kit/grid-kit/hosts/nested/labels.ts": "import { PanelType } from '@fluentui/react';\n\n/**\n * Resolve a nested trigger label, substituting the `{count}` token. When the\n * count isn't known yet (children not loaded), the token is dropped and extra\n * whitespace collapsed (e.g. \"View {count} related\" \u2192 \"View related\").\n */\nexport function formatTriggerLabel(label: string | undefined, count: number | undefined): string {\n const base = label ?? 'View {count} related';\n if (count === undefined) {\n return base.replace(/\\{count\\}\\s*/g, '').replace(/\\s+/g, ' ').trim();\n }\n return base.replace(/\\{count\\}/g, String(count));\n}\n\n/** Map a side-panel size token to a Fluent v8 `PanelType`. */\nexport function panelTypeFor(size: 'small' | 'medium' | 'large' | undefined): PanelType {\n return size === 'small' ? PanelType.smallFixedFar : size === 'large' ? PanelType.large : PanelType.medium;\n}\n",
|
|
12413
|
-
"src/lib/grid-kit/grid-kit/hosts/nested/useChildren.ts": "import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface ChildEntry<C> {\n loading: boolean;\n rows: C[];\n}\n\n/**\n * Lazily resolves a nested grid's children per parent row. `getChildren` may\n * return a sync array or a promise; results are cached by row key. grid-kit\n * never fetches \u2014 this just normalizes the consumer-supplied resolver and tracks\n * loading. Call `ensure(key, parent)` on demand (row expand / panel open / hover).\n */\nexport function useChildren<T, C>(getChildren: (parent: T) => C[] | Promise<C[]
|
|
12269
|
+
"src/lib/grid-kit/grid-kit/hosts/nested/useChildren.ts": "import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface ChildEntry<C> {\n loading: boolean;\n rows: C[];\n}\n\n/**\n * Lazily resolves a nested grid's children per parent row. `getChildren` may\n * return a sync array or a promise; results are cached by row key. grid-kit\n * never fetches \u2014 this just normalizes the consumer-supplied resolver and tracks\n * loading. Call `ensure(key, parent)` on demand (row expand / panel open / hover).\n */\nexport function useChildren<T, C>(\n getChildren: (parent: T) => C[] | Promise<C[]>,\n reloadToken?: string | number\n) {\n const [cache, setCache] = useState<Record<string, ChildEntry<C>>>({});\n const cacheRef = useRef(cache);\n cacheRef.current = cache;\n // Remember the parent object per cached key so a `reloadToken` bump can re-run\n // `getChildren(parent)` for already-loaded rows.\n const parentsRef = useRef<Record<string, T>>({});\n // `getChildren` accessed via a ref inside the reload effect \u2192 the effect can depend\n // on `reloadToken` alone without a stale closure or an exhaustive-deps warning.\n const getChildrenRef = useRef(getChildren);\n getChildrenRef.current = getChildren;\n // Guard against setState after unmount (e.g. host swapped while a child fetch\n // is in flight) \u2014 mirrors NestedTriggerCell / DetailPaneChildren.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const ensure = useCallback(\n (key: string, parent: T) => {\n if (cacheRef.current[key]) return;\n parentsRef.current[key] = parent;\n const result = getChildren(parent);\n if (Array.isArray(result)) {\n setCache((p) => ({ ...p, [key]: { loading: false, rows: result } }));\n } else {\n setCache((p) => ({ ...p, [key]: { loading: true, rows: [] } }));\n Promise.resolve(result).then((rows) => {\n if (mounted.current) setCache((p) => ({ ...p, [key]: { loading: false, rows } }));\n });\n }\n },\n [getChildren]\n );\n\n // Re-fetch every already-loaded parent's children when `reloadToken` changes. Compare\n // the token by VALUE (not a first-run counter) so it's safe under StrictMode's\n // double-invoke. Crucially we do NOT flip `loading: true` here \u2014 the old rows stay\n // rendered until the fresh ones land, so there's no spinner flash and (since `ensure`\n // is only re-called on expand) no perpetual spinner for an already-expanded row.\n const prevToken = useRef(reloadToken);\n useEffect(() => {\n if (reloadToken === prevToken.current) return;\n prevToken.current = reloadToken;\n for (const key of Object.keys(parentsRef.current)) {\n const parent = parentsRef.current[key];\n const result = getChildrenRef.current(parent);\n if (Array.isArray(result)) {\n setCache((p) => ({ ...p, [key]: { loading: false, rows: result } }));\n } else {\n Promise.resolve(result).then((rows) => {\n if (mounted.current) setCache((p) => ({ ...p, [key]: { loading: false, rows } }));\n });\n }\n }\n }, [reloadToken]);\n\n const get = useCallback((key: string): ChildEntry<C> | undefined => cache[key], [cache]);\n\n return { get, ensure };\n}\n",
|
|
12414
12270
|
"src/lib/grid-kit/grid-kit/hosts/rowContextMenu.ts": "import type { IContextualMenuItem } from '@fluentui/react';\nimport type { RowCommand } from '../types';\n\n/**\n * Pure projection of a row's `RowCommand[]` \u2192 Fluent `ContextualMenu` items for a\n * given row. Commands whose `visible(item)` returns `false` are dropped; `disabled`\n * is resolved per row; `onClick` is bound to the row and chained with `onAfter`\n * (used by the host to dismiss the menu). No React / no rendering \u2014 unit-testable in\n * grid-kit's node test env (the type-only Fluent import has no runtime cost).\n */\nexport function buildRowMenuItems<T>(\n commands: RowCommand<T>[],\n item: T,\n onAfter: () => void\n): IContextualMenuItem[] {\n return commands\n .filter((c) => c.visible?.(item) !== false)\n .map((c) => ({\n key: c.key,\n text: c.text,\n iconProps: c.iconName ? { iconName: c.iconName } : undefined,\n disabled: c.disabled?.(item) ?? false,\n onClick: () => {\n c.onClick(item);\n onAfter();\n },\n }));\n}\n",
|
|
12415
12271
|
"src/lib/grid-kit/grid-kit/hosts/toolbar/GridToolbar.tsx": `import React from 'react';
|
|
12416
12272
|
import {
|
|
@@ -12471,7 +12327,7 @@ export const GridToolbar: React.FC<{ config: GridToolbarConfig }> = ({ config })
|
|
|
12471
12327
|
"src/lib/grid-kit/grid-kit/registry/createCellRegistry.tsx": "/**\n * The cell-renderer registry \u2014 the single source of truth that both grid hosts\n * (DetailsList adapter + GridCustomizer overrides) consume. Maps each\n * `CellRendererType` to a read (and optional edit) wrapper over the canonical\n * @khester/dynamics-cell-renderers components.\n */\nimport {\n TextCell,\n LinkCell,\n CurrencyCell,\n DateCell,\n NumberCell,\n OptionSetCell,\n ToggleCell,\n IconCell,\n ProgressBarCell,\n ColoredCell,\n LookupCell,\n RatingCell,\n CompositeCell,\n EditableTextCell,\n EditableNumberCell,\n EditableDateCell,\n EditableOptionSetCell,\n EditableLookupCell,\n EditableRatingCell,\n} from '../../cell-renderers';\nimport type { CellRegistration, GridRegistry } from '../types';\nimport { makeReadWrapper, makeEditWrapper } from './renderers';\nimport { FileDropCell } from '../dnd/FileDropCell';\n\nfunction buildDefaults(): Map<string, CellRegistration> {\n const m = new Map<string, CellRegistration>();\n\n m.set('text', {\n read: makeReadWrapper(TextCell, 'GridKit(text)'),\n edit: makeEditWrapper(EditableTextCell, 'GridKit(edit:text)'),\n });\n m.set('link', { read: makeReadWrapper(LinkCell, 'GridKit(link)') });\n // Editable lookup is fetch-free: the consumer supplies `searchLookup(term)` on\n // the column's rendererConfig (mapped to `column.lookupSearch`), so the cell\n // never needs an apiService. A column opts in with editorType:'lookup'.\n m.set('lookup', {\n read: makeReadWrapper(LookupCell, 'GridKit(lookup)'),\n edit: makeEditWrapper(EditableLookupCell, 'GridKit(edit:lookup)'),\n });\n m.set('optionset', {\n read: makeReadWrapper(OptionSetCell, 'GridKit(optionset)'),\n edit: makeEditWrapper(EditableOptionSetCell, 'GridKit(edit:optionset)'),\n });\n m.set('currency', {\n read: makeReadWrapper(CurrencyCell, 'GridKit(currency)'),\n edit: makeEditWrapper(EditableNumberCell, 'GridKit(edit:currency)'),\n });\n m.set('number', {\n read: makeReadWrapper(NumberCell, 'GridKit(number)'),\n edit: makeEditWrapper(EditableNumberCell, 'GridKit(edit:number)'),\n });\n m.set('date', {\n read: makeReadWrapper(DateCell, 'GridKit(date)'),\n edit: makeEditWrapper(EditableDateCell, 'GridKit(edit:date)'),\n });\n m.set('datetime', {\n read: makeReadWrapper(DateCell, 'GridKit(datetime)'),\n edit: makeEditWrapper(EditableDateCell, 'GridKit(edit:datetime)'),\n });\n m.set('boolean', { read: makeReadWrapper(ToggleCell, 'GridKit(boolean)') });\n m.set('toggle', { read: makeReadWrapper(ToggleCell, 'GridKit(toggle)') });\n m.set('icon', { read: makeReadWrapper(IconCell, 'GridKit(icon)') });\n m.set('progress', { read: makeReadWrapper(ProgressBarCell, 'GridKit(progress)') });\n m.set('coloredCell', { read: makeReadWrapper(ColoredCell, 'GridKit(coloredCell)') });\n // Rating is click-to-set editable (stars or sentiment emoji). Opt in with\n // editorType:'number'; rendererConfig.style:'emoji' switches to faces.\n m.set('rating', {\n read: makeReadWrapper(RatingCell, 'GridKit(rating)'),\n edit: makeEditWrapper(EditableRatingCell, 'GridKit(edit:rating)'),\n });\n m.set('composite', { read: makeReadWrapper(CompositeCell, 'GridKit(composite)') });\n // grid-kit-native (not a canonical-lib wrapper): a per-cell file-drop target.\n m.set('fileDrop', { read: FileDropCell });\n\n return m;\n}\n\n/**\n * Create a cell-renderer registry with all built-ins, optionally overridden.\n * Consumers can also call `registry.register(type, { read, edit })` later to\n * override a built-in or add a brand-new cell type.\n */\nexport function createCellRegistry(overrides?: Record<string, CellRegistration>): GridRegistry {\n const map = buildDefaults();\n if (overrides) {\n for (const [key, reg] of Object.entries(overrides)) map.set(key, reg);\n }\n\n const registry: GridRegistry = {\n get: (type) => map.get(type),\n register: (type, reg) => {\n map.set(type, reg);\n },\n resolve: (column, editable) => {\n const reg = map.get(column.rendererType);\n if (!reg) {\n // Unknown renderer type \u2192 safe text fallback (no silent blank cells).\n // Read 'text' live so a consumer's register('text', \u2026) override is honored.\n // eslint-disable-next-line no-console\n if (typeof console !== 'undefined') {\n console.warn(`[grid-kit] No renderer registered for \"${column.rendererType}\"; using text.`);\n }\n return (map.get('text') as CellRegistration).read as never;\n }\n const canEdit =\n editable &&\n !!column.editorType &&\n column.editorType !== 'none' &&\n !column.isLocked &&\n !!reg.edit;\n return (canEdit ? reg.edit! : reg.read) as never;\n },\n };\n\n return registry;\n}\n",
|
|
12472
12328
|
"src/lib/grid-kit/grid-kit/registry/index.ts": "export { createCellRegistry } from './createCellRegistry';\nexport { makeReadWrapper, makeEditWrapper } from './renderers';\nexport { toReusableColumn, rendererToFieldType } from './columnMapping';\n",
|
|
12473
12329
|
"src/lib/grid-kit/grid-kit/registry/renderers.tsx": "/**\n * Wrapper factories that adapt grid-kit `GridCellProps` onto a canonical\n * @khester/dynamics-cell-renderers component. The returned value is a React\n * COMPONENT (rendered as an element by the adapters / DataGrid \u2014 never called\n * as a function), so the canonical editable cells' hooks stay legal.\n */\nimport React from 'react';\nimport type { CellRendererProps as LibCellProps, IReusableColumn } from '../../cell-renderers';\nimport type { GridCellProps, GridCellRenderer } from '../types';\nimport { toReusableColumn } from './columnMapping';\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype LibComponent = React.ComponentType<any>;\n\nconst noop = (): void => undefined;\n\n/** Ensure the row exposes a `.key` (some canonical editable cells require it). */\nfunction ensureKey<T>(item: T, key: string): T {\n if (item && typeof item === 'object' && (item as any).key !== undefined) return item;\n return { ...(item as any), key } as T;\n}\n\n/**\n * Bridge the display value into the canonical renderer's `formatValue` path so\n * read cells honor the right label instead of re-deriving from the raw code.\n * Precedence: user-provided formatValue > host-supplied formatted value\n * (Dataverse optionset/lookup label) > optionset value\u2192label map > the\n * computed fallback for numeric/date types (which also fixes numeric-string\n * values that the canonical Currency/Number cells would otherwise render as '-').\n * Mutates the freshly-mapped column (a new object per render \u2014 safe).\n */\nfunction applyDisplayValue<T>(column: IReusableColumn<T>, props: GridCellProps<T>): void {\n if (column.formatValue) return; // explicit user formatter wins\n if (props.suppliedFormattedValue) {\n const s = props.suppliedFormattedValue;\n column.formatValue = () => s;\n return;\n }\n switch (column.fieldType) {\n case 'optionset': {\n const opts = column.optionSetOptions;\n if (opts && opts.length) {\n column.formatValue = (v: unknown) => {\n const o = opts.find((opt) => String(opt.value) === String(v));\n return o ? o.label : v !== null && v !== undefined && v !== '' ? String(v) : '-';\n };\n }\n return;\n }\n case 'currency':\n case 'number':\n case 'date':\n case 'datetime': {\n const fv = props.formattedValue;\n column.formatValue = () => fv || '-';\n return;\n }\n default:\n return;\n }\n}\n\n/** Read-only wrapper: maps GridCellProps \u2192 the canonical CellRendererProps. */\nexport function makeReadWrapper(Comp: LibComponent, displayName: string): GridCellRenderer {\n const Wrapped: React.FC<GridCellProps> = (props) => {\n const column = toReusableColumn(props.column);\n applyDisplayValue(column, props);\n // Editable toggles render an interactive ToggleCell (no editable variant\n // exists); wire its change back to the grid's onValueChange so edits persist.\n if (column.editable && !column.onToggleChange && props.onValueChange) {\n const onVC = props.onValueChange;\n const original = props.value;\n column.onToggleChange = (key: string, fieldName: string, v: boolean) =>\n onVC(key, fieldName, v, original);\n }\n const cellProps: LibCellProps<any> = {\n item: props.item,\n column,\n value: props.value,\n itemKey: props.itemKey,\n };\n return <Comp {...cellProps} />;\n };\n Wrapped.displayName = displayName;\n return Wrapped as GridCellRenderer;\n}\n\n/** Editable wrapper: threads edit-state onto the canonical editable cell props. */\nexport function makeEditWrapper(Comp: LibComponent, displayName: string): GridCellRenderer {\n const Wrapped: React.FC<GridCellProps> = (props) => {\n const column = toReusableColumn(props.column);\n applyDisplayValue(column, props); // read-mode display of an editable cell\n const item = ensureKey(props.item, props.itemKey);\n const editProps = {\n item,\n column,\n value: props.value,\n itemKey: props.itemKey,\n isEditMode: props.isEditing ?? false,\n isDirty: props.isDirty ?? false,\n editedValue: props.editedValue,\n onValueChange: props.onValueChange ?? noop,\n errorMessage: props.errorMessage,\n onValidationError: props.onValidationError,\n };\n return <Comp {...editProps} />;\n };\n Wrapped.displayName = displayName;\n return Wrapped as GridCellRenderer;\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n",
|
|
12474
|
-
"src/lib/grid-kit/grid-kit/types/index.ts": "import type * as React from 'react';\nimport type { IContextualMenuProps } from '@fluentui/react';\nimport type { NavTarget } from '../navigation/navigateTo';\nimport type { GridFilterModel } from '../core/filter';\n\n/**\n * grid-kit canonical contract.\n *\n * grid-kit OWNS this vocabulary (it does not import form-runtime, which is a\n * sibling, zero-React, typecheck-standalone package). The literals are aligned\n * with both `@khester/dynamics-cell-renderers`' `ListFieldType` and\n * form-runtime's `CellRendererType` so a grid-customizer definition maps onto\n * these via `fromGridCustomizerDefinition()` without a runtime dependency.\n */\n\n// \u2500\u2500\u2500 Renderer & editor vocabularies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Cell renderer types. Superset of the canonical library + form-runtime. */\nexport type CellRendererType =\n | 'text'\n | 'link'\n | 'lookup'\n | 'optionset'\n | 'currency'\n | 'number'\n | 'date'\n | 'datetime'\n | 'boolean'\n | 'toggle'\n | 'icon'\n | 'progress'\n | 'coloredCell'\n | 'rating'\n | 'composite'\n | 'fileDrop';\n\n/** Inline editor types for editable grids. `'none'` (or undefined) = read-only. */\nexport type CellEditorType = 'text' | 'number' | 'date' | 'dropdown' | 'toggle' | 'lookup' | 'none';\n\nexport type GridSelectionMode = 'none' | 'single' | 'multiple';\n\n// \u2500\u2500\u2500 Column definition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * grid-kit's renderer-agnostic column. The host adapters convert this into the\n * shape each grid host needs (Fluent `IColumn.onRender` or a GridCustomizer FC).\n */\nexport interface ColumnDef<T = Record<string, unknown>> {\n /** Stable column key. */\n key: string;\n /** Logical attribute / row field this column reads. */\n fieldName: string;\n /** Header text. */\n name: string;\n\n /**\n * Which renderer to use. A built-in `CellRendererType`, or any custom string\n * registered via `registry.register(type, \u2026)`. The `(string & {})` keeps\n * autocomplete for the built-ins while allowing custom types without a cast.\n */\n // eslint-disable-next-line @typescript-eslint/ban-types -- `string & {}` is the idiom that keeps literal autocomplete\n rendererType: CellRendererType | (string & {});\n /**\n * Type-specific renderer config. Shape depends on `rendererType` \u2014 e.g.\n * progress \u2192 { max, color, showLabel }\n * currency \u2192 { currencyCode, locale, decimals }\n * coloredCell \u2192 { colorMap, getBackgroundColor }\n * optionset \u2192 { options: [{ value, label, color }] }\n * rating \u2192 { max, color, allowHalf }\n * composite \u2192 CompositeRendererConfig (layout/gap/slots)\n * lookup \u2192 { entitySetName, searchField, targetEntity, \u2026 }\n */\n rendererConfig?: Record<string, unknown>;\n\n /** Inline editor type (editable grids). Undefined/`'none'` \u21D2 read-only. */\n editorType?: CellEditorType;\n /** Locked even when the grid is editable (computed/permission-gated fields). */\n isLocked?: boolean;\n\n // sizing / behavior\n width?: number;\n minWidth?: number;\n maxWidth?: number;\n isResizable?: boolean;\n isSortable?: boolean;\n isFilterable?: boolean;\n\n /** Footer aggregate for this (numeric) column. Rendered in the grid footer row. */\n aggregate?: 'sum' | 'avg' | 'count' | 'min' | 'max';\n /**\n * Custom format for the aggregate footer cell. Default: `count` \u2192 integer;\n * `sum`/`avg`/`min`/`max` \u2192 `computeFormattedValue` by `rendererType` (so a\n * currency column's sum shows as currency); `null` \u2192 blank.\n */\n aggregateFormat?: (value: number | null, fn: 'sum' | 'avg' | 'count' | 'min' | 'max') => string;\n\n // callbacks\n /** Link/record-link click handler. */\n onLinkClick?: (item: T) => void;\n /** Per-column value-change handler (4-arg, matches the editable cells). */\n onValueChange?: (rowKey: string, fieldName: string, value: unknown, originalValue: unknown) => void;\n /** Edit-mode validation \u2014 returns an error message or undefined. */\n validate?: (value: unknown, rowKey: string) => string | undefined;\n\n // hover callout parity (preserves Track-and-Ship behavior on migration)\n /**\n * Custom callout content shown on hover over the cell (e.g. a lookup's\n * related-record preview). Consumed by the DetailsList host (`toDetailsListColumns`\n * \u2192 `CalloutCell`); ignored when `onRender` (the escape hatch) is set, and not yet\n * wired on the Grid Customizer host.\n */\n calloutContent?: (item: T) => React.ReactNode;\n /** Callout content shown on icon hover (icon columns). */\n onIconMouseEnter?: (item: T) => React.ReactNode;\n /** How the callout dismisses. */\n dismissMode?: 'mouseleave' | 'timeout';\n /** Auto-dismiss duration (ms) when `dismissMode === 'timeout'`. */\n timeoutDuration?: number;\n\n /** Escape hatch: fully custom cell render, bypassing the registry. */\n onRender?: (item: T) => React.ReactNode;\n}\n\n// \u2500\u2500\u2500 The keystone cell contract \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The single normalized cell-render contract every grid-kit renderer speaks.\n * A superset union of the three host shapes (DetailsList `onRender(item,idx,col)`,\n * GridCustomizer `FC<CellRendererProps>`, and the canonical lib's\n * `Cell({item,column,value,itemKey})`). Adapters build this per cell; renderers\n * never reach back into a host.\n */\nexport interface GridCellProps<T = Record<string, unknown>> {\n /** Raw field value. */\n value: unknown;\n /** Display string \u2014 host-supplied (Dataverse formatted value) or computed fallback. */\n formattedValue: string;\n /**\n * Only set when the value was supplied by the host/platform (e.g. Dataverse\n * `@OData\u2026FormattedValue`), NOT when `formattedValue` is a computed fallback.\n * Renderers use this to honor a real optionset/lookup label over the raw code.\n */\n suppliedFormattedValue?: string;\n /** Full row. */\n item: T;\n /** Row key (for change callbacks). */\n itemKey: string;\n /** Column definition. */\n column: ColumnDef<T>;\n\n // edit-state (read-only renderers ignore these)\n isEditing?: boolean;\n isDirty?: boolean;\n editedValue?: unknown;\n errorMessage?: string;\n /** 4-arg change signal, matching the canonical editable cells. */\n onValueChange?: (rowKey: string, fieldName: string, value: unknown, originalValue: unknown) => void;\n onValidationError?: (rowKey: string, fieldName: string, message: string | undefined) => void;\n onEditStart?: () => void;\n onEditEnd?: () => void;\n}\n\n/** A grid-kit cell renderer is a component (rendered as an element, never called). */\nexport type GridCellRenderer<T = Record<string, unknown>> = React.ComponentType<GridCellProps<T>>;\n\n// \u2500\u2500\u2500 Registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Read + optional edit renderer pair for a cell type. */\nexport interface CellRegistration<T = Record<string, unknown>> {\n read: GridCellRenderer<T>;\n edit?: GridCellRenderer<T>;\n}\n\n/** The cell-renderer registry shared across both grid hosts. */\nexport interface GridRegistry {\n /** Look up a registration (built-in or custom). */\n get(type: CellRendererType | string): CellRegistration | undefined;\n /** Register or override a cell type (built-in override or brand-new type). */\n register(type: CellRendererType | string, reg: CellRegistration): void;\n /** Resolve the renderer for a column, honoring read-vs-edit. */\n resolve<T = Record<string, unknown>>(column: ColumnDef<T>, editable: boolean): GridCellRenderer<T>;\n}\n\n// \u2500\u2500\u2500 Grid host props \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GridSortState {\n fieldName: string;\n direction: 'asc' | 'desc';\n}\n\nexport interface GridPaginationState {\n currentPage: number;\n pageSize: number;\n totalItems: number;\n}\n\nexport interface GridCommand {\n key: string;\n text: string;\n iconName?: string;\n onClick?: () => void;\n disabled?: boolean;\n /** Render as a split button (primary `onClick` + a `subMenuProps` dropdown). */\n split?: boolean;\n /** Dropdown menu for split/menu commands (e.g. the export CSV/JSON choice). */\n subMenuProps?: IContextualMenuProps;\n}\n\n/**\n * A per-row command shown in the right-click context menu (`GridProps.rowCommands`).\n *\n * Intentionally NOT a `GridCommand`: that is a toolbar command with a zero-arg,\n * optional `onClick`, a boolean `disabled`, and `split`/`subMenuProps` (meaningless\n * per-row). A `RowCommand` is item-aware \u2014 `onClick(item)` and the `visible`/`disabled`\n * predicates all receive the row. `onClick` takes only the item (no positional index):\n * resolve the row off the item itself (e.g. a stamped `__rowIndex`) so callers can't\n * mis-key on a display index.\n */\nexport interface RowCommand<T = Record<string, unknown>> {\n key: string;\n text: string;\n iconName?: string;\n onClick: (item: T) => void;\n /** Hide this command for a row when it returns `false`. Default: shown. */\n visible?: (item: T) => boolean;\n /** Disable (grey out) this command for a row when it returns `true`. Default: enabled. */\n disabled?: (item: T) => boolean;\n}\n\n/** Turnkey CSV/JSON export config (a toolbar split-button over the host's items). */\nexport interface GridExportConfig {\n /** Show the Export split-button in the toolbar. */\n enabled?: boolean;\n /** Base filename (a date suffix is added). Default 'grid-export'. */\n filename?: string;\n /** Formats offered in the split-button menu. Default ['csv', 'json']. */\n formats?: Array<'csv' | 'json'>;\n /**\n * `'all'` always exports every row in `items`. `'selected'` exports only the\n * current selection (requires `selectionMode` \u2260 'none'; no-ops when nothing is\n * selected). Default (unset): the selection when non-empty, else all `items`.\n */\n scope?: 'all' | 'selected';\n /**\n * Open the `GridExportDialog` (format + column picker + filename) instead of\n * exporting immediately. The split-button becomes a single \"Export\u2026\" button.\n */\n dialog?: boolean;\n}\n\nexport interface GridToolbarConfig {\n showSearch?: boolean;\n searchPlaceholder?: string;\n onSearch?: (term: string) => void;\n commands?: GridCommand[];\n}\n\n/** Whole-row file-drop config (HTML5 drop of OS files onto a row). */\nexport interface RowFileDropConfig<T = Record<string, unknown>> {\n /** Called with the row + the accepted dropped files. */\n onDrop: (item: T, files: File[]) => void;\n /** Allowed extensions (e.g. `['.pdf', '.png']`). Empty/undefined = any. */\n accept?: string[];\n /** Allow multiple files (default false \u2192 first accepted file only). */\n multiple?: boolean;\n /** Called when a drop is rejected (wrong type / no files). */\n onReject?: (item: T, reason: string) => void;\n}\n\nexport interface GridProps<T = Record<string, unknown>> {\n items: T[];\n columns: ColumnDef<T>[];\n /** Defaults to `createCellRegistry()`. */\n registry?: GridRegistry;\n selectionMode?: GridSelectionMode;\n /** Fires with the currently selected rows (requires `selectionMode` \u2260 'none'). */\n onSelectionChanged?: (selectedItems: T[]) => void;\n /**\n * Enables inline editing for columns whose `editorType` is set (and not\n * `'none'`/locked). By default editing is **per-cell click-to-edit**\n * (`editTrigger: 'click'`) \u2014 a cell shows read text until clicked, then its\n * inline editor. Set `editTrigger: 'always'` for an Excel-like grid where\n * every editable cell shows its editor at once.\n */\n editable?: boolean;\n /** Edit activation model when `editable`. Default `'click'`. */\n editTrigger?: 'click' | 'always';\n isLoading?: boolean;\n sort?: GridSortState;\n onSortChange?: (fieldName: string, direction: 'asc' | 'desc') => void;\n /**\n * Presentational pager only \u2014 the consumer MUST pre-slice `items` to the\n * current page. The grid renders exactly what is in `items`; it does not\n * page internally.\n */\n pagination?: GridPaginationState;\n onPageChange?: (page: number) => void;\n /**\n * Rows the aggregate footer computes over. Defaults to the displayed (filtered)\n * rows \u2014 i.e. the current page when the consumer pre-paginates. Pass the FULL\n * dataset here for cross-page grand totals; an active filter (`filterBuilder`) is\n * applied to it too, so the total reflects what the filter shows across all pages.\n * Note: on `<GroupedGrid>`, per-group subtotals (`showGroupAggregates`) stay\n * page-scoped \u2014 only this overall footer goes cross-page \u2014 so they intentionally\n * need not sum to the grand total.\n */\n aggregateItems?: T[];\n toolbar?: GridToolbarConfig;\n /** Row-key accessor (default: `item.key`). */\n getKey?: (item: T) => string;\n /** Bubbled value-change from any editable cell. */\n onValueChange?: (rowKey: string, fieldName: string, value: unknown, originalValue: unknown) => void;\n onActiveItemChanged?: (item: T, index?: number) => void;\n /** Compact row height. */\n compact?: boolean;\n /**\n * Fill the host's container: the grid root becomes a flex column at `height`\n * (default 100%) and the list/cards scroll internally, so the toolbar + footers\n * stay fixed. Default off (natural height). For dropping a grid into a surface-kit\n * Section / Dialog / Panel that owns the available height.\n *\n * Requirements / limits:\n * - The container MUST have a resolved height (a flex/grid parent or an explicit\n * height) \u2014 `height: '100%'` against an auto-height parent renders nothing.\n * - v1: the DetailsList column header scrolls with the body (not pinned).\n * - Honored by side-panel / hover-callout nested modes (via DataGrid), but NOT by\n * `<NestedGrid mode=\"inline\">` (inline child height is driven by row expansion).\n */\n fill?: boolean;\n /** Explicit grid height (number = px, or any CSS value). With `fill`, defaults to '100%'. */\n height?: number | string;\n /** Make each row a file-drop target (OS files \u2192 `onDrop(item, files)`). */\n rowFileDrop?: RowFileDropConfig<T>;\n /**\n * Adds a turnkey Export (CSV/JSON) split-button to the toolbar over the host's\n * current `items` (or the selection when non-empty). Renders the toolbar even\n * if `toolbar` is undefined.\n */\n exportConfig?: GridExportConfig;\n /**\n * Zebra-stripe rows with the given even/odd row background colors. Composes\n * with `rowFileDrop` (both share the DetailsList `onRenderRow`). No-op on\n * CardGrid (no rows). Mirrors form-runtime's `alternateRowColors`.\n */\n alternateRowColors?: { even: string; odd: string };\n /**\n * Resolve a row \u2192 a navigation target. When set, `link`/`lookup` cells open it\n * on click and a toolbar \"Open\" command navigates the active/selected row.\n * Executed via `Xrm.Navigation` (no-op + console on localhost / no Xrm).\n */\n navigateTo?: (item: T) => NavTarget | undefined;\n /** Row-level navigation hook, wired to DetailsList `onItemInvoked` (double-click / Enter). */\n onRowNavigate?: (item: T) => void;\n /**\n * Per-row right-click context menu. When set (and non-empty), right-clicking a row\n * opens a Fluent v8 `ContextualMenu` of these commands (the native browser menu is\n * suppressed). When unset/empty the native menu is left intact (strict no-op for\n * existing consumers). Honored by every host: the DetailsList-backed grids (`DataGrid`,\n * `GroupedGrid`, and `<NestedGrid>`'s inline/side-panel/hover-callout/detail-pane\n * parents), the card grids (flat `CardGrid` and the `NestedCardParent` card parents,\n * per-card right-click), and the `FocusedViewGrid` rail.\n */\n rowCommands?: RowCommand<T>[];\n /**\n * Adds a \"Columns\" toolbar command that opens a column-chooser panel\n * (show/hide + reorder). The grid manages the visible set internally; opt-in.\n */\n columnChooser?: boolean;\n /** Notified with the new visible column keys (in order) when the chooser applies. */\n onColumnOrderChange?: (visibleOrder: string[]) => void;\n /**\n * Adds a \"Filters\" toolbar command that opens a filter-builder panel (field +\n * operator + value conditions combined with All/Any). The grid filters its\n * in-memory `items` by the built model \u2014 the list, aggregate footer, and export\n * scope all reflect the filter; opt-in. Operates on the passed `items`, so pair\n * it with a client-side (non-paginated) dataset, like the aggregate footer; for\n * server-side filtering, read `onFilterChange` and re-query.\n */\n filterBuilder?: boolean;\n /** Notified with the applied filter model when the filter panel applies. */\n onFilterChange?: (model: GridFilterModel) => void;\n}\n\n// \u2500\u2500\u2500 Read-Only grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Read-only grid: `<DataGrid>` with selection off, editing off, and command-bar\n * commands stripped. A thin preset \u2014 no extra config.\n */\nexport type ReadOnlyGridProps<T = Record<string, unknown>> = Omit<\n GridProps<T>,\n 'selectionMode' | 'editable' | 'editTrigger' | 'onSelectionChanged' | 'onValueChange'\n>;\n\n// \u2500\u2500\u2500 Card grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Card-view layout config. Mirrors form-runtime's `GridCardViewConfig` (minus\n * its `enabled` flag \u2014 choosing `<CardGrid>` IS \"enabled\").\n */\nexport interface CardConfig {\n /** Cards per row (CSS grid columns). Default 3. */\n cardsPerRow?: number;\n /** Fixed card height in px (otherwise content-sized). */\n cardHeight?: number;\n /** Column whose value is the card title. Default: first column. */\n titleField?: string;\n /** Column whose value is the card subtitle. */\n subtitleField?: string;\n /** Column whose value is an image URL shown as a card thumbnail. */\n imageField?: string;\n /**\n * Columns rendered (label + value) in the card body. Default: all columns\n * except the title/subtitle/image fields.\n */\n bodyFields?: string[];\n}\n\nexport interface CardGridProps<T = Record<string, unknown>> extends Omit<GridProps<T>, 'compact'> {\n card?: CardConfig;\n}\n\n// \u2500\u2500\u2500 Grouped grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Grouping config for `<GroupedGrid>`. grid-kit-originated (form-runtime has no\n * `groupBy` field yet); maps onto Fluent v8 DetailsList `IGroup[]`.\n */\nexport interface GroupConfig<T = Record<string, unknown>> {\n /** Field whose value buckets rows into groups. */\n groupBy: string;\n /** Custom header label; default is the formatted group value. */\n getGroupLabel?: (groupValue: unknown, items: T[]) => string;\n /** Start groups collapsed. Default false. */\n collapsedByDefault?: boolean;\n /** Show a per-group count in the header. Default true. */\n showCount?: boolean;\n /**\n * Render a per-group subtotal footer row (aggregating each group's rows for the\n * columns that declared an `aggregate`). Default false. The overall footer below\n * the grid is unaffected. Subtotals always aggregate the displayed (page) rows of\n * each group \u2014 only the overall footer goes cross-page via `aggregateItems`.\n */\n showGroupAggregates?: boolean;\n}\n\nexport interface GroupedGridProps<T = Record<string, unknown>> extends GridProps<T> {\n group: GroupConfig<T>;\n}\n\n// \u2500\u2500\u2500 Nested / expandable grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * How a parent row surfaces its child records. Mirrors form-runtime's\n * `NestedDisplayMode`.\n * - `inline` \u2014 chevron expands the child grid under the parent row\n * - `detail-pane` \u2014 child grid renders in a master-detail pane (FocusedViewGrid)\n * - `side-panel` \u2014 a trigger opens a Fluent Panel hosting the child grid\n * - `hover-callout` \u2014 hovering a count badge shows a compact callout of top-N rows\n */\nexport type NestedDisplayMode = 'inline' | 'detail-pane' | 'side-panel' | 'hover-callout';\n\n/** Side-panel size (maps to Fluent v8 `PanelType`). */\nexport type NestedSidePanelSize = 'small' | 'medium' | 'large';\n\n/**\n * Nested-grid config for `<NestedGrid>`. grid-kit never fetches \u2014 the consumer\n * supplies `getChildren(parent)` (sync array or promise); it owns the\n * relationship/FetchXML. The child grid is itself a grid-kit grid.\n */\nexport interface NestedConfig<T = Record<string, unknown>, C = Record<string, unknown>> {\n /** How children surface. */\n mode: NestedDisplayMode;\n /** Child grid columns. */\n childColumns: ColumnDef<C>[];\n /** Resolve a parent row \u2192 its child rows (sync or async). */\n getChildren: (parent: T) => C[] | Promise<C[]>;\n /** Side-panel size when `mode === 'side-panel'`. Default 'medium'. */\n panelSize?: NestedSidePanelSize;\n /** Hover delay (ms) when `mode === 'hover-callout'`. Default 300. */\n hoverDelay?: number;\n /** Max rows shown in the hover-callout preview. Default 5. */\n calloutMaxRows?: number;\n /** Trigger label (supports the `{count}` token). Default \"View {count} related\". */\n triggerLabel?: string;\n /** Fluent icon name for the trigger. Default 'OpenPaneMirrored'. */\n triggerIcon?: string;\n /** Registry for the child grid; defaults to the parent's registry. */\n childRegistry?: GridRegistry;\n\n // \u2500\u2500 parent layout \u2500\u2500\n /**\n * How parent rows render. `'rows'` (default) = DetailsList/DataGrid rows per\n * `mode`. `'cards'` = each parent is a card that expands IN PLACE to reveal its\n * child grid (`<NestedCardParent>`); `mode` is ignored for card parents.\n */\n parentLayout?: 'rows' | 'cards';\n /** Card layout config when `parentLayout === 'cards'` (reuses `CardConfig`). */\n parentCard?: CardConfig;\n\n // \u2500\u2500 child grid selection / editing (inline + side-panel; not hover-callout) \u2500\u2500\n /** Selection mode for the child grid. Default 'none'. */\n childSelectionMode?: GridSelectionMode;\n /**\n * Gates child-row selection relative to the parent (mirrors form-runtime's\n * `nestedSelectionMode`). `'independent'` (default) = children selectable\n * regardless of parent. `'requires-parent'` = a parent's child grid is only\n * selectable while that parent row is selected (inline mode; requires the\n * parent grid to be selectable, i.e. `selectionMode` \u2260 'none'). De-selecting\n * then re-selecting a parent restores that child grid's prior selection (the\n * child keeps its own selection state while gated off).\n */\n childSelectionGating?: 'independent' | 'requires-parent';\n /** Fires with the selected child rows + their parent. */\n onChildSelectionChanged?: (parent: T, selected: C[]) => void;\n /** Make the child grid editable. */\n childEditable?: boolean;\n /** Edit activation for the child grid. Default 'click'. */\n childEditTrigger?: 'click' | 'always';\n /** Bubbled value-change from a child cell (with its parent row). */\n onChildValueChange?: (\n parent: T,\n rowKey: string,\n fieldName: string,\n value: unknown,\n originalValue: unknown\n ) => void;\n}\n\nexport interface NestedGridProps<T = Record<string, unknown>, C = Record<string, unknown>>\n extends GridProps<T> {\n nested: NestedConfig<T, C>;\n}\n\n// \u2500\u2500\u2500 Focused-view / master-detail grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** A field reference in a focused-view summary row. Mirrors form-runtime `FocusedViewField`. */\nexport interface FocusedViewField {\n fieldName: string;\n label: string;\n linkedEntityAlias?: string;\n attributeType?: string;\n}\n\n/** A summary row in the focused-view left rail. Mirrors form-runtime `FocusedViewRow`. */\nexport interface FocusedViewRow {\n id: string;\n primaryField: FocusedViewField;\n secondaryField?: FocusedViewField;\n /** Fluent v8 icon name shown beside the row. */\n iconName?: string;\n}\n\n/** Focused-view config for `<FocusedViewGrid>`. Mirrors form-runtime `FocusedViewConfig`. */\nexport interface FocusedViewConfig {\n /** 1\u20134 summary rows for each left-rail item. */\n rows: FocusedViewRow[];\n /** Reserved (form-runtime parity) \u2014 show an \"up next\" activity slot. */\n showUpNextActivity?: boolean;\n /** Columns rendered in the right detail pane. Default: all columns. */\n detailFields?: string[];\n /**\n * Right-pane layout: when `true`, the pane renders ONLY the nested child grid \u2014\n * no record-title heading, no detail-field list, no \"Related\" subheading. Use\n * for a pure master/detail where the rail is the master and the pane is the\n * related-records grid (the export-engine focused-view shape). Requires `nested`.\n * Default (`false`/unset): the classic detail-field list, with the child grid\n * appended under a \"Related\" heading when a `detail-pane` nested config is given.\n */\n childOnly?: boolean;\n}\n\nexport interface FocusedViewGridProps<T = Record<string, unknown>> extends GridProps<T> {\n focusedView: FocusedViewConfig;\n /** Optional child grid rendered in the detail pane (nested `mode: 'detail-pane'`). */\n nested?: NestedConfig<T, Record<string, unknown>>;\n}\n",
|
|
12330
|
+
"src/lib/grid-kit/grid-kit/types/index.ts": "import type * as React from 'react';\nimport type { IContextualMenuProps } from '@fluentui/react';\nimport type { NavTarget } from '../navigation/navigateTo';\nimport type { GridFilterModel } from '../core/filter';\n\n/**\n * grid-kit canonical contract.\n *\n * grid-kit OWNS this vocabulary (it does not import form-runtime, which is a\n * sibling, zero-React, typecheck-standalone package). The literals are aligned\n * with both `@khester/dynamics-cell-renderers`' `ListFieldType` and\n * form-runtime's `CellRendererType` so a grid-customizer definition maps onto\n * these via `fromGridCustomizerDefinition()` without a runtime dependency.\n */\n\n// \u2500\u2500\u2500 Renderer & editor vocabularies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Cell renderer types. Superset of the canonical library + form-runtime. */\nexport type CellRendererType =\n | 'text'\n | 'link'\n | 'lookup'\n | 'optionset'\n | 'currency'\n | 'number'\n | 'date'\n | 'datetime'\n | 'boolean'\n | 'toggle'\n | 'icon'\n | 'progress'\n | 'coloredCell'\n | 'rating'\n | 'composite'\n | 'fileDrop';\n\n/** Inline editor types for editable grids. `'none'` (or undefined) = read-only. */\nexport type CellEditorType = 'text' | 'number' | 'date' | 'dropdown' | 'toggle' | 'lookup' | 'none';\n\nexport type GridSelectionMode = 'none' | 'single' | 'multiple';\n\n// \u2500\u2500\u2500 Column definition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * grid-kit's renderer-agnostic column. The host adapters convert this into the\n * shape each grid host needs (Fluent `IColumn.onRender` or a GridCustomizer FC).\n */\nexport interface ColumnDef<T = Record<string, unknown>> {\n /** Stable column key. */\n key: string;\n /** Logical attribute / row field this column reads. */\n fieldName: string;\n /** Header text. */\n name: string;\n\n /**\n * Which renderer to use. A built-in `CellRendererType`, or any custom string\n * registered via `registry.register(type, \u2026)`. The `(string & {})` keeps\n * autocomplete for the built-ins while allowing custom types without a cast.\n */\n // eslint-disable-next-line @typescript-eslint/ban-types -- `string & {}` is the idiom that keeps literal autocomplete\n rendererType: CellRendererType | (string & {});\n /**\n * Type-specific renderer config. Shape depends on `rendererType` \u2014 e.g.\n * progress \u2192 { max, color, showLabel }\n * currency \u2192 { currencyCode, locale, decimals }\n * coloredCell \u2192 { colorMap, getBackgroundColor }\n * optionset \u2192 { options: [{ value, label, color }] }\n * rating \u2192 { max, color, allowHalf }\n * composite \u2192 CompositeRendererConfig (layout/gap/slots)\n * lookup \u2192 { entitySetName, searchField, targetEntity, \u2026 }\n */\n rendererConfig?: Record<string, unknown>;\n\n /** Inline editor type (editable grids). Undefined/`'none'` \u21D2 read-only. */\n editorType?: CellEditorType;\n /** Locked even when the grid is editable (computed/permission-gated fields). */\n isLocked?: boolean;\n\n // sizing / behavior\n width?: number;\n minWidth?: number;\n maxWidth?: number;\n isResizable?: boolean;\n isSortable?: boolean;\n isFilterable?: boolean;\n\n /** Footer aggregate for this (numeric) column. Rendered in the grid footer row. */\n aggregate?: 'sum' | 'avg' | 'count' | 'min' | 'max';\n /**\n * Custom format for the aggregate footer cell. Default: `count` \u2192 integer;\n * `sum`/`avg`/`min`/`max` \u2192 `computeFormattedValue` by `rendererType` (so a\n * currency column's sum shows as currency); `null` \u2192 blank.\n */\n aggregateFormat?: (value: number | null, fn: 'sum' | 'avg' | 'count' | 'min' | 'max') => string;\n\n // callbacks\n /** Link/record-link click handler. */\n onLinkClick?: (item: T) => void;\n /** Per-column value-change handler (4-arg, matches the editable cells). */\n onValueChange?: (rowKey: string, fieldName: string, value: unknown, originalValue: unknown) => void;\n /** Edit-mode validation \u2014 returns an error message or undefined. */\n validate?: (value: unknown, rowKey: string) => string | undefined;\n\n // hover callout parity (preserves Track-and-Ship behavior on migration)\n /**\n * Custom callout content shown on hover over the cell (e.g. a lookup's\n * related-record preview). Consumed by the DetailsList host (`toDetailsListColumns`\n * \u2192 `CalloutCell`); ignored when `onRender` (the escape hatch) is set, and not yet\n * wired on the Grid Customizer host.\n */\n calloutContent?: (item: T) => React.ReactNode;\n /** Callout content shown on icon hover (icon columns). */\n onIconMouseEnter?: (item: T) => React.ReactNode;\n /** How the callout dismisses. */\n dismissMode?: 'mouseleave' | 'timeout';\n /** Auto-dismiss duration (ms) when `dismissMode === 'timeout'`. */\n timeoutDuration?: number;\n\n /** Escape hatch: fully custom cell render, bypassing the registry. */\n onRender?: (item: T) => React.ReactNode;\n}\n\n// \u2500\u2500\u2500 The keystone cell contract \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * The single normalized cell-render contract every grid-kit renderer speaks.\n * A superset union of the three host shapes (DetailsList `onRender(item,idx,col)`,\n * GridCustomizer `FC<CellRendererProps>`, and the canonical lib's\n * `Cell({item,column,value,itemKey})`). Adapters build this per cell; renderers\n * never reach back into a host.\n */\nexport interface GridCellProps<T = Record<string, unknown>> {\n /** Raw field value. */\n value: unknown;\n /** Display string \u2014 host-supplied (Dataverse formatted value) or computed fallback. */\n formattedValue: string;\n /**\n * Only set when the value was supplied by the host/platform (e.g. Dataverse\n * `@OData\u2026FormattedValue`), NOT when `formattedValue` is a computed fallback.\n * Renderers use this to honor a real optionset/lookup label over the raw code.\n */\n suppliedFormattedValue?: string;\n /** Full row. */\n item: T;\n /** Row key (for change callbacks). */\n itemKey: string;\n /** Column definition. */\n column: ColumnDef<T>;\n\n // edit-state (read-only renderers ignore these)\n isEditing?: boolean;\n isDirty?: boolean;\n editedValue?: unknown;\n errorMessage?: string;\n /** 4-arg change signal, matching the canonical editable cells. */\n onValueChange?: (rowKey: string, fieldName: string, value: unknown, originalValue: unknown) => void;\n onValidationError?: (rowKey: string, fieldName: string, message: string | undefined) => void;\n onEditStart?: () => void;\n onEditEnd?: () => void;\n}\n\n/** A grid-kit cell renderer is a component (rendered as an element, never called). */\nexport type GridCellRenderer<T = Record<string, unknown>> = React.ComponentType<GridCellProps<T>>;\n\n// \u2500\u2500\u2500 Registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** Read + optional edit renderer pair for a cell type. */\nexport interface CellRegistration<T = Record<string, unknown>> {\n read: GridCellRenderer<T>;\n edit?: GridCellRenderer<T>;\n}\n\n/** The cell-renderer registry shared across both grid hosts. */\nexport interface GridRegistry {\n /** Look up a registration (built-in or custom). */\n get(type: CellRendererType | string): CellRegistration | undefined;\n /** Register or override a cell type (built-in override or brand-new type). */\n register(type: CellRendererType | string, reg: CellRegistration): void;\n /** Resolve the renderer for a column, honoring read-vs-edit. */\n resolve<T = Record<string, unknown>>(column: ColumnDef<T>, editable: boolean): GridCellRenderer<T>;\n}\n\n// \u2500\u2500\u2500 Grid host props \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface GridSortState {\n fieldName: string;\n direction: 'asc' | 'desc';\n}\n\nexport interface GridPaginationState {\n currentPage: number;\n pageSize: number;\n totalItems: number;\n}\n\nexport interface GridCommand {\n key: string;\n text: string;\n iconName?: string;\n onClick?: () => void;\n disabled?: boolean;\n /** Render as a split button (primary `onClick` + a `subMenuProps` dropdown). */\n split?: boolean;\n /** Dropdown menu for split/menu commands (e.g. the export CSV/JSON choice). */\n subMenuProps?: IContextualMenuProps;\n}\n\n/**\n * A per-row command shown in the right-click context menu (`GridProps.rowCommands`).\n *\n * Intentionally NOT a `GridCommand`: that is a toolbar command with a zero-arg,\n * optional `onClick`, a boolean `disabled`, and `split`/`subMenuProps` (meaningless\n * per-row). A `RowCommand` is item-aware \u2014 `onClick(item)` and the `visible`/`disabled`\n * predicates all receive the row. `onClick` takes only the item (no positional index):\n * resolve the row off the item itself (e.g. a stamped `__rowIndex`) so callers can't\n * mis-key on a display index.\n */\nexport interface RowCommand<T = Record<string, unknown>> {\n key: string;\n text: string;\n iconName?: string;\n onClick: (item: T) => void;\n /** Hide this command for a row when it returns `false`. Default: shown. */\n visible?: (item: T) => boolean;\n /** Disable (grey out) this command for a row when it returns `true`. Default: enabled. */\n disabled?: (item: T) => boolean;\n}\n\n/** Turnkey CSV/JSON export config (a toolbar split-button over the host's items). */\nexport interface GridExportConfig {\n /** Show the Export split-button in the toolbar. */\n enabled?: boolean;\n /** Base filename (a date suffix is added). Default 'grid-export'. */\n filename?: string;\n /** Formats offered in the split-button menu. Default ['csv', 'json']. */\n formats?: Array<'csv' | 'json'>;\n /**\n * `'all'` always exports every row in `items`. `'selected'` exports only the\n * current selection (requires `selectionMode` \u2260 'none'; no-ops when nothing is\n * selected). Default (unset): the selection when non-empty, else all `items`.\n */\n scope?: 'all' | 'selected';\n /**\n * Open the `GridExportDialog` (format + column picker + filename) instead of\n * exporting immediately. The split-button becomes a single \"Export\u2026\" button.\n */\n dialog?: boolean;\n}\n\nexport interface GridToolbarConfig {\n showSearch?: boolean;\n searchPlaceholder?: string;\n onSearch?: (term: string) => void;\n commands?: GridCommand[];\n}\n\n/** Whole-row file-drop config (HTML5 drop of OS files onto a row). */\nexport interface RowFileDropConfig<T = Record<string, unknown>> {\n /** Called with the row + the accepted dropped files. */\n onDrop: (item: T, files: File[]) => void;\n /** Allowed extensions (e.g. `['.pdf', '.png']`). Empty/undefined = any. */\n accept?: string[];\n /** Allow multiple files (default false \u2192 first accepted file only). */\n multiple?: boolean;\n /** Called when a drop is rejected (wrong type / no files). */\n onReject?: (item: T, reason: string) => void;\n}\n\nexport interface GridProps<T = Record<string, unknown>> {\n items: T[];\n columns: ColumnDef<T>[];\n /** Defaults to `createCellRegistry()`. */\n registry?: GridRegistry;\n selectionMode?: GridSelectionMode;\n /** Fires with the currently selected rows (requires `selectionMode` \u2260 'none'). */\n onSelectionChanged?: (selectedItems: T[]) => void;\n /**\n * Enables inline editing for columns whose `editorType` is set (and not\n * `'none'`/locked). By default editing is **per-cell click-to-edit**\n * (`editTrigger: 'click'`) \u2014 a cell shows read text until clicked, then its\n * inline editor. Set `editTrigger: 'always'` for an Excel-like grid where\n * every editable cell shows its editor at once.\n */\n editable?: boolean;\n /** Edit activation model when `editable`. Default `'click'`. */\n editTrigger?: 'click' | 'always';\n isLoading?: boolean;\n sort?: GridSortState;\n onSortChange?: (fieldName: string, direction: 'asc' | 'desc') => void;\n /**\n * Presentational pager only \u2014 the consumer MUST pre-slice `items` to the\n * current page. The grid renders exactly what is in `items`; it does not\n * page internally.\n */\n pagination?: GridPaginationState;\n onPageChange?: (page: number) => void;\n /**\n * Rows the aggregate footer computes over. Defaults to the displayed (filtered)\n * rows \u2014 i.e. the current page when the consumer pre-paginates. Pass the FULL\n * dataset here for cross-page grand totals; an active filter (`filterBuilder`) is\n * applied to it too, so the total reflects what the filter shows across all pages.\n * Note: on `<GroupedGrid>`, per-group subtotals (`showGroupAggregates`) stay\n * page-scoped \u2014 only this overall footer goes cross-page \u2014 so they intentionally\n * need not sum to the grand total.\n */\n aggregateItems?: T[];\n toolbar?: GridToolbarConfig;\n /** Row-key accessor (default: `item.key`). */\n getKey?: (item: T) => string;\n /** Bubbled value-change from any editable cell. */\n onValueChange?: (rowKey: string, fieldName: string, value: unknown, originalValue: unknown) => void;\n onActiveItemChanged?: (item: T, index?: number) => void;\n /** Compact row height. */\n compact?: boolean;\n /**\n * Fill the host's container: the grid root becomes a flex column at `height`\n * (default 100%) and the list/cards scroll internally, so the toolbar + footers\n * stay fixed. Default off (natural height). For dropping a grid into a surface-kit\n * Section / Dialog / Panel that owns the available height.\n *\n * Requirements / limits:\n * - The container MUST have a resolved height (a flex/grid parent or an explicit\n * height) \u2014 `height: '100%'` against an auto-height parent renders nothing.\n * - v1: the DetailsList column header scrolls with the body (not pinned).\n * - Honored by side-panel / hover-callout nested modes (via DataGrid), but NOT by\n * `<NestedGrid mode=\"inline\">` (inline child height is driven by row expansion).\n */\n fill?: boolean;\n /** Explicit grid height (number = px, or any CSS value). With `fill`, defaults to '100%'. */\n height?: number | string;\n /** Make each row a file-drop target (OS files \u2192 `onDrop(item, files)`). */\n rowFileDrop?: RowFileDropConfig<T>;\n /**\n * Adds a turnkey Export (CSV/JSON) split-button to the toolbar over the host's\n * current `items` (or the selection when non-empty). Renders the toolbar even\n * if `toolbar` is undefined.\n */\n exportConfig?: GridExportConfig;\n /**\n * Zebra-stripe rows with the given even/odd row background colors. Composes\n * with `rowFileDrop` (both share the DetailsList `onRenderRow`). No-op on\n * CardGrid (no rows). Mirrors form-runtime's `alternateRowColors`.\n */\n alternateRowColors?: { even: string; odd: string };\n /**\n * Resolve a row \u2192 a navigation target. When set, `link`/`lookup` cells open it\n * on click and a toolbar \"Open\" command navigates the active/selected row.\n * Executed via `Xrm.Navigation` (no-op + console on localhost / no Xrm).\n */\n navigateTo?: (item: T) => NavTarget | undefined;\n /** Row-level navigation hook, wired to DetailsList `onItemInvoked` (double-click / Enter). */\n onRowNavigate?: (item: T) => void;\n /**\n * Per-row right-click context menu. When set (and non-empty), right-clicking a row\n * opens a Fluent v8 `ContextualMenu` of these commands (the native browser menu is\n * suppressed). When unset/empty the native menu is left intact (strict no-op for\n * existing consumers). Honored by every host: the DetailsList-backed grids (`DataGrid`,\n * `GroupedGrid`, and `<NestedGrid>`'s inline/side-panel/hover-callout/detail-pane\n * parents), the card grids (flat `CardGrid` and the `NestedCardParent` card parents,\n * per-card right-click), and the `FocusedViewGrid` rail.\n */\n rowCommands?: RowCommand<T>[];\n /**\n * Adds a \"Columns\" toolbar command that opens a column-chooser panel\n * (show/hide + reorder). The grid manages the visible set internally; opt-in.\n */\n columnChooser?: boolean;\n /** Notified with the new visible column keys (in order) when the chooser applies. */\n onColumnOrderChange?: (visibleOrder: string[]) => void;\n /**\n * Adds a \"Filters\" toolbar command that opens a filter-builder panel (field +\n * operator + value conditions combined with All/Any). The grid filters its\n * in-memory `items` by the built model \u2014 the list, aggregate footer, and export\n * scope all reflect the filter; opt-in. Operates on the passed `items`, so pair\n * it with a client-side (non-paginated) dataset, like the aggregate footer; for\n * server-side filtering, read `onFilterChange` and re-query.\n */\n filterBuilder?: boolean;\n /** Notified with the applied filter model when the filter panel applies. */\n onFilterChange?: (model: GridFilterModel) => void;\n}\n\n// \u2500\u2500\u2500 Read-Only grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Read-only grid: `<DataGrid>` with selection off, editing off, and command-bar\n * commands stripped. A thin preset \u2014 no extra config.\n */\nexport type ReadOnlyGridProps<T = Record<string, unknown>> = Omit<\n GridProps<T>,\n 'selectionMode' | 'editable' | 'editTrigger' | 'onSelectionChanged' | 'onValueChange'\n>;\n\n// \u2500\u2500\u2500 Card grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Card-view layout config. Mirrors form-runtime's `GridCardViewConfig` (minus\n * its `enabled` flag \u2014 choosing `<CardGrid>` IS \"enabled\").\n */\nexport interface CardConfig {\n /** Cards per row (CSS grid columns). Default 3. */\n cardsPerRow?: number;\n /** Fixed card height in px (otherwise content-sized). */\n cardHeight?: number;\n /** Column whose value is the card title. Default: first column. */\n titleField?: string;\n /** Column whose value is the card subtitle. */\n subtitleField?: string;\n /** Column whose value is an image URL shown as a card thumbnail. */\n imageField?: string;\n /**\n * Columns rendered (label + value) in the card body. Default: all columns\n * except the title/subtitle/image fields.\n */\n bodyFields?: string[];\n}\n\nexport interface CardGridProps<T = Record<string, unknown>> extends Omit<GridProps<T>, 'compact'> {\n card?: CardConfig;\n}\n\n// \u2500\u2500\u2500 Grouped grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Grouping config for `<GroupedGrid>`. grid-kit-originated (form-runtime has no\n * `groupBy` field yet); maps onto Fluent v8 DetailsList `IGroup[]`.\n */\nexport interface GroupConfig<T = Record<string, unknown>> {\n /** Field whose value buckets rows into groups. */\n groupBy: string;\n /** Custom header label; default is the formatted group value. */\n getGroupLabel?: (groupValue: unknown, items: T[]) => string;\n /** Start groups collapsed. Default false. */\n collapsedByDefault?: boolean;\n /** Show a per-group count in the header. Default true. */\n showCount?: boolean;\n /**\n * Render a per-group subtotal footer row (aggregating each group's rows for the\n * columns that declared an `aggregate`). Default false. The overall footer below\n * the grid is unaffected. Subtotals always aggregate the displayed (page) rows of\n * each group \u2014 only the overall footer goes cross-page via `aggregateItems`.\n */\n showGroupAggregates?: boolean;\n}\n\nexport interface GroupedGridProps<T = Record<string, unknown>> extends GridProps<T> {\n group: GroupConfig<T>;\n}\n\n// \u2500\u2500\u2500 Nested / expandable grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * How a parent row surfaces its child records. Mirrors form-runtime's\n * `NestedDisplayMode`.\n * - `inline` \u2014 chevron expands the child grid under the parent row\n * - `detail-pane` \u2014 child grid renders in a master-detail pane (FocusedViewGrid)\n * - `side-panel` \u2014 a trigger opens a Fluent Panel hosting the child grid\n * - `hover-callout` \u2014 hovering a count badge shows a compact callout of top-N rows\n */\nexport type NestedDisplayMode = 'inline' | 'detail-pane' | 'side-panel' | 'hover-callout';\n\n/** Side-panel size (maps to Fluent v8 `PanelType`). */\nexport type NestedSidePanelSize = 'small' | 'medium' | 'large';\n\n/**\n * Nested-grid config for `<NestedGrid>`. grid-kit never fetches \u2014 the consumer\n * supplies `getChildren(parent)` (sync array or promise); it owns the\n * relationship/FetchXML. The child grid is itself a grid-kit grid.\n */\nexport interface NestedConfig<T = Record<string, unknown>, C = Record<string, unknown>> {\n /** How children surface. */\n mode: NestedDisplayMode;\n /** Child grid columns. */\n childColumns: ColumnDef<C>[];\n /** Resolve a parent row \u2192 its child rows (sync or async). */\n getChildren: (parent: T) => C[] | Promise<C[]>;\n /**\n * Bump to invalidate + re-fetch already-loaded children (e.g. after associating a\n * new child to a parent). Caching hosts (`useChildren` / `NestedTriggerCell`) re-run\n * `getChildren` for loaded parents in place \u2014 old rows stay visible until the new\n * ones arrive. Leave unset for a static list.\n */\n reloadToken?: string | number;\n /** Side-panel size when `mode === 'side-panel'`. Default 'medium'. */\n panelSize?: NestedSidePanelSize;\n /** Hover delay (ms) when `mode === 'hover-callout'`. Default 300. */\n hoverDelay?: number;\n /** Max rows shown in the hover-callout preview. Default 5. */\n calloutMaxRows?: number;\n /** Trigger label (supports the `{count}` token). Default \"View {count} related\". */\n triggerLabel?: string;\n /** Fluent icon name for the trigger. Default 'OpenPaneMirrored'. */\n triggerIcon?: string;\n /** Registry for the child grid; defaults to the parent's registry. */\n childRegistry?: GridRegistry;\n\n // \u2500\u2500 parent layout \u2500\u2500\n /**\n * How parent rows render. `'rows'` (default) = DetailsList/DataGrid rows per\n * `mode`. `'cards'` = each parent is a card that expands IN PLACE to reveal its\n * child grid (`<NestedCardParent>`); `mode` is ignored for card parents.\n */\n parentLayout?: 'rows' | 'cards';\n /** Card layout config when `parentLayout === 'cards'` (reuses `CardConfig`). */\n parentCard?: CardConfig;\n\n // \u2500\u2500 child grid selection / editing (inline + side-panel; not hover-callout) \u2500\u2500\n /** Selection mode for the child grid. Default 'none'. */\n childSelectionMode?: GridSelectionMode;\n /**\n * Gates child-row selection relative to the parent (mirrors form-runtime's\n * `nestedSelectionMode`). `'independent'` (default) = children selectable\n * regardless of parent. `'requires-parent'` = a parent's child grid is only\n * selectable while that parent row is selected (inline mode; requires the\n * parent grid to be selectable, i.e. `selectionMode` \u2260 'none'). De-selecting\n * then re-selecting a parent restores that child grid's prior selection (the\n * child keeps its own selection state while gated off).\n */\n childSelectionGating?: 'independent' | 'requires-parent';\n /** Fires with the selected child rows + their parent. */\n onChildSelectionChanged?: (parent: T, selected: C[]) => void;\n /** Make the child grid editable. */\n childEditable?: boolean;\n /** Edit activation for the child grid. Default 'click'. */\n childEditTrigger?: 'click' | 'always';\n /** Bubbled value-change from a child cell (with its parent row). */\n onChildValueChange?: (\n parent: T,\n rowKey: string,\n fieldName: string,\n value: unknown,\n originalValue: unknown\n ) => void;\n}\n\nexport interface NestedGridProps<T = Record<string, unknown>, C = Record<string, unknown>>\n extends GridProps<T> {\n nested: NestedConfig<T, C>;\n}\n\n// \u2500\u2500\u2500 Focused-view / master-detail grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** A field reference in a focused-view summary row. Mirrors form-runtime `FocusedViewField`. */\nexport interface FocusedViewField {\n fieldName: string;\n label: string;\n linkedEntityAlias?: string;\n attributeType?: string;\n}\n\n/** A summary row in the focused-view left rail. Mirrors form-runtime `FocusedViewRow`. */\nexport interface FocusedViewRow {\n id: string;\n primaryField: FocusedViewField;\n secondaryField?: FocusedViewField;\n /** Fluent v8 icon name shown beside the row. */\n iconName?: string;\n}\n\n/** Focused-view config for `<FocusedViewGrid>`. Mirrors form-runtime `FocusedViewConfig`. */\nexport interface FocusedViewConfig {\n /** 1\u20134 summary rows for each left-rail item. */\n rows: FocusedViewRow[];\n /** Reserved (form-runtime parity) \u2014 show an \"up next\" activity slot. */\n showUpNextActivity?: boolean;\n /** Columns rendered in the right detail pane. Default: all columns. */\n detailFields?: string[];\n /**\n * Right-pane layout: when `true`, the pane renders ONLY the nested child grid \u2014\n * no record-title heading, no detail-field list, no \"Related\" subheading. Use\n * for a pure master/detail where the rail is the master and the pane is the\n * related-records grid (the export-engine focused-view shape). Requires `nested`.\n * Default (`false`/unset): the classic detail-field list, with the child grid\n * appended under a \"Related\" heading when a `detail-pane` nested config is given.\n */\n childOnly?: boolean;\n}\n\nexport interface FocusedViewGridProps<T = Record<string, unknown>> extends GridProps<T> {\n focusedView: FocusedViewConfig;\n /** Optional child grid rendered in the detail pane (nested `mode: 'detail-pane'`). */\n nested?: NestedConfig<T, Record<string, unknown>>;\n}\n",
|
|
12475
12331
|
"src/lib/grid-kit/index.ts": "// Vendored @dataverse-kit/grid-kit barrel. GENERATED \u2014 do not edit.\nexport * from './grid-kit';\n"
|
|
12476
12332
|
};
|
|
12477
12333
|
function gridKitVendorFiles(prefix = "") {
|
|
@@ -13324,11 +13180,14 @@ function buildNestedSubgridBody(args) {
|
|
|
13324
13180
|
handlerBlock,
|
|
13325
13181
|
deleteDialogJsx,
|
|
13326
13182
|
createDialogJsx,
|
|
13183
|
+
emitChildReloadToken,
|
|
13327
13184
|
hasSelection,
|
|
13328
13185
|
hasHandler,
|
|
13329
13186
|
dataverseHooks,
|
|
13330
13187
|
commandBarImports
|
|
13331
13188
|
} = args;
|
|
13189
|
+
const childReloadTokenState = emitChildReloadToken ? "\n const [childReloadToken, setChildReloadToken] = React.useState(0);" : "";
|
|
13190
|
+
const reloadTokenProp = emitChildReloadToken ? "\n reloadToken: childReloadToken," : "";
|
|
13332
13191
|
const wireChildSaveBack = childCommitBlock.length > 0;
|
|
13333
13192
|
const getChildrenExpr = useLiveNested && live ? buildLiveGetChildren(componentName, live, wireChildSaveBack, childPrimaryIdAttribute) : buildMockGetChildren(childByParentLiteral);
|
|
13334
13193
|
const paginationProp = parentPaginationMode && parentPaginationMode !== "none" ? `
|
|
@@ -13352,7 +13211,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
|
|
|
13352
13211
|
const keyedItems = React.useMemo(
|
|
13353
13212
|
() => items.map((r, i) => ({ ...r, __rowIndex: (r.__rowIndex as number | undefined) ?? i })),
|
|
13354
13213
|
[items],
|
|
13355
|
-
);${selectedIndicesState}${handlerBlock}${childCommitBlock}
|
|
13214
|
+
);${selectedIndicesState}${childReloadTokenState}${handlerBlock}${childCommitBlock}
|
|
13356
13215
|
const getChildren = React.useCallback(${getChildrenExpr}, []);
|
|
13357
13216
|
return (
|
|
13358
13217
|
<div>${commandBarBlock}
|
|
@@ -13369,7 +13228,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
|
|
|
13369
13228
|
childColumns: [
|
|
13370
13229
|
${childColEntries}
|
|
13371
13230
|
] as ColumnDef[],
|
|
13372
|
-
getChildren
|
|
13231
|
+
getChildren,${reloadTokenProp}
|
|
13373
13232
|
childRegistry: registry,
|
|
13374
13233
|
childSelectionMode: ${JSON.stringify(childSelectionMode)},
|
|
13375
13234
|
childSelectionGating: ${JSON.stringify(childSelectionGating)},
|
|
@@ -13423,6 +13282,7 @@ function buildFocusedViewSubgridBody(args) {
|
|
|
13423
13282
|
handlerBlock,
|
|
13424
13283
|
deleteDialogJsx,
|
|
13425
13284
|
createDialogJsx,
|
|
13285
|
+
emitChildReloadToken,
|
|
13426
13286
|
hasSelection,
|
|
13427
13287
|
hasHandler,
|
|
13428
13288
|
dataverseHooks,
|
|
@@ -13432,6 +13292,8 @@ function buildFocusedViewSubgridBody(args) {
|
|
|
13432
13292
|
parentDataLiteral,
|
|
13433
13293
|
childByParentLiteral
|
|
13434
13294
|
} = args;
|
|
13295
|
+
const childReloadTokenState = emitChildReloadToken ? "\n const [childReloadToken, setChildReloadToken] = React.useState(0);" : "";
|
|
13296
|
+
const reloadTokenProp = emitChildReloadToken ? "\n reloadToken: childReloadToken," : "";
|
|
13435
13297
|
const wireChildSaveBack = childCommitBlock.length > 0;
|
|
13436
13298
|
const getChildrenExpr = useLiveNested && live ? buildLiveGetChildren(componentName, live, wireChildSaveBack, childPrimaryIdAttribute) : buildMockGetChildren(childByParentLiteral);
|
|
13437
13299
|
const selectedIndicesState = hasSelection || hasHandler ? "\n const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(\n () => new Set<number>(keyedItems.length ? [0] : []),\n );" : "";
|
|
@@ -13451,7 +13313,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
|
|
|
13451
13313
|
const keyedItems = React.useMemo(
|
|
13452
13314
|
() => items.map((r, i) => ({ ...r, __rowIndex: (r.__rowIndex as number | undefined) ?? i })),
|
|
13453
13315
|
[items],
|
|
13454
|
-
);${selectedIndicesState}${handlerBlock}${childCommitBlock}
|
|
13316
|
+
);${selectedIndicesState}${childReloadTokenState}${handlerBlock}${childCommitBlock}
|
|
13455
13317
|
const getChildren = React.useCallback(${getChildrenExpr}, []);
|
|
13456
13318
|
return (
|
|
13457
13319
|
<div>${commandBarBlock}
|
|
@@ -13471,7 +13333,7 @@ const ${componentName}: React.FC<{ items: Record<string, unknown>[] }> = ({ item
|
|
|
13471
13333
|
childColumns: [
|
|
13472
13334
|
${childColEntries}
|
|
13473
13335
|
] as ColumnDef[],
|
|
13474
|
-
getChildren
|
|
13336
|
+
getChildren,${reloadTokenProp}
|
|
13475
13337
|
childRegistry: registry,
|
|
13476
13338
|
childSelectionMode: ${JSON.stringify(childSelectionMode)},
|
|
13477
13339
|
childSelectionGating: ${JSON.stringify(childSelectionGating)},${childEditableLiteral}
|
|
@@ -14449,6 +14311,10 @@ function buildGridCommandHandlerBlock(args) {
|
|
|
14449
14311
|
await gridAddExistingMutation.mutateAsync({ id: childId, data: { '${addExisting.childField}@odata.bind': '/${addExisting.parentEntitySet}(' + parentId + ')' } as Record<string, unknown> });
|
|
14450
14312
|
}
|
|
14451
14313
|
await refreshGrid();
|
|
14314
|
+
// Re-fetch the parent's expanded child list in place \u2014 the associated child won't
|
|
14315
|
+
// appear otherwise (getChildren isn't React-Query-keyed). The setter is declared by
|
|
14316
|
+
// the nested/focused-view emitter whenever addExisting is wired (see emitChildReloadToken).
|
|
14317
|
+
setChildReloadToken((v) => v + 1);
|
|
14452
14318
|
} else {
|
|
14453
14319
|
console.warn('[grid] Add Existing requires Xrm.Utility.lookupObjects; not available in this host.');
|
|
14454
14320
|
}
|
|
@@ -14478,9 +14344,10 @@ function buildGridCommandHandlerBlock(args) {
|
|
|
14478
14344
|
if (!pendingDelete || pendingDelete.length === 0) return;
|
|
14479
14345
|
setDeleteError(null);
|
|
14480
14346
|
try {
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
|
|
14347
|
+
// Delete the selected rows concurrently (was a serial await-loop). Not atomic: a mid-batch
|
|
14348
|
+
// failure can leave some rows already deleted; Promise.all rejects on the first failure and
|
|
14349
|
+
// the message surfaces below. Use a $batch changeset if you need all-or-nothing.
|
|
14350
|
+
await Promise.all(pendingDelete.map((id) => gridDeleteMutation.mutateAsync(id)));
|
|
14484
14351
|
setPendingDelete(null);
|
|
14485
14352
|
} catch (err) {
|
|
14486
14353
|
setDeleteError((err as Error)?.message ?? 'Delete failed.');
|
|
@@ -14535,11 +14402,14 @@ function buildGridCommandHandlerBlock(args) {
|
|
|
14535
14402
|
case 'activate':
|
|
14536
14403
|
case 'deactivate': {
|
|
14537
14404
|
if (!requireSel(actionType)) return;
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14405
|
+
// Set only statecode (0=Active / 1=Inactive \u2014 standard across (nearly) all entities) and
|
|
14406
|
+
// let Dataverse apply the entity's DEFAULT status reason for that state. Hardcoding
|
|
14407
|
+
// statuscode 1/2 broke any entity with custom status reasons (400, or a wrong-but-existing
|
|
14408
|
+
// reason). NOTE: opportunity/lead/incident need dedicated messages (Win/Qualify/Close) \u2014 a
|
|
14409
|
+
// plain statecode Update never worked for those (no change here).
|
|
14410
|
+
const statecode = actionType === 'activate' ? 0 : 1;
|
|
14541
14411
|
for (const id of selectedIds) {
|
|
14542
|
-
await gridUpdateMutation.mutateAsync({ id, data: { statecode
|
|
14412
|
+
await gridUpdateMutation.mutateAsync({ id, data: { statecode } as any });
|
|
14543
14413
|
}
|
|
14544
14414
|
return;
|
|
14545
14415
|
}
|
|
@@ -15189,6 +15059,12 @@ ${childEntries.join(",\n")},
|
|
|
15189
15059
|
const ngHasContextMenu = contextMenuItems.some(
|
|
15190
15060
|
(ci) => (ci.actionType ?? "custom") !== "custom"
|
|
15191
15061
|
);
|
|
15062
|
+
const ngAddExisting = enableAddExisting && childEntitySet && childEntityLogical && parentEntitySet && nestedRel?.childField ? {
|
|
15063
|
+
childEntitySet,
|
|
15064
|
+
childEntityLogical,
|
|
15065
|
+
childField: nestedRel.childField,
|
|
15066
|
+
parentEntitySet
|
|
15067
|
+
} : null;
|
|
15192
15068
|
const ngHandlerBlock = _handlerAvailable && (ngHasCommandBar || ngHasContextMenu && useLiveNested) ? buildGridCommandHandlerBlock({
|
|
15193
15069
|
entityName: gridDef.dataSource.entityName,
|
|
15194
15070
|
entitySetName: gridDef.dataSource.entitySetName,
|
|
@@ -15197,17 +15073,7 @@ ${childEntries.join(",\n")},
|
|
|
15197
15073
|
selectionExpr: "selectedIndices",
|
|
15198
15074
|
exportColumnsLiteral,
|
|
15199
15075
|
createColumnsLiteral,
|
|
15200
|
-
|
|
15201
|
-
// that wires it). Shared by the nested AND focused-view emitters below — both
|
|
15202
|
-
// reuse this ngHandlerBlock. Gated on enableAddExisting (feasible AND an
|
|
15203
|
-
// addExisting item present); the && chain narrows the optional entity-set strings
|
|
15204
|
-
// to `string` (enableAddExisting ⟹ addExistingFeasible guarantees they're present).
|
|
15205
|
-
addExisting: enableAddExisting && childEntitySet && childEntityLogical && parentEntitySet && nestedRel?.childField ? {
|
|
15206
|
-
childEntitySet,
|
|
15207
|
-
childEntityLogical,
|
|
15208
|
-
childField: nestedRel.childField,
|
|
15209
|
-
parentEntitySet
|
|
15210
|
-
} : null
|
|
15076
|
+
addExisting: ngAddExisting
|
|
15211
15077
|
}) : "";
|
|
15212
15078
|
const ngHasDelete = (gridDef.commandBarItems ?? []).some(
|
|
15213
15079
|
(ci) => ci.actionType === "delete"
|
|
@@ -15300,6 +15166,11 @@ ${childEntries.join(",\n")},
|
|
|
15300
15166
|
handlerBlock: ngHandlerBlock,
|
|
15301
15167
|
deleteDialogJsx: ngDeleteDialogJsx,
|
|
15302
15168
|
createDialogJsx: ngCreateDialogJsx,
|
|
15169
|
+
// Couple to ngHandlerBlock !== '' (like ngCreateDialogJsx/ngDeleteDialogJsx): the bump
|
|
15170
|
+
// lives in the dispatcher, so declare the state ONLY when the dispatcher is emitted —
|
|
15171
|
+
// else a bar-only addExisting on a showCommandBar:false grid would orphan the setter
|
|
15172
|
+
// (a noUnusedLocals build break in the PCF target).
|
|
15173
|
+
emitChildReloadToken: ngAddExisting !== null && ngHandlerBlock !== "",
|
|
15303
15174
|
hasSelection: ngHasCommandBar,
|
|
15304
15175
|
hasHandler: ngHandlerBlock !== "",
|
|
15305
15176
|
dataverseHooks: ngDataverseHooks,
|
|
@@ -15356,6 +15227,9 @@ ${childEntries.join(",\n")},
|
|
|
15356
15227
|
handlerBlock: ngHandlerBlock,
|
|
15357
15228
|
deleteDialogJsx: ngDeleteDialogJsx,
|
|
15358
15229
|
createDialogJsx: ngCreateDialogJsx,
|
|
15230
|
+
// See the focused-view call site: gate on ngHandlerBlock !== '' so the state is declared
|
|
15231
|
+
// only when the dispatcher (and thus the setChildReloadToken bump) is emitted.
|
|
15232
|
+
emitChildReloadToken: ngAddExisting !== null && ngHandlerBlock !== "",
|
|
15359
15233
|
hasSelection: ngHasCommandBar,
|
|
15360
15234
|
dataverseHooks: ngDataverseHooks,
|
|
15361
15235
|
commandBarImports: ngCommandBarImports,
|
|
@@ -25073,19 +24947,21 @@ export async function handleCommandClick(
|
|
|
25073
24947
|
case 'activate':
|
|
25074
24948
|
case 'deactivate': {
|
|
25075
24949
|
if (!requireSelection(ctx, item.actionType)) return;
|
|
25076
|
-
|
|
25077
|
-
|
|
25078
|
-
|
|
24950
|
+
// Set only statecode (0=Active / 1=Inactive); Dataverse applies the entity's default status
|
|
24951
|
+
// reason. Hardcoding statuscode 1/2 was wrong for entities with custom status reasons. The
|
|
24952
|
+
// bulk-edit form prefills statecode and lets the user pick the reason. (Same fix as the
|
|
24953
|
+
// subgrid dispatcher; opportunity/lead/incident still need Win/Qualify/Close \u2014 unchanged.)
|
|
24954
|
+
const statecode = item.actionType === 'activate' ? 0 : 1;
|
|
25079
24955
|
if (ctx.selectedIds.length > 1) {
|
|
25080
24956
|
await Xrm?.Navigation?.openBulkEditForm?.({
|
|
25081
24957
|
entityName,
|
|
25082
24958
|
entityIds: ctx.selectedIds,
|
|
25083
|
-
formParameters: { statecode
|
|
24959
|
+
formParameters: { statecode },
|
|
25084
24960
|
});
|
|
25085
24961
|
await ctx.refreshGrid();
|
|
25086
24962
|
return;
|
|
25087
24963
|
}
|
|
25088
|
-
await Xrm?.WebApi?.updateRecord?.(entityName, ctx.selectedIds[0], { statecode
|
|
24964
|
+
await Xrm?.WebApi?.updateRecord?.(entityName, ctx.selectedIds[0], { statecode });
|
|
25089
24965
|
await ctx.refreshGrid();
|
|
25090
24966
|
return;
|
|
25091
24967
|
}
|