@dataverse-kit/grid-kit 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2254,13 +2254,29 @@ const GROUP_CHEVRON_WIDTH = 36;
2254
2254
  * groups + count badges), reusing the same columns/registry as `<DataGrid>`.
2255
2255
  */
2256
2256
  function GroupedGrid(props) {
2257
- const { items, columns, group, selectionMode = 'none', isLoading, pagination, onPageChange, aggregateItems, toolbar, compact, alternateRowColors, fill, height, rowCommands } = props;
2257
+ const { items, columns, group, selectionMode = 'none', onSelectionChanged, isLoading, pagination, onPageChange, aggregateItems, toolbar, compact, alternateRowColors, fill, height, rowCommands } = props;
2258
2258
  const edit = useEditState();
2259
- const { ctx } = useGridContext(props, edit);
2259
+ const { ctx, getKeyFn } = useGridContext(props, edit);
2260
2260
  const dlColumns = React.useMemo(() => toDetailsListColumns(columns, ctx), [columns, ctx]);
2261
2261
  // Per-row right-click menu (DetailsList path, same as DataGrid/NestedInline). No-op when
2262
2262
  // rowCommands is unset (onItemContextMenu is undefined → no handler attached).
2263
2263
  const { onItemContextMenu, contextMenuElement } = useRowContextMenu(rowCommands);
2264
+ // Fluent Selection so a toolbar's selection-gated commands can read the checked rows via
2265
+ // onSelectionChanged. Ported verbatim from <DataGrid> (the grouped DetailsList supports
2266
+ // selection + groups together: the Selection is over the flattened `orderedItems`, and
2267
+ // group headers select-all per group). `getKey` (= the consumer's getKey via getKeyFn) keys
2268
+ // the selection per row; null-safe for enableShimmer placeholder rows. selectionMode='none'
2269
+ // (the default) passes no selection → display-only / per-row-menu-only grids are unchanged.
2270
+ const safeRowKey = (it, index) => it == null ? `__gridkit_shimmer_${index ?? 0}` : getKeyFn(it);
2271
+ const onSelectionChangedRef = React.useRef(onSelectionChanged);
2272
+ onSelectionChangedRef.current = onSelectionChanged;
2273
+ const selectionRef = React.useRef();
2274
+ if (!selectionRef.current) {
2275
+ selectionRef.current = new react.Selection({
2276
+ getKey: (it, index) => safeRowKey(it, index),
2277
+ onSelectionChanged: () => onSelectionChangedRef.current?.(selectionRef.current.getSelection()),
2278
+ });
2279
+ }
2264
2280
  // Bucket by groupBy → ordered items + IGroup[] (DetailsList needs contiguous
2265
2281
  // group ranges). See buildGroups (pure + unit-tested).
2266
2282
  const { orderedItems, groups } = React.useMemo(() => buildGroups(items, group), [items, group]);
@@ -2292,7 +2308,7 @@ function GroupedGrid(props) {
2292
2308
  });
2293
2309
  }
2294
2310
  : undefined, [alternateRowColors]);
2295
- return (jsxRuntime.jsxs(react.Stack, { styles: fillStackStyles(fill, height), children: [toolbar && jsxRuntime.jsx(GridToolbar, { config: toolbar }), jsxRuntime.jsx(FillRegion, { fill: fill, children: jsxRuntime.jsx(react.ShimmeredDetailsList, { items: orderedItems, columns: dlColumns, groups: groups, selectionMode: mapSelectionMode$1(selectionMode), layoutMode: react.DetailsListLayoutMode.justified, compact: compact, setKey: "grid-kit-grouped", onRenderRow: onRenderRow, onItemContextMenu: onItemContextMenu, styles: dynamicsCellRenderers.getDetailsListStyles(), ariaLabelForGrid: "Grouped grid", groupProps: {
2311
+ return (jsxRuntime.jsxs(react.Stack, { styles: fillStackStyles(fill, height), children: [toolbar && jsxRuntime.jsx(GridToolbar, { config: toolbar }), jsxRuntime.jsx(FillRegion, { fill: fill, children: jsxRuntime.jsx(react.ShimmeredDetailsList, { items: orderedItems, columns: dlColumns, groups: groups, selectionMode: mapSelectionMode$1(selectionMode), selection: selectionMode !== 'none' ? selectionRef.current : undefined, getKey: (it, index) => safeRowKey(it, index), layoutMode: react.DetailsListLayoutMode.justified, compact: compact, setKey: "grid-kit-grouped", onRenderRow: onRenderRow, onItemContextMenu: onItemContextMenu, styles: dynamicsCellRenderers.getDetailsListStyles(), ariaLabelForGrid: "Grouped grid", groupProps: {
2296
2312
  showEmptyGroups: false,
2297
2313
  // Render a subtotal under each EXPANDED group. Fluent v8 calls the group
2298
2314
  // footer renderer even for collapsed groups, so guard on isCollapsed —
@@ -2309,10 +2325,17 @@ function GroupedGrid(props) {
2309
2325
  * never fetches — this just normalizes the consumer-supplied resolver and tracks
2310
2326
  * loading. Call `ensure(key, parent)` on demand (row expand / panel open / hover).
2311
2327
  */
2312
- function useChildren(getChildren) {
2328
+ function useChildren(getChildren, reloadToken) {
2313
2329
  const [cache, setCache] = React.useState({});
2314
2330
  const cacheRef = React.useRef(cache);
2315
2331
  cacheRef.current = cache;
2332
+ // Remember the parent object per cached key so a `reloadToken` bump can re-run
2333
+ // `getChildren(parent)` for already-loaded rows.
2334
+ const parentsRef = React.useRef({});
2335
+ // `getChildren` accessed via a ref inside the reload effect → the effect can depend
2336
+ // on `reloadToken` alone without a stale closure or an exhaustive-deps warning.
2337
+ const getChildrenRef = React.useRef(getChildren);
2338
+ getChildrenRef.current = getChildren;
2316
2339
  // Guard against setState after unmount (e.g. host swapped while a child fetch
2317
2340
  // is in flight) — mirrors NestedTriggerCell / DetailPaneChildren.
2318
2341
  const mounted = React.useRef(true);
@@ -2325,6 +2348,7 @@ function useChildren(getChildren) {
2325
2348
  const ensure = React.useCallback((key, parent) => {
2326
2349
  if (cacheRef.current[key])
2327
2350
  return;
2351
+ parentsRef.current[key] = parent;
2328
2352
  const result = getChildren(parent);
2329
2353
  if (Array.isArray(result)) {
2330
2354
  setCache((p) => ({ ...p, [key]: { loading: false, rows: result } }));
@@ -2337,6 +2361,30 @@ function useChildren(getChildren) {
2337
2361
  });
2338
2362
  }
2339
2363
  }, [getChildren]);
2364
+ // Re-fetch every already-loaded parent's children when `reloadToken` changes. Compare
2365
+ // the token by VALUE (not a first-run counter) so it's safe under StrictMode's
2366
+ // double-invoke. Crucially we do NOT flip `loading: true` here — the old rows stay
2367
+ // rendered until the fresh ones land, so there's no spinner flash and (since `ensure`
2368
+ // is only re-called on expand) no perpetual spinner for an already-expanded row.
2369
+ const prevToken = React.useRef(reloadToken);
2370
+ React.useEffect(() => {
2371
+ if (reloadToken === prevToken.current)
2372
+ return;
2373
+ prevToken.current = reloadToken;
2374
+ for (const key of Object.keys(parentsRef.current)) {
2375
+ const parent = parentsRef.current[key];
2376
+ const result = getChildrenRef.current(parent);
2377
+ if (Array.isArray(result)) {
2378
+ setCache((p) => ({ ...p, [key]: { loading: false, rows: result } }));
2379
+ }
2380
+ else {
2381
+ Promise.resolve(result).then((rows) => {
2382
+ if (mounted.current)
2383
+ setCache((p) => ({ ...p, [key]: { loading: false, rows } }));
2384
+ });
2385
+ }
2386
+ }
2387
+ }, [reloadToken]);
2340
2388
  const get = React.useCallback((key) => cache[key], [cache]);
2341
2389
  return { get, ensure };
2342
2390
  }
@@ -2363,7 +2411,7 @@ function NestedInline(props) {
2363
2411
  const { items, columns, nested, selectionMode, isLoading, compact, alternateRowColors, pagination, onPageChange, aggregateItems, onSelectionChanged, rowCommands, } = parentProps;
2364
2412
  const { onItemContextMenu, contextMenuElement } = useRowContextMenu(rowCommands);
2365
2413
  const [expanded, setExpanded] = React.useState(new Set());
2366
- const { get, ensure } = useChildren(nested.getChildren);
2414
+ const { get, ensure } = useChildren(nested.getChildren, nested.reloadToken);
2367
2415
  // Forward parent-row selection to the consumer (DataGrid does this; inline did
2368
2416
  // not). A ref keeps the once-created Selection's callback from going stale when
2369
2417
  // the prop changes (mirrors DataGrid's onSelectionChangedRef).
@@ -2491,7 +2539,7 @@ function NestedCardParent(props) {
2491
2539
  const hasAggregate = columns.some((c) => c.aggregate);
2492
2540
  const [selected, setSelected] = React.useState(new Set());
2493
2541
  const [expanded, setExpanded] = React.useState(new Set());
2494
- const { get, ensure } = useChildren(nested.getChildren);
2542
+ const { get, ensure } = useChildren(nested.getChildren, nested.reloadToken);
2495
2543
  // requires-parent gating needs a Fluent Selection over the parent rows; cards have
2496
2544
  // none, so surface the unsupported config and fall back to independent selection.
2497
2545
  React.useEffect(() => {
@@ -2614,6 +2662,18 @@ function NestedTriggerCell(props) {
2614
2662
  clearTimeout(hoverTimer.current);
2615
2663
  };
2616
2664
  }, []);
2665
+ // Invalidate the on-demand cache when `reloadToken` changes (e.g. after associating a
2666
+ // new child): drop the loaded `entry` so the next open/hover re-fetches via `load()`
2667
+ // (which early-returns on a non-null `entry`). Value-compared so it's StrictMode-safe.
2668
+ // Edge: if the panel/callout is OPEN at bump time it shows the spinner until reopened —
2669
+ // acceptable, these modes are on-demand and usually closed during a toolbar action.
2670
+ const prevReloadToken = React.useRef(nested.reloadToken);
2671
+ React.useEffect(() => {
2672
+ if (nested.reloadToken === prevReloadToken.current)
2673
+ return;
2674
+ prevReloadToken.current = nested.reloadToken;
2675
+ setEntry(null);
2676
+ }, [nested.reloadToken]);
2617
2677
  // Resolve children once, on demand.
2618
2678
  const load = React.useCallback(() => {
2619
2679
  setEntry((prev) => {
@@ -2791,7 +2851,9 @@ function DetailPaneChildren(props) {
2791
2851
  return () => {
2792
2852
  alive = false;
2793
2853
  };
2794
- }, [parent, nested]);
2854
+ // `nested.reloadToken` makes the refetch explicit on a child mutation (e.g. addExisting)
2855
+ // rather than relying on `nested`'s inline-literal identity changing each render.
2856
+ }, [parent, nested, nested.reloadToken]);
2795
2857
  if (rows === null)
2796
2858
  return jsxRuntime.jsx(react.Text, { variant: "small", children: "Loading\u2026" });
2797
2859
  return (jsxRuntime.jsx(DataGrid, { items: rows, columns: nested.childColumns, registry: nested.childRegistry, compact: true, selectionMode: nested.childSelectionMode, onSelectionChanged: nested.onChildSelectionChanged ? (sel) => nested.onChildSelectionChanged(parent, sel) : undefined, editable: nested.childEditable, editTrigger: nested.childEditTrigger, onValueChange: nested.onChildValueChange