@jielga/tmdatagrid 2.0.0-beta.2 → 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.
- package/dist/index.d.ts +185 -48
- package/dist/index.js +1558 -1341
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +1 -1
- package/skills/cell-selection/SKILL.md +1 -1
- package/skills/columns/SKILL.md +1 -1
- package/skills/data/SKILL.md +1 -1
- package/skills/editing/SKILL.md +59 -25
- package/skills/editing/references/common-mistakes.md +10 -8
- package/skills/editing/references/editing-api.md +20 -13
- package/skills/filtering/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/grouping/SKILL.md +1 -1
- package/skills/options/SKILL.md +33 -3
- package/skills/rows/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +10 -7
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +96 -25
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +21 -18
- package/src/tmdatagrid/components/TMDataGridTable.tsx +13 -0
- package/src/tmdatagrid/core/controlledState.ts +179 -0
- package/src/tmdatagrid/core/editEngine.ts +480 -77
- package/src/tmdatagrid/core/labels.ts +8 -1
- package/src/tmdatagrid/core/labelsSv.ts +2 -0
- package/src/tmdatagrid/index.ts +5 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +154 -22
|
@@ -111,10 +111,15 @@ export type TMDataGridLabels = {
|
|
|
111
111
|
cancelRowEdit: string;
|
|
112
112
|
/** Save tooltip when field errors block the row, with no row message. */
|
|
113
113
|
editRowErrors: string;
|
|
114
|
-
/** `EditActions`' Save, with the
|
|
114
|
+
/** `EditActions`' Save, with the count of rows in the draft store. */
|
|
115
115
|
saveAllEdits: (rows: number) => string;
|
|
116
116
|
/** `EditActions`' Discard. */
|
|
117
117
|
discardAllEdits: string;
|
|
118
|
+
/**
|
|
119
|
+
* `EditActions`' note about rows still open - edited but not committed, so
|
|
120
|
+
* not part of the save. Shown only while there are any.
|
|
121
|
+
*/
|
|
122
|
+
editRowsStillOpen: (rows: number) => string;
|
|
118
123
|
/** The entry row's ✓ - commit the add. */
|
|
119
124
|
confirmNewRow: string;
|
|
120
125
|
/** The entry row's ✕ - drop the entry. */
|
|
@@ -243,6 +248,8 @@ export const TMDATAGRID_LABELS_EN: TMDataGridLabels = {
|
|
|
243
248
|
editRowErrors: "Fix the marked cells",
|
|
244
249
|
saveAllEdits: (rows) => (rows === 1 ? "Save 1 row" : `Save ${rows} rows`),
|
|
245
250
|
discardAllEdits: "Discard",
|
|
251
|
+
editRowsStillOpen: (rows) =>
|
|
252
|
+
rows === 1 ? "1 row still being edited" : `${rows} rows still being edited`,
|
|
246
253
|
confirmNewRow: "Add row",
|
|
247
254
|
discardNewRow: "Discard new row",
|
|
248
255
|
deleteRow: "Delete row",
|
|
@@ -103,6 +103,8 @@ export const TMDATAGRID_LABELS_SV: TMDataGridLabels = {
|
|
|
103
103
|
editRowErrors: "Åtgärda de markerade cellerna",
|
|
104
104
|
saveAllEdits: (rows) => (rows === 1 ? "Spara 1 rad" : `Spara ${rows} rader`),
|
|
105
105
|
discardAllEdits: "Förkasta",
|
|
106
|
+
editRowsStillOpen: (rows) =>
|
|
107
|
+
rows === 1 ? "1 rad redigeras fortfarande" : `${rows} rader redigeras fortfarande`,
|
|
106
108
|
confirmNewRow: "Lägg till rad",
|
|
107
109
|
discardNewRow: "Släng ny rad",
|
|
108
110
|
deleteRow: "Ta bort rad",
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -82,7 +82,12 @@ export {
|
|
|
82
82
|
type TMDataGridEditApi,
|
|
83
83
|
type TMDataGridEditChange,
|
|
84
84
|
type TMDataGridEditCommitArgs,
|
|
85
|
+
type TMDataGridSaveDraftsArgs,
|
|
86
|
+
type TMDataGridSaveDraftsResult,
|
|
87
|
+
type TMDataGridSaveOutcomes,
|
|
85
88
|
type TMDataGridEditCommitDraftsArgs,
|
|
89
|
+
type TMDataGridAddRowsOptions,
|
|
90
|
+
type TMDataGridAddRowsResult,
|
|
86
91
|
type TMDataGridEditField,
|
|
87
92
|
type TMDataGridEditMode,
|
|
88
93
|
type TMDataGridEditorArgs,
|
|
@@ -51,7 +51,8 @@ import {
|
|
|
51
51
|
createEditEngine,
|
|
52
52
|
type TMDataGridEditApi,
|
|
53
53
|
type TMDataGridEditCommitArgs,
|
|
54
|
-
type
|
|
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,
|
|
@@ -390,7 +396,7 @@ export type TMDataGridApi<TData extends RowData> = {
|
|
|
390
396
|
* out the same TanStack Form the inline editors write through, so a drawer
|
|
391
397
|
* or detail panel can share a row's draft. Inert until `editing` is set.
|
|
392
398
|
*/
|
|
393
|
-
edit: TMDataGridEditApi
|
|
399
|
+
edit: TMDataGridEditApi<TData>;
|
|
394
400
|
/** Table-level feature switches, re-read from options on every render. */
|
|
395
401
|
features: TMDataGridFeatureFlags;
|
|
396
402
|
/** Every string the chrome renders, `labels` merged over the English defaults. */
|
|
@@ -471,6 +477,7 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
|
|
|
471
477
|
/**
|
|
472
478
|
* Seed values for `edit.addRow()` - the entry row's starting point. A
|
|
473
479
|
* function is called per added row (fresh timestamps, empty arrays).
|
|
480
|
+
* `edit.addRow(values)` overrides this key by key for that one row.
|
|
474
481
|
*/
|
|
475
482
|
newRowDefaults?: TData | (() => TData);
|
|
476
483
|
/**
|
|
@@ -498,12 +505,12 @@ type TMDataGridEditingCallbacks<TData extends RowData> = {
|
|
|
498
505
|
* | `"cell"` | Enter, Tab, blur - Sheets | Escape |
|
|
499
506
|
* | `"cellConfirm"` | ✓ or Enter only; blur keeps the draft | ✕ or Escape |
|
|
500
507
|
* | `"row"` | Save in the edit lane, or Ctrl+Enter | Cancel, or Escape |
|
|
501
|
-
* | `"draft"` | `edit.
|
|
508
|
+
* | `"draft"` | `edit.commit(rowId)` into the draft store, `edit.saveDrafts()` out | `edit.cancelAll()` |
|
|
502
509
|
*
|
|
503
510
|
* Setting `editing` makes `getRowId` required - drafts are keyed by row id,
|
|
504
511
|
* and the index fallback would name a different record after any sort - and
|
|
505
|
-
* `
|
|
506
|
-
*
|
|
512
|
+
* `onSaveDrafts` exists only under `mode: "draft"`, the one mode with a draft
|
|
513
|
+
* store to save.
|
|
507
514
|
*
|
|
508
515
|
* The object may be written inline: the callbacks are read through a ref
|
|
509
516
|
* every render, so its identity does not matter.
|
|
@@ -520,28 +527,49 @@ export type TMDataGridEditingOptions<TData extends RowData> =
|
|
|
520
527
|
| {
|
|
521
528
|
mode: "draft";
|
|
522
529
|
/**
|
|
523
|
-
* Draft mode's save
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
530
|
+
* Draft mode's save: called once by `edit.saveDrafts()` with the
|
|
531
|
+
* whole draft store - committed edits, added rows and deletion
|
|
532
|
+
* marks - so a server can apply it as one transaction. Without it,
|
|
533
|
+
* `saveDrafts` falls back to the per-row
|
|
534
|
+
* {@link TMDataGridEditingCallbacks.onCommit} loop.
|
|
535
|
+
*
|
|
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.
|
|
539
|
+
*/
|
|
540
|
+
onSaveDrafts?: (
|
|
541
|
+
args: TMDataGridSaveDraftsArgs<TData>,
|
|
542
|
+
) =>
|
|
543
|
+
| void
|
|
544
|
+
| TMDataGridSaveDraftsResult
|
|
545
|
+
| Promise<void | TMDataGridSaveDraftsResult>;
|
|
546
|
+
/**
|
|
547
|
+
* @deprecated Renamed to {@link onSaveDrafts} - it fires when the
|
|
548
|
+
* draft store is saved, not when a row commits into it. Still
|
|
549
|
+
* honoured; removed in a later beta.
|
|
527
550
|
*/
|
|
528
551
|
onCommitDrafts?: (
|
|
529
|
-
args:
|
|
530
|
-
) =>
|
|
552
|
+
args: TMDataGridSaveDraftsArgs<TData>,
|
|
553
|
+
) =>
|
|
554
|
+
| void
|
|
555
|
+
| TMDataGridSaveDraftsResult
|
|
556
|
+
| Promise<void | TMDataGridSaveDraftsResult>;
|
|
531
557
|
/**
|
|
532
|
-
* Keep
|
|
533
|
-
*
|
|
534
|
-
* flow above the body rows instead - the block a row
|
|
535
|
-
* into is always sticky, but
|
|
536
|
-
* cannot fill the viewport with sticky chrome.
|
|
558
|
+
* Keep committed entry rows pinned in the sticky entry block until
|
|
559
|
+
* the draft store is saved. Off by default: a committed row joins
|
|
560
|
+
* the scrolling flow above the body rows instead - the block a row
|
|
561
|
+
* is *typed* into is always sticky, but committed rows scroll, so
|
|
562
|
+
* committing many cannot fill the viewport with sticky chrome.
|
|
537
563
|
*/
|
|
538
564
|
newRowsSticky?: boolean;
|
|
539
565
|
}
|
|
540
566
|
| {
|
|
541
567
|
mode: Exclude<TMDataGridEditMode, "draft">;
|
|
542
|
-
/** Only `"draft"`
|
|
568
|
+
/** Only `"draft"` has a draft store to save - see the other branch. */
|
|
569
|
+
onSaveDrafts?: never;
|
|
570
|
+
/** @deprecated See {@link onSaveDrafts}. */
|
|
543
571
|
onCommitDrafts?: never;
|
|
544
|
-
/**
|
|
572
|
+
/** Committed entry rows exist only under `"draft"` - see there. */
|
|
545
573
|
newRowsSticky?: never;
|
|
546
574
|
}
|
|
547
575
|
);
|
|
@@ -1004,6 +1032,58 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1004
1032
|
const initialGrouping =
|
|
1005
1033
|
persistedState.grouping ?? options.initialState?.grouping ?? [];
|
|
1006
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
|
+
|
|
1007
1087
|
const table = useTable({
|
|
1008
1088
|
columnResizeMode: "onChange",
|
|
1009
1089
|
enableSorting: true,
|
|
@@ -1042,6 +1122,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1042
1122
|
: options.enableRowPinning === true),
|
|
1043
1123
|
features: tmDataGridFeatures,
|
|
1044
1124
|
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
1125
|
+
// The stabilized controlled state; `undefined` when nothing is controlled.
|
|
1126
|
+
state: controlledState,
|
|
1045
1127
|
initialState: {
|
|
1046
1128
|
...options.initialState,
|
|
1047
1129
|
...persistedState,
|
|
@@ -1109,16 +1191,22 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1109
1191
|
editing?.isRowEditable as TMDataGridEditEngineContext["isRowEditable"],
|
|
1110
1192
|
onEditCommit:
|
|
1111
1193
|
editing?.onCommit as TMDataGridEditEngineContext["onEditCommit"],
|
|
1112
|
-
|
|
1113
|
-
|
|
1194
|
+
// The deprecated name still works; the new one wins if both are set.
|
|
1195
|
+
onSaveDrafts: (editing?.onSaveDrafts ??
|
|
1196
|
+
editing?.onCommitDrafts) as TMDataGridEditEngineContext["onSaveDrafts"],
|
|
1114
1197
|
newRowDefaults:
|
|
1115
1198
|
editing?.newRowDefaults as TMDataGridEditEngineContext["newRowDefaults"],
|
|
1116
1199
|
onRowAdd: editing?.onRowAdd as TMDataGridEditEngineContext["onRowAdd"],
|
|
1117
1200
|
onRowDelete:
|
|
1118
1201
|
editing?.onRowDelete as TMDataGridEditEngineContext["onRowDelete"],
|
|
1119
1202
|
};
|
|
1120
|
-
|
|
1121
|
-
|
|
1203
|
+
// The engine is erased; the row type comes back on the way out, which is
|
|
1204
|
+
// what makes `edit.addRow(values)` check against `TData`.
|
|
1205
|
+
const [edit] = useState(
|
|
1206
|
+
() =>
|
|
1207
|
+
createEditEngine(
|
|
1208
|
+
() => editContextRef.current,
|
|
1209
|
+
) as unknown as TMDataGridEditApi<TData>,
|
|
1122
1210
|
);
|
|
1123
1211
|
|
|
1124
1212
|
// Switching modes mid-flight drops every draft: the policies disagree about
|
|
@@ -1143,6 +1231,20 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1143
1231
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1144
1232
|
}, []);
|
|
1145
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
|
+
|
|
1146
1248
|
// Two things have to happen whenever `grouping` changes.
|
|
1147
1249
|
//
|
|
1148
1250
|
// One: the tree column appears with the first grouped column and goes away
|
|
@@ -1182,10 +1284,40 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
1182
1284
|
useEffect(() => {
|
|
1183
1285
|
if (!groupColumnEnabled) return;
|
|
1184
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
|
+
}
|
|
1185
1310
|
|
|
1186
1311
|
const subscription = table.store.subscribe((state) => {
|
|
1187
1312
|
if (state.grouping === previousGrouping) return;
|
|
1188
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;
|
|
1189
1321
|
|
|
1190
1322
|
table.setColumnVisibility((old) => ({
|
|
1191
1323
|
...old,
|