@dataverse-kit/grid-kit 0.1.0 → 0.3.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
@@ -2135,10 +2135,18 @@ function resolveCardLayout(columns, card) {
2135
2135
  * ratings, etc. look identical). Mirrors the Grid Customizer's "Card List" type.
2136
2136
  *
2137
2137
  * Selection is checkbox-based (manual Set), independent of Fluent's DetailsList
2138
- * Selection.
2138
+ * Selection. `rowCommands` adds a per-card right-click context menu (parity with
2139
+ * `DataGrid` / `NestedCardParent`), suppressing the native browser menu.
2139
2140
  */
2140
2141
  function CardGrid(props) {
2141
- const { items, columns, card, selectionMode = 'none', onSelectionChanged, toolbar, pagination, onPageChange, onActiveItemChanged, fill, height, } = props;
2142
+ const { items, columns, card, selectionMode = 'none', onSelectionChanged, toolbar, pagination, onPageChange, onActiveItemChanged, fill, height, rowCommands, } = props;
2143
+ // Per-row right-click menu (parity with DataGrid / NestedCardParent). For these
2144
+ // raw-div cards we invoke the hook fn from a native onContextMenu and suppress the
2145
+ // browser menu with e.preventDefault() ourselves (the hook's `return false` only
2146
+ // matters to Fluent's DetailsList onItemContextMenu contract). CardGrid is FLAT — no
2147
+ // nested child region — so (unlike NestedCardParent) nothing needs contextmenu
2148
+ // stopPropagation.
2149
+ const { onItemContextMenu, contextMenuElement } = useRowContextMenu(rowCommands);
2142
2150
  const edit = useEditState();
2143
2151
  const { getKeyFn, ctx } = useGridContext(props, edit);
2144
2152
  // Card layout derivation is shared (and unit-tested) via resolveCardLayout —
@@ -2177,10 +2185,21 @@ function CardGrid(props) {
2177
2185
  paddingTop: 4,
2178
2186
  }, children: items.map((item) => {
2179
2187
  const key = getKeyFn(item);
2180
- return (jsxRuntime.jsxs("div", { className: cardClass, style: cardHeight ? { height: cardHeight } : undefined, onClick: () => onActiveItemChanged?.(item), children: [jsxRuntime.jsxs(react.Stack, { horizontal: true, horizontalAlign: "space-between", verticalAlign: "start", children: [jsxRuntime.jsx(react.Text, { variant: "mediumPlus", styles: { root: { fontWeight: 600 } }, children: renderField(byField.get(titleField ?? ''), item) ?? rawValue(item, titleField) }), selectionMode !== 'none' && (
2188
+ return (jsxRuntime.jsxs("div", { className: cardClass, style: cardHeight ? { height: cardHeight } : undefined, onClick: () => onActiveItemChanged?.(item),
2189
+ // Right-click anywhere on the card opens its row menu. The card is the
2190
+ // row-equivalent target: the title/body cells have no handler to intercept
2191
+ // it, and the selection checkbox below only stops LEFT-click (its onClick
2192
+ // stopPropagation), so a right-click on any of them bubbles here
2193
+ // (preventDefault suppresses the browser menu).
2194
+ onContextMenu: onItemContextMenu
2195
+ ? (e) => {
2196
+ e.preventDefault();
2197
+ onItemContextMenu(item, undefined, e.nativeEvent);
2198
+ }
2199
+ : undefined, children: [jsxRuntime.jsxs(react.Stack, { horizontal: true, horizontalAlign: "space-between", verticalAlign: "start", children: [jsxRuntime.jsx(react.Text, { variant: "mediumPlus", styles: { root: { fontWeight: 600 } }, children: renderField(byField.get(titleField ?? ''), item) ?? rawValue(item, titleField) }), selectionMode !== 'none' && (
2181
2200
  // Stop the click from bubbling to the card's onActiveItemChanged.
2182
2201
  jsxRuntime.jsx("span", { onClick: (e) => e.stopPropagation(), children: jsxRuntime.jsx(react.Checkbox, { checked: selected.has(key), onChange: (_, checked) => toggleSelect(item, checked), ariaLabel: "Select card" }) }))] }), imageField && rawValue(item, imageField) && (jsxRuntime.jsx("img", { className: cardImageClass, src: rawValue(item, imageField), alt: "" })), subtitleField && (jsxRuntime.jsx(react.Text, { variant: "small", styles: { root: { color: '#605e5c' } }, children: rawValue(item, subtitleField) })), bodyColumns.map((col) => (jsxRuntime.jsxs("div", { className: bodyRowClass, children: [jsxRuntime.jsx(react.Text, { variant: "small", styles: { root: { color: '#605e5c' } }, children: col.name }), jsxRuntime.jsx("span", { children: renderField(col, item) })] }, col.key)))] }, key));
2183
- }) }) }), pagination && jsxRuntime.jsx(GridPaginationFooter, { pagination: pagination, onPageChange: onPageChange })] }));
2202
+ }) }) }), pagination && jsxRuntime.jsx(GridPaginationFooter, { pagination: pagination, onPageChange: onPageChange }), contextMenuElement] }));
2184
2203
  }
2185
2204
 
2186
2205
  /**
@@ -2290,10 +2309,17 @@ function GroupedGrid(props) {
2290
2309
  * never fetches — this just normalizes the consumer-supplied resolver and tracks
2291
2310
  * loading. Call `ensure(key, parent)` on demand (row expand / panel open / hover).
2292
2311
  */
2293
- function useChildren(getChildren) {
2312
+ function useChildren(getChildren, reloadToken) {
2294
2313
  const [cache, setCache] = React.useState({});
2295
2314
  const cacheRef = React.useRef(cache);
2296
2315
  cacheRef.current = cache;
2316
+ // Remember the parent object per cached key so a `reloadToken` bump can re-run
2317
+ // `getChildren(parent)` for already-loaded rows.
2318
+ const parentsRef = React.useRef({});
2319
+ // `getChildren` accessed via a ref inside the reload effect → the effect can depend
2320
+ // on `reloadToken` alone without a stale closure or an exhaustive-deps warning.
2321
+ const getChildrenRef = React.useRef(getChildren);
2322
+ getChildrenRef.current = getChildren;
2297
2323
  // Guard against setState after unmount (e.g. host swapped while a child fetch
2298
2324
  // is in flight) — mirrors NestedTriggerCell / DetailPaneChildren.
2299
2325
  const mounted = React.useRef(true);
@@ -2306,6 +2332,7 @@ function useChildren(getChildren) {
2306
2332
  const ensure = React.useCallback((key, parent) => {
2307
2333
  if (cacheRef.current[key])
2308
2334
  return;
2335
+ parentsRef.current[key] = parent;
2309
2336
  const result = getChildren(parent);
2310
2337
  if (Array.isArray(result)) {
2311
2338
  setCache((p) => ({ ...p, [key]: { loading: false, rows: result } }));
@@ -2318,6 +2345,30 @@ function useChildren(getChildren) {
2318
2345
  });
2319
2346
  }
2320
2347
  }, [getChildren]);
2348
+ // Re-fetch every already-loaded parent's children when `reloadToken` changes. Compare
2349
+ // the token by VALUE (not a first-run counter) so it's safe under StrictMode's
2350
+ // double-invoke. Crucially we do NOT flip `loading: true` here — the old rows stay
2351
+ // rendered until the fresh ones land, so there's no spinner flash and (since `ensure`
2352
+ // is only re-called on expand) no perpetual spinner for an already-expanded row.
2353
+ const prevToken = React.useRef(reloadToken);
2354
+ React.useEffect(() => {
2355
+ if (reloadToken === prevToken.current)
2356
+ return;
2357
+ prevToken.current = reloadToken;
2358
+ for (const key of Object.keys(parentsRef.current)) {
2359
+ const parent = parentsRef.current[key];
2360
+ const result = getChildrenRef.current(parent);
2361
+ if (Array.isArray(result)) {
2362
+ setCache((p) => ({ ...p, [key]: { loading: false, rows: result } }));
2363
+ }
2364
+ else {
2365
+ Promise.resolve(result).then((rows) => {
2366
+ if (mounted.current)
2367
+ setCache((p) => ({ ...p, [key]: { loading: false, rows } }));
2368
+ });
2369
+ }
2370
+ }
2371
+ }, [reloadToken]);
2321
2372
  const get = React.useCallback((key) => cache[key], [cache]);
2322
2373
  return { get, ensure };
2323
2374
  }
@@ -2344,7 +2395,7 @@ function NestedInline(props) {
2344
2395
  const { items, columns, nested, selectionMode, isLoading, compact, alternateRowColors, pagination, onPageChange, aggregateItems, onSelectionChanged, rowCommands, } = parentProps;
2345
2396
  const { onItemContextMenu, contextMenuElement } = useRowContextMenu(rowCommands);
2346
2397
  const [expanded, setExpanded] = React.useState(new Set());
2347
- const { get, ensure } = useChildren(nested.getChildren);
2398
+ const { get, ensure } = useChildren(nested.getChildren, nested.reloadToken);
2348
2399
  // Forward parent-row selection to the consumer (DataGrid does this; inline did
2349
2400
  // not). A ref keeps the once-created Selection's callback from going stale when
2350
2401
  // the prop changes (mirrors DataGrid's onSelectionChangedRef).
@@ -2472,7 +2523,7 @@ function NestedCardParent(props) {
2472
2523
  const hasAggregate = columns.some((c) => c.aggregate);
2473
2524
  const [selected, setSelected] = React.useState(new Set());
2474
2525
  const [expanded, setExpanded] = React.useState(new Set());
2475
- const { get, ensure } = useChildren(nested.getChildren);
2526
+ const { get, ensure } = useChildren(nested.getChildren, nested.reloadToken);
2476
2527
  // requires-parent gating needs a Fluent Selection over the parent rows; cards have
2477
2528
  // none, so surface the unsupported config and fall back to independent selection.
2478
2529
  React.useEffect(() => {
@@ -2595,6 +2646,18 @@ function NestedTriggerCell(props) {
2595
2646
  clearTimeout(hoverTimer.current);
2596
2647
  };
2597
2648
  }, []);
2649
+ // Invalidate the on-demand cache when `reloadToken` changes (e.g. after associating a
2650
+ // new child): drop the loaded `entry` so the next open/hover re-fetches via `load()`
2651
+ // (which early-returns on a non-null `entry`). Value-compared so it's StrictMode-safe.
2652
+ // Edge: if the panel/callout is OPEN at bump time it shows the spinner until reopened —
2653
+ // acceptable, these modes are on-demand and usually closed during a toolbar action.
2654
+ const prevReloadToken = React.useRef(nested.reloadToken);
2655
+ React.useEffect(() => {
2656
+ if (nested.reloadToken === prevReloadToken.current)
2657
+ return;
2658
+ prevReloadToken.current = nested.reloadToken;
2659
+ setEntry(null);
2660
+ }, [nested.reloadToken]);
2598
2661
  // Resolve children once, on demand.
2599
2662
  const load = React.useCallback(() => {
2600
2663
  setEntry((prev) => {
@@ -2772,7 +2835,9 @@ function DetailPaneChildren(props) {
2772
2835
  return () => {
2773
2836
  alive = false;
2774
2837
  };
2775
- }, [parent, nested]);
2838
+ // `nested.reloadToken` makes the refetch explicit on a child mutation (e.g. addExisting)
2839
+ // rather than relying on `nested`'s inline-literal identity changing each render.
2840
+ }, [parent, nested, nested.reloadToken]);
2776
2841
  if (rows === null)
2777
2842
  return jsxRuntime.jsx(react.Text, { variant: "small", children: "Loading\u2026" });
2778
2843
  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