@jielga/tmdatagrid 2.0.0-beta.3 → 2.0.0-beta.4

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.
@@ -52,6 +52,7 @@ import {
52
52
  type TMDataGridEditApi,
53
53
  type TMDataGridEditCommitArgs,
54
54
  type TMDataGridSaveDraftsArgs,
55
+ type TMDataGridSaveDraftsResult,
55
56
  type TMDataGridEditEngineContext,
56
57
  type TMDataGridColumnEditOptions,
57
58
  type TMDataGridEditMode,
@@ -93,6 +94,11 @@ import {
93
94
  isSameCell,
94
95
  type TMDataGridCellPosition,
95
96
  } from "./core/cellNavigation";
97
+ import {
98
+ findFrozenStateSlices,
99
+ stabilizeControlledState,
100
+ withoutUndefinedSlices,
101
+ } from "./core/controlledState";
96
102
  import type { TMDataGridCellRange } from "./core/cellRange";
97
103
  import {
98
104
  createSelectColumn,
@@ -527,12 +533,16 @@ export type TMDataGridEditingOptions<TData extends RowData> =
527
533
  * `saveDrafts` falls back to the per-row
528
534
  * {@link TMDataGridEditingCallbacks.onCommit} loop.
529
535
  *
530
- * Rows still open are not in the payload and stay open; a rejection
531
- * keeps every draft.
536
+ * Rows still open are not in the payload and stay open. Returning
537
+ * nothing saves the whole store and throwing saves none of it;
538
+ * return a {@link TMDataGridSaveDraftsResult} to save part of it.
532
539
  */
533
540
  onSaveDrafts?: (
534
541
  args: TMDataGridSaveDraftsArgs<TData>,
535
- ) => void | Promise<void>;
542
+ ) =>
543
+ | void
544
+ | TMDataGridSaveDraftsResult
545
+ | Promise<void | TMDataGridSaveDraftsResult>;
536
546
  /**
537
547
  * @deprecated Renamed to {@link onSaveDrafts} - it fires when the
538
548
  * draft store is saved, not when a row commits into it. Still
@@ -540,7 +550,10 @@ export type TMDataGridEditingOptions<TData extends RowData> =
540
550
  */
541
551
  onCommitDrafts?: (
542
552
  args: TMDataGridSaveDraftsArgs<TData>,
543
- ) => void | Promise<void>;
553
+ ) =>
554
+ | void
555
+ | TMDataGridSaveDraftsResult
556
+ | Promise<void | TMDataGridSaveDraftsResult>;
544
557
  /**
545
558
  * Keep committed entry rows pinned in the sticky entry block until
546
559
  * the draft store is saved. Off by default: a committed row joins
@@ -1019,6 +1032,58 @@ export function useTMDataGrid<TData extends RowData>({
1019
1032
  const initialGrouping =
1020
1033
  persistedState.grouping ?? options.initialState?.grouping ?? [];
1021
1034
 
1035
+ // Current grouping state, feeding the tree column's entry in a controlled
1036
+ // `columnVisibility` (below). A ref, not state: it is only read while
1037
+ // building the options, and the store change that updates it re-renders the
1038
+ // hook anyway. A controlled `grouping` takes precedence over the persisted
1039
+ // one; grouping held in an external atom is not readable here and is
1040
+ // corrected by the effect below with one extra write on mount.
1041
+ const groupingActiveRef = useRef(
1042
+ (options.state?.grouping ?? initialGrouping).length > 0,
1043
+ );
1044
+
1045
+ // Removes keys set to `undefined` before TanStack writes them into the
1046
+ // slice atoms; such a key means the slice is not controlled.
1047
+ const consumerState = withoutUndefinedSlices(options.state);
1048
+
1049
+ // A controlled `columnVisibility` replaces the whole map on every options
1050
+ // sync, including the grid's own entries, so the tree column's entry must be
1051
+ // re-applied here. Entries for the control columns are removed, as they are
1052
+ // for `initialState`: their visibility follows the feature options, not the
1053
+ // visibility map.
1054
+ const controlledColumnVisibility = consumerState?.columnVisibility;
1055
+ const requestedState =
1056
+ consumerState !== undefined && controlledColumnVisibility !== undefined
1057
+ ? {
1058
+ ...consumerState,
1059
+ columnVisibility: {
1060
+ ...withoutControlColumnVisibility(controlledColumnVisibility),
1061
+ ...(groupColumnEnabled
1062
+ ? { [GROUP_COLUMN_ID]: groupingActiveRef.current }
1063
+ : {}),
1064
+ },
1065
+ }
1066
+ : consumerState;
1067
+
1068
+ // Controlled state is synced to the table on every render and compared by
1069
+ // identity, so a `state` object built in the consumer's render body would
1070
+ // cause an infinite render loop. Unchanged slices are forwarded with the
1071
+ // previous render's identity instead - see controlledState.ts.
1072
+ const controlledStateRef = useRef<Partial<TableState<TMDataGridFeatures>>>(
1073
+ undefined,
1074
+ );
1075
+ // Exception: the grouping workaround below republishes `columnOrder` and
1076
+ // `columnVisibility` with unchanged contents and a new identity to repair
1077
+ // table-core's missing memo deps - the exact write stabilization cancels.
1078
+ // After it runs, one render forwards the controlled state unstabilized so
1079
+ // the new identities reach the atoms. A single render cannot loop.
1080
+ const republishControlledStateRef = useRef(false);
1081
+ const controlledState = republishControlledStateRef.current
1082
+ ? requestedState
1083
+ : stabilizeControlledState(requestedState, controlledStateRef.current);
1084
+ republishControlledStateRef.current = false;
1085
+ controlledStateRef.current = controlledState;
1086
+
1022
1087
  const table = useTable({
1023
1088
  columnResizeMode: "onChange",
1024
1089
  enableSorting: true,
@@ -1057,6 +1122,8 @@ export function useTMDataGrid<TData extends RowData>({
1057
1122
  : options.enableRowPinning === true),
1058
1123
  features: tmDataGridFeatures,
1059
1124
  columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
1125
+ // The stabilized controlled state; `undefined` when nothing is controlled.
1126
+ state: controlledState,
1060
1127
  initialState: {
1061
1128
  ...options.initialState,
1062
1129
  ...persistedState,
@@ -1164,6 +1231,20 @@ export function useTMDataGrid<TData extends RowData>({
1164
1231
  // eslint-disable-next-line react-hooks/exhaustive-deps
1165
1232
  }, []);
1166
1233
 
1234
+ // A controlled slice without its `onXChange` cannot change: TanStack routes
1235
+ // every write through the callback, and the next options sync restores the
1236
+ // consumer's value. Warned once in development; `initialState` is the option
1237
+ // for a starting value.
1238
+ useEffect(() => {
1239
+ for (const { slice, handler } of findFrozenStateSlices(options)) {
1240
+ console.warn(
1241
+ `TMDataGrid: state.${slice} is controlled but no ${handler} was passed - the slice cannot change. Add ${handler}, or use initialState.${slice} for a starting value.`,
1242
+ );
1243
+ }
1244
+ // A mount-time contract, like the check above.
1245
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1246
+ }, []);
1247
+
1167
1248
  // Two things have to happen whenever `grouping` changes.
1168
1249
  //
1169
1250
  // One: the tree column appears with the first grouped column and goes away
@@ -1203,10 +1284,40 @@ export function useTMDataGrid<TData extends RowData>({
1203
1284
  useEffect(() => {
1204
1285
  if (!groupColumnEnabled) return;
1205
1286
  let previousGrouping = table.store.state.grouping;
1287
+ // Update the ref before any write: it feeds the visibility injection
1288
+ // above, and a write below re-renders the hook. With a stale ref the
1289
+ // injection would restore the old value on every render. This also
1290
+ // corrects the mount value when an external atom owns `grouping`, which
1291
+ // the ref's initializer cannot read.
1292
+ groupingActiveRef.current = previousGrouping.length > 0;
1293
+
1294
+ // The tree column's entry in `initialState` only reaches a slice the
1295
+ // table owns. With `columnVisibility` in an external atom the entry is
1296
+ // missing, and a missing entry means visible: the tree column would
1297
+ // render in an ungrouped grid. The entry is seeded here through the table
1298
+ // API instead, so the write reaches whichever store owns the slice. No-op
1299
+ // when the entry is already correct. `?.`: an external atom can hold
1300
+ // `undefined`.
1301
+ if (
1302
+ table.store.state.columnVisibility?.[GROUP_COLUMN_ID] !==
1303
+ (previousGrouping.length > 0)
1304
+ ) {
1305
+ table.setColumnVisibility((old) => ({
1306
+ ...old,
1307
+ [GROUP_COLUMN_ID]: previousGrouping.length > 0,
1308
+ }));
1309
+ }
1206
1310
 
1207
1311
  const subscription = table.store.subscribe((state) => {
1208
1312
  if (state.grouping === previousGrouping) return;
1209
1313
  previousGrouping = state.grouping;
1314
+ // Keeps the entry injected into a controlled `columnVisibility` (see
1315
+ // requestedState) in sync with grouping.
1316
+ groupingActiveRef.current = state.grouping.length > 0;
1317
+ // On a controlled slice the writes below round-trip through the
1318
+ // consumer's handler; the next render must forward them unstabilized.
1319
+ // See republishControlledStateRef.
1320
+ republishControlledStateRef.current = true;
1210
1321
 
1211
1322
  table.setColumnVisibility((old) => ({
1212
1323
  ...old,