@jielga/tmdatagrid 1.0.0 → 1.0.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/README.md +8 -8
- package/dist/index.d.ts +225 -225
- package/dist/index.js +58 -50
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +3 -2
- package/skills/appearance/SKILL.md +322 -0
- package/skills/cell-selection/SKILL.md +240 -0
- package/skills/columns/SKILL.md +261 -86
- package/skills/data/SKILL.md +289 -0
- package/skills/editing/SKILL.md +492 -0
- package/skills/editing/references/editing-api.md +124 -0
- package/skills/editing/references/editors-and-validation.md +198 -0
- package/skills/filtering/SKILL.md +344 -0
- package/skills/getting-started/SKILL.md +48 -27
- package/skills/grouping/SKILL.md +264 -0
- package/skills/options/SKILL.md +31 -20
- package/skills/rows/SKILL.md +369 -0
- package/skills/rows/references/rows-api.md +117 -0
- package/skills/server-side/SKILL.md +7 -7
- package/skills/testing/SKILL.md +12 -12
- package/src/tmdatagrid/TMDataGridContext.ts +2 -2
- package/src/tmdatagrid/components/TMDataGrid.module.css +2 -2
- package/src/tmdatagrid/components/TMDataGrid.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +4 -4
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +6 -6
- package/src/tmdatagrid/components/TMDataGridEditActions.tsx +2 -2
- package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +4 -4
- package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +6 -6
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +6 -6
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +2 -2
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +1 -1
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +1 -1
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +12 -6
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +1 -1
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +8 -8
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +12 -12
- package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +4 -4
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +8 -8
- package/src/tmdatagrid/components/TMDataGridTable.module.css +18 -18
- package/src/tmdatagrid/components/TMDataGridTable.tsx +116 -116
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +5 -5
- package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +1 -1
- package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +2 -2
- package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +1 -1
- package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +1 -1
- package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +2 -2
- package/src/tmdatagrid/components/editors/editorShared.ts +3 -3
- package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +1 -1
- package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +1 -1
- package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +1 -1
- package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +2 -2
- package/src/tmdatagrid/components/sticky.module.css +8 -8
- package/src/tmdatagrid/core/autosize.ts +4 -4
- package/src/tmdatagrid/core/capabilities.ts +17 -17
- package/src/tmdatagrid/core/cellExport.ts +13 -13
- package/src/tmdatagrid/core/cellNavigation.ts +6 -6
- package/src/tmdatagrid/core/cellRange.ts +4 -4
- package/src/tmdatagrid/core/columnOptions.ts +2 -2
- package/src/tmdatagrid/core/columnOrdering.ts +2 -2
- package/src/tmdatagrid/core/columnUtils.ts +3 -3
- package/src/tmdatagrid/core/editEngine.ts +49 -49
- package/src/tmdatagrid/core/expanding.ts +5 -5
- package/src/tmdatagrid/core/filterControls.ts +5 -5
- package/src/tmdatagrid/core/filterOperators.ts +14 -14
- package/src/tmdatagrid/core/labels.ts +8 -8
- package/src/tmdatagrid/core/matchHighlight.ts +4 -4
- package/src/tmdatagrid/core/persistence.ts +8 -8
- package/src/tmdatagrid/core/quickSearch.ts +8 -8
- package/src/tmdatagrid/core/rowPinning.ts +3 -3
- package/src/tmdatagrid/core/rowSelection.ts +12 -12
- package/src/tmdatagrid/core/sizes.ts +1 -1
- package/src/tmdatagrid/core/summary.ts +3 -3
- package/src/tmdatagrid/useTMDataGrid.tsx +79 -79
- package/skills/features/SKILL.md +0 -352
|
@@ -15,15 +15,15 @@ import type { TMDataGridColumnType } from "./filterOperators";
|
|
|
15
15
|
import type { TMDataGridSize } from "./sizes";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* How commits happen
|
|
18
|
+
* How commits happen - one axis, each mode a thin policy over the same
|
|
19
19
|
* engine. See `editMode` on `UseTMDataGridOptions`.
|
|
20
20
|
*/
|
|
21
21
|
export type TMDataGridEditMode = "cell" | "cellConfirm" | "row" | "batch";
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* One editing row's live form. TanStack Form's `FormApi`, not a wrapper: the
|
|
25
|
-
* engine is a form library, and everything mid-edit
|
|
26
|
-
* errors, async validation, submit lifecycle
|
|
25
|
+
* engine is a form library, and everything mid-edit - values, dirty state,
|
|
26
|
+
* errors, async validation, submit lifecycle - is form state, read off
|
|
27
27
|
* `form.store` with the same selector idiom as the rest of the grid.
|
|
28
28
|
*
|
|
29
29
|
* The alias erases Form's validator generics the way `TMDataGridRowData`
|
|
@@ -31,20 +31,20 @@ export type TMDataGridEditMode = "cell" | "cellConfirm" | "row" | "batch";
|
|
|
31
31
|
*/
|
|
32
32
|
export type TMDataGridRowEditForm = AnyFormApi;
|
|
33
33
|
|
|
34
|
-
/** The live field a cell editor binds to
|
|
34
|
+
/** The live field a cell editor binds to - TanStack Form's own `FieldApi`. */
|
|
35
35
|
export type TMDataGridEditField = AnyFieldApi;
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* A validator as TanStack Form takes it: a Standard Schema (Zod, Valibot,
|
|
39
39
|
* ArkType…) or a plain function returning an error or nothing. Forwarded
|
|
40
|
-
* untouched
|
|
40
|
+
* untouched - the vocabulary is Form's, not a grid re-implementation.
|
|
41
41
|
*/
|
|
42
42
|
export type TMDataGridValidator =
|
|
43
43
|
| StandardSchemaV1<unknown, unknown>
|
|
44
44
|
| ((args: { value: never; fieldApi: never }) => unknown);
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* `meta.validate`
|
|
47
|
+
* `meta.validate` - per-column field validators. A bare schema or function is
|
|
48
48
|
* shorthand for `{ onChange: it }`; the object form takes every trigger
|
|
49
49
|
* TanStack Form defines, `Async` variants and debounce included.
|
|
50
50
|
*/
|
|
@@ -62,7 +62,7 @@ export type TMDataGridFieldValidate =
|
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* `rowValidators`
|
|
65
|
+
* `rowValidators` - form-level validators for the whole row, where
|
|
66
66
|
* cross-field rules live. Passed into each row form's `validators` untouched;
|
|
67
67
|
* pathed issues land on the matching fields, pathless ones on the row.
|
|
68
68
|
*/
|
|
@@ -82,7 +82,7 @@ export type TMDataGridRowValidators = {
|
|
|
82
82
|
export type TMDataGridEditChange = {
|
|
83
83
|
/** Column the field maps back to, for consumers thinking in columns. */
|
|
84
84
|
columnId: string;
|
|
85
|
-
/** The data path
|
|
85
|
+
/** The data path - `getEditFieldName` of the column. */
|
|
86
86
|
field: string;
|
|
87
87
|
previous: unknown;
|
|
88
88
|
next: unknown;
|
|
@@ -90,23 +90,23 @@ export type TMDataGridEditChange = {
|
|
|
90
90
|
|
|
91
91
|
export type TMDataGridEditCommitArgs<TData extends RowData> = {
|
|
92
92
|
rowId: string;
|
|
93
|
-
/** The whole row as edited
|
|
93
|
+
/** The whole row as edited - for the consumer who saves records. */
|
|
94
94
|
value: TData;
|
|
95
95
|
/** The row as it was when editing began. */
|
|
96
96
|
original: TData;
|
|
97
|
-
/** Per-field diff
|
|
97
|
+
/** Per-field diff - for the consumer who PATCHes. One entry in cell mode. */
|
|
98
98
|
changes: Array<TMDataGridEditChange>;
|
|
99
99
|
/** Which policy committed. */
|
|
100
100
|
source: TMDataGridEditMode;
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
-
/** What one open row's form looks like from outside
|
|
103
|
+
/** What one open row's form looks like from outside - the cell markers. */
|
|
104
104
|
export type TMDataGridEditRowProjection = {
|
|
105
105
|
/** Field names whose value differs from the original. */
|
|
106
106
|
dirtyFields: ReadonlyArray<string>;
|
|
107
107
|
/** Field names carrying a validation error. */
|
|
108
108
|
errorFields: ReadonlyArray<string>;
|
|
109
|
-
/** A row-level error
|
|
109
|
+
/** A row-level error - a pathless `.refine()`, or a rejected commit. */
|
|
110
110
|
hasRowError: boolean;
|
|
111
111
|
isSubmitting: boolean;
|
|
112
112
|
};
|
|
@@ -115,7 +115,7 @@ export type TMDataGridEditRowProjection = {
|
|
|
115
115
|
* The grid-facing index of everything mid-edit. A projection synced from the
|
|
116
116
|
* open forms' stores, so body cells subscribe to this one store for their
|
|
117
117
|
* dirty/error markers instead of one store per form. Only the editor host
|
|
118
|
-
* reads a form's store directly
|
|
118
|
+
* reads a form's store directly - it is mounted for one cell at a time.
|
|
119
119
|
*/
|
|
120
120
|
export type TMDataGridEditState = {
|
|
121
121
|
/** The cell whose editor is open; `columnId: null` is a whole row (row mode). */
|
|
@@ -123,9 +123,9 @@ export type TMDataGridEditState = {
|
|
|
123
123
|
/** Rows with a live form. In cell mode at most one; in batch, many. */
|
|
124
124
|
openRowIds: ReadonlyArray<string>;
|
|
125
125
|
rows: Record<string, TMDataGridEditRowProjection>;
|
|
126
|
-
/** Phase 4
|
|
126
|
+
/** Phase 4 - rows being created, not yet in `data`. */
|
|
127
127
|
newRows: ReadonlyArray<{ tempId: string }>;
|
|
128
|
-
/** Phase 4
|
|
128
|
+
/** Phase 4 - rows marked deleted under batch mode. */
|
|
129
129
|
deletedRowIds: ReadonlyArray<string>;
|
|
130
130
|
};
|
|
131
131
|
|
|
@@ -141,14 +141,14 @@ type ErasedRow = Row<TMDataGridFeatures, TMDataGridRowData>;
|
|
|
141
141
|
type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
* What a cell editor is handed
|
|
144
|
+
* What a cell editor is handed - deliberately both vocabularies at once. The
|
|
145
145
|
* form side is TanStack Form's real `field` API (`field.state.value`,
|
|
146
146
|
* `field.state.meta.errors`, `field.handleChange`, `field.handleBlur`), so a
|
|
147
147
|
* custom calendar, slider or async combobox binds to it exactly as it would
|
|
148
148
|
* inside any TanStack Form. The table side is where the editor is standing.
|
|
149
149
|
*
|
|
150
150
|
* The built-ins are implemented against this same contract, so
|
|
151
|
-
* `meta.editor` is not a special case
|
|
151
|
+
* `meta.editor` is not a special case - it is the slot the defaults
|
|
152
152
|
* fill, and the exported built-ins can be wrapped instead of replaced.
|
|
153
153
|
*/
|
|
154
154
|
export type TMDataGridEditorArgs = {
|
|
@@ -160,9 +160,9 @@ export type TMDataGridEditorArgs = {
|
|
|
160
160
|
row: ErasedRow;
|
|
161
161
|
column: ErasedColumn;
|
|
162
162
|
table: TMDataGridTable<TMDataGridRowData>;
|
|
163
|
-
/** What Enter would do
|
|
163
|
+
/** What Enter would do - commit the edit. For the editor's own UI. */
|
|
164
164
|
commit: () => Promise<boolean>;
|
|
165
|
-
/** What Escape would do
|
|
165
|
+
/** What Escape would do - drop the draft. */
|
|
166
166
|
cancel: () => void;
|
|
167
167
|
size: TMDataGridSize;
|
|
168
168
|
/**
|
|
@@ -171,33 +171,33 @@ export type TMDataGridEditorArgs = {
|
|
|
171
171
|
*/
|
|
172
172
|
autoFocus: boolean;
|
|
173
173
|
/**
|
|
174
|
-
* Set when typing opened the editor (the Sheets gesture)
|
|
174
|
+
* Set when typing opened the editor (the Sheets gesture) - the built-ins
|
|
175
175
|
* replace the value with it and keep typing. A custom editor may ignore it.
|
|
176
176
|
*/
|
|
177
177
|
seedText?: string;
|
|
178
178
|
};
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
|
-
* `meta.editor`
|
|
181
|
+
* `meta.editor` - replaces the built-in editor for this column. Rendered as
|
|
182
182
|
* JSX, never invoked as a bare function, so hooks are legal inside. Define it
|
|
183
183
|
* at module scope: a new identity per render remounts the editor mid-edit.
|
|
184
184
|
*/
|
|
185
185
|
export type TMDataGridEditorComponent = ComponentType<TMDataGridEditorArgs>;
|
|
186
186
|
|
|
187
|
-
/** A new row being committed
|
|
187
|
+
/** A new row being committed - `onRowAdd`, and `submitAll`'s `added`. */
|
|
188
188
|
export type TMDataGridRowAddArgs<TData extends RowData> = {
|
|
189
189
|
/** The engine's placeholder id; the real id is the consumer's to mint. */
|
|
190
190
|
tempId: string;
|
|
191
191
|
value: TData;
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
/** A deletion
|
|
194
|
+
/** A deletion - `onRowDelete`. */
|
|
195
195
|
export type TMDataGridRowDeleteArgs<TData extends RowData> = {
|
|
196
196
|
rowId: string;
|
|
197
197
|
row: Row<TMDataGridFeatures, TData>;
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
/** What `submitAll` hands `onEditCommitBatch`
|
|
200
|
+
/** What `submitAll` hands `onEditCommitBatch` - everything pending at once. */
|
|
201
201
|
export type TMDataGridEditCommitBatchArgs<TData extends RowData> = {
|
|
202
202
|
/** Every valid dirty existing row. */
|
|
203
203
|
rows: Array<TMDataGridEditCommitArgs<TData>>;
|
|
@@ -207,7 +207,7 @@ export type TMDataGridEditCommitBatchArgs<TData extends RowData> = {
|
|
|
207
207
|
deleted: Array<string>;
|
|
208
208
|
};
|
|
209
209
|
|
|
210
|
-
/** What the engine reads fresh on every call
|
|
210
|
+
/** What the engine reads fresh on every call - see `createEditEngine`. */
|
|
211
211
|
export type TMDataGridEditEngineContext = {
|
|
212
212
|
table: TMDataGridTable<TMDataGridRowData>;
|
|
213
213
|
editMode: TMDataGridEditMode;
|
|
@@ -236,7 +236,7 @@ export type TMDataGridEditEngineContext = {
|
|
|
236
236
|
* nested rows work for free: `accessorKey: "address.city"` edits
|
|
237
237
|
* `values.address.city`. A column built on `accessorFn` has no path and is
|
|
238
238
|
* not editable unless `meta.editField` names one. TanStack's default column
|
|
239
|
-
* id turns dots into underscores
|
|
239
|
+
* id turns dots into underscores - which is why this starts from
|
|
240
240
|
* `accessorKey`, never from `id`.
|
|
241
241
|
*/
|
|
242
242
|
export function getEditFieldName(column: {
|
|
@@ -250,7 +250,7 @@ export function getEditFieldName(column: {
|
|
|
250
250
|
return typeof accessorKey === "string" ? accessorKey : null;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/** What Delete writes into a cell
|
|
253
|
+
/** What Delete writes into a cell - the type's honest empty value. */
|
|
254
254
|
export function clearedValueForType(type: TMDataGridColumnType): unknown {
|
|
255
255
|
switch (type) {
|
|
256
256
|
case "string":
|
|
@@ -265,14 +265,14 @@ export function clearedValueForType(type: TMDataGridColumnType): unknown {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/**
|
|
268
|
-
* The engine plus its store
|
|
268
|
+
* The engine plus its store - `api.edit`.
|
|
269
269
|
*
|
|
270
270
|
* "One row, one form": `getForm` hands out the same `FormApi` the inline
|
|
271
271
|
* editors write through, so a consumer can render it in a drawer or a detail
|
|
272
272
|
* panel and share values, dirty state and errors with the cells.
|
|
273
273
|
*/
|
|
274
274
|
export type TMDataGridEditApi = {
|
|
275
|
-
/** The projection store
|
|
275
|
+
/** The projection store - subscribe with `useSelector(edit.store, …)`. */
|
|
276
276
|
store: Store<TMDataGridEditState>;
|
|
277
277
|
/** Current snapshot, for reads outside React. */
|
|
278
278
|
readonly state: TMDataGridEditState;
|
|
@@ -283,12 +283,12 @@ export type TMDataGridEditApi = {
|
|
|
283
283
|
* switched it off, and the row takes edits at all.
|
|
284
284
|
*/
|
|
285
285
|
canEditCell: (row: ErasedRow, column: ErasedColumn) => boolean;
|
|
286
|
-
/** Whether the row takes edits at all
|
|
286
|
+
/** Whether the row takes edits at all - the edit lane's pencil gate. */
|
|
287
287
|
canEditRow: (row: ErasedRow) => boolean;
|
|
288
288
|
/** Opens an editor. `columnId: null` opens the whole row (row mode). */
|
|
289
289
|
begin: (target: { rowId: string; columnId: string | null }) => void;
|
|
290
290
|
/**
|
|
291
|
-
* Commits one row
|
|
291
|
+
* Commits one row - `form.handleSubmit` under the hood. Resolves `true`
|
|
292
292
|
* when the form is gone: validation passed and `onEditCommit` resolved, or
|
|
293
293
|
* there was nothing to save. `false` keeps the form open with its errors.
|
|
294
294
|
*/
|
|
@@ -296,31 +296,31 @@ export type TMDataGridEditApi = {
|
|
|
296
296
|
/** Drops one row's draft. */
|
|
297
297
|
cancel: (rowId: string) => void;
|
|
298
298
|
/**
|
|
299
|
-
* Closes the editor without touching the draft
|
|
299
|
+
* Closes the editor without touching the draft - what blur does under
|
|
300
300
|
* `"cellConfirm"`, where the dirty cell keeps waiting for its ✓.
|
|
301
301
|
*/
|
|
302
302
|
deactivate: () => void;
|
|
303
303
|
/** Drops every draft. */
|
|
304
304
|
cancelAll: () => void;
|
|
305
|
-
/** Commits every open row
|
|
305
|
+
/** Commits every open row - batch mode's save. `true` when all landed. */
|
|
306
306
|
submitAll: () => Promise<boolean>;
|
|
307
|
-
/** Writes the type's empty value into a cell and commits it
|
|
307
|
+
/** Writes the type's empty value into a cell and commits it - Delete. */
|
|
308
308
|
clearCell: (rowId: string, columnId: string) => Promise<boolean>;
|
|
309
309
|
/**
|
|
310
310
|
* Opens a new entry row (the sticky block under the header) seeded from
|
|
311
|
-
* `newRowDefaults`. Returns its temporary id
|
|
311
|
+
* `newRowDefaults`. Returns its temporary id - a form with no backing row
|
|
312
312
|
* yet. Committing it calls `onRowAdd` (immediate modes) or joins
|
|
313
313
|
* `submitAll`'s `added` (batch).
|
|
314
314
|
*/
|
|
315
315
|
addRow: () => string;
|
|
316
316
|
/**
|
|
317
317
|
* Deletes a row: `onRowDelete` straight away under the immediate modes;
|
|
318
|
-
* under batch it toggles the id in `deletedRowIds`
|
|
318
|
+
* under batch it toggles the id in `deletedRowIds` - the row renders
|
|
319
319
|
* struck through until `submitAll` reports it. On an uncommitted entry
|
|
320
320
|
* row it just discards the entry.
|
|
321
321
|
*/
|
|
322
322
|
deleteRow: (rowId: string) => void;
|
|
323
|
-
/** Whether delete chrome makes sense
|
|
323
|
+
/** Whether delete chrome makes sense - the lane's trash gate. */
|
|
324
324
|
canDeleteRows: () => boolean;
|
|
325
325
|
};
|
|
326
326
|
|
|
@@ -366,7 +366,7 @@ function hasAnyError(errors: ReadonlyArray<unknown>): boolean {
|
|
|
366
366
|
* sees the latest table, mode and consumer callbacks without being rebuilt.
|
|
367
367
|
*
|
|
368
368
|
* Virtualization, per the plan: nothing here is DOM. A form is a plain object
|
|
369
|
-
* in a map keyed by rowId
|
|
369
|
+
* in a map keyed by rowId - scroll its row away and the editor unmounts, the
|
|
370
370
|
* form keeps its values, meta and errors; scroll back and the editor
|
|
371
371
|
* re-mounts over the same form.
|
|
372
372
|
*/
|
|
@@ -378,11 +378,11 @@ export function createEditEngine(
|
|
|
378
378
|
type FormEntry = {
|
|
379
379
|
form: TMDataGridRowEditForm;
|
|
380
380
|
original: TMDataGridRowData;
|
|
381
|
-
/** An entry-block row
|
|
381
|
+
/** An entry-block row - a form with no backing row yet. */
|
|
382
382
|
isNew: boolean;
|
|
383
383
|
/** Set by the wrapped onSubmit when the consumer's commit resolved. */
|
|
384
384
|
lastSubmitOk: boolean;
|
|
385
|
-
/** A commit already running
|
|
385
|
+
/** A commit already running - Enter and blur race on the same edit. */
|
|
386
386
|
pendingCommit: Promise<boolean> | null;
|
|
387
387
|
unsubscribe: () => void;
|
|
388
388
|
unmount: () => void;
|
|
@@ -392,7 +392,7 @@ export function createEditEngine(
|
|
|
392
392
|
|
|
393
393
|
/**
|
|
394
394
|
* While `submitAll` runs with an `onEditCommitBatch`, each row's wrapped
|
|
395
|
-
* onSubmit contributes its args here instead of calling `onEditCommit`
|
|
395
|
+
* onSubmit contributes its args here instead of calling `onEditCommit` -
|
|
396
396
|
* validation stays per row (Form's), the consumer call becomes one.
|
|
397
397
|
*/
|
|
398
398
|
let batchCollector: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> | null =
|
|
@@ -432,7 +432,7 @@ export function createEditEngine(
|
|
|
432
432
|
const errorFields = Object.entries(state.fieldMeta)
|
|
433
433
|
.filter(
|
|
434
434
|
([name, meta]) =>
|
|
435
|
-
// The empty path is a pathless (row-level) issue's landing spot
|
|
435
|
+
// The empty path is a pathless (row-level) issue's landing spot -
|
|
436
436
|
// it belongs to `hasRowError`, not to any cell's marker.
|
|
437
437
|
name !== "" &&
|
|
438
438
|
hasAnyError((meta as { errors: ReadonlyArray<unknown> }).errors),
|
|
@@ -593,7 +593,7 @@ export function createEditEngine(
|
|
|
593
593
|
// Enter and blur race on the same edit; the second caller joins the
|
|
594
594
|
// first's commit instead of submitting the row twice.
|
|
595
595
|
if (entry.pendingCommit !== null) return entry.pendingCommit;
|
|
596
|
-
// A pristine form has nothing to say
|
|
596
|
+
// A pristine form has nothing to say - drop it without a consumer call.
|
|
597
597
|
// Not for a new row: adding an untouched entry is still an add.
|
|
598
598
|
if (
|
|
599
599
|
!entry.isNew &&
|
|
@@ -641,8 +641,8 @@ export function createEditEngine(
|
|
|
641
641
|
//
|
|
642
642
|
// | Mode | Another row open |
|
|
643
643
|
// | ---- | ---------------- |
|
|
644
|
-
// | cell | committed
|
|
645
|
-
// | row | dropped if pristine, otherwise the pencil is refused
|
|
644
|
+
// | cell | committed - Sheets; a failed commit keeps holding the edit |
|
|
645
|
+
// | row | dropped if pristine, otherwise the pencil is refused - a
|
|
646
646
|
// row edit saves explicitly, so leaving must not save quietly |
|
|
647
647
|
// | cellConfirm, batch | accumulates; every draft waits for its ✓ |
|
|
648
648
|
if (openElsewhere !== undefined) {
|
|
@@ -702,7 +702,7 @@ export function createEditEngine(
|
|
|
702
702
|
}
|
|
703
703
|
const context = getContext();
|
|
704
704
|
if (context.editMode === "batch") {
|
|
705
|
-
// A toggle: the second press unmarks
|
|
705
|
+
// A toggle: the second press unmarks - the mark is a draft too.
|
|
706
706
|
store.setState((prev) => ({
|
|
707
707
|
...prev,
|
|
708
708
|
deletedRowIds: prev.deletedRowIds.includes(rowId)
|
|
@@ -739,7 +739,7 @@ export function createEditEngine(
|
|
|
739
739
|
|
|
740
740
|
const submitAll = async (): Promise<boolean> => {
|
|
741
741
|
if (getContext().onEditCommitBatch === undefined) {
|
|
742
|
-
// The default: the per-row loop
|
|
742
|
+
// The default: the per-row loop - edits through `onEditCommit`, entry
|
|
743
743
|
// rows through `onRowAdd`, marked deletions through `onRowDelete`.
|
|
744
744
|
const results = await Promise.all(
|
|
745
745
|
[...forms.keys()].map((rowId) => commit(rowId)),
|
|
@@ -755,7 +755,7 @@ export function createEditEngine(
|
|
|
755
755
|
|
|
756
756
|
// One consumer call for the lot. Validation stays Form's, per row: rows
|
|
757
757
|
// that fail keep their forms and markers; the valid ones travel
|
|
758
|
-
// together, and only a resolved batch drops them
|
|
758
|
+
// together, and only a resolved batch drops them - a rejected save keeps
|
|
759
759
|
// every draft, deletions included.
|
|
760
760
|
const collected: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> = [];
|
|
761
761
|
const added: Array<TMDataGridRowAddArgs<TMDataGridRowData>> = [];
|
|
@@ -844,7 +844,7 @@ export function createEditEngine(
|
|
|
844
844
|
|
|
845
845
|
/**
|
|
846
846
|
* `meta.validate` normalised to the object TanStack Form's `validators`
|
|
847
|
-
* option takes
|
|
847
|
+
* option takes - a bare schema or function is shorthand for `{ onChange }`.
|
|
848
848
|
*/
|
|
849
849
|
export function normalizeFieldValidate(
|
|
850
850
|
validate: TMDataGridFieldValidate | undefined,
|
|
@@ -6,8 +6,8 @@ import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
|
6
6
|
*
|
|
7
7
|
* TanStack keeps one `expanded` state, and the grid opens two unrelated things
|
|
8
8
|
* out of it: a group row opens into its children, a data row opens into its
|
|
9
|
-
* detail panel. So `toggleAllRowsExpanded`
|
|
10
|
-
* whole-table form
|
|
9
|
+
* detail panel. So `toggleAllRowsExpanded` - which writes the state's
|
|
10
|
+
* whole-table form - is the wrong verb for either control. "Expand all groups"
|
|
11
11
|
* in the tree menu would open every panel in the grid, and the details lane's
|
|
12
12
|
* chevron would unfold the whole tree.
|
|
13
13
|
*
|
|
@@ -25,7 +25,7 @@ function isTarget<TData extends RowData>(
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* The state as a map. `true` is the whole-table form, which has to be written
|
|
28
|
-
* out before one kind of row can be taken back out of it
|
|
28
|
+
* out before one kind of row can be taken back out of it - the same thing
|
|
29
29
|
* TanStack does before toggling a single row.
|
|
30
30
|
*/
|
|
31
31
|
function toExpandedMap<TData extends RowData>(
|
|
@@ -40,7 +40,7 @@ function toExpandedMap<TData extends RowData>(
|
|
|
40
40
|
|
|
41
41
|
export type TMDataGridExpandAllArgs<TData extends RowData> = {
|
|
42
42
|
/**
|
|
43
|
-
* Every row in the model, groups and records alike
|
|
43
|
+
* Every row in the model, groups and records alike - `flatRows` off any of
|
|
44
44
|
* them. A grouped model lists its leaves both under their group and in the
|
|
45
45
|
* flat list, so this may repeat rows; both helpers are written so that a
|
|
46
46
|
* repeat costs nothing.
|
|
@@ -51,7 +51,7 @@ export type TMDataGridExpandAllArgs<TData extends RowData> = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Whether every row of this kind is open
|
|
54
|
+
* Whether every row of this kind is open - what the control shows, and what a
|
|
55
55
|
* bare toggle inverts.
|
|
56
56
|
*
|
|
57
57
|
* `false` when there are none of them, matching TanStack's own
|
|
@@ -8,13 +8,13 @@ import type { TMDataGridLabels } from "./labels";
|
|
|
8
8
|
import type { TMDataGridSize } from "./sizes";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* What a filter control is handed
|
|
11
|
+
* What a filter control is handed - the value slot of one filter-panel row.
|
|
12
12
|
*
|
|
13
13
|
* The contract is value-only: the control reads `operator` to shape itself
|
|
14
14
|
* (a `between` pair renders two ends, an `equals` a single input) and calls
|
|
15
15
|
* `onChange` with the bare value; the grid composes the operator-aware
|
|
16
16
|
* `TMDataGridFilterValue` around it. A control never builds that wrapper and
|
|
17
|
-
* never writes the operator
|
|
17
|
+
* never writes the operator - `table` is the escape hatch for the rare
|
|
18
18
|
* control that must.
|
|
19
19
|
*/
|
|
20
20
|
export type TMDataGridFilterControlArgs = {
|
|
@@ -22,13 +22,13 @@ export type TMDataGridFilterControlArgs = {
|
|
|
22
22
|
table: TMDataGridTable<TMDataGridRowData>;
|
|
23
23
|
/** The row's currently selected operator. Read-only for the control. */
|
|
24
24
|
operator: TMDataGridFilterOperator;
|
|
25
|
-
/** The bare filter value
|
|
25
|
+
/** The bare filter value - never the `{ operator, value }` wrapper. */
|
|
26
26
|
value: string | ReadonlyArray<string>;
|
|
27
27
|
/** Writes the bare value; the grid pairs it with the current operator. */
|
|
28
28
|
onChange: (next: string | ReadonlyArray<string>) => void;
|
|
29
29
|
/**
|
|
30
30
|
* The column's options through `resolveColumnOptions`, pre-resolved for a
|
|
31
|
-
* column that declares `meta.options` or is select-shaped. Empty otherwise
|
|
31
|
+
* column that declares `meta.options` or is select-shaped. Empty otherwise -
|
|
32
32
|
* a control wanting faceted values on other columns resolves them itself.
|
|
33
33
|
*/
|
|
34
34
|
options: ReadonlyArray<TMDataGridOption>;
|
|
@@ -37,7 +37,7 @@ export type TMDataGridFilterControlArgs = {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* `meta.filterControl`
|
|
40
|
+
* `meta.filterControl` - replaces the built-in value control for this column.
|
|
41
41
|
* Rendered as JSX, never invoked as a bare function, so hooks are legal
|
|
42
42
|
* inside. Define it at module scope so its identity is stable across renders.
|
|
43
43
|
*/
|
|
@@ -5,12 +5,12 @@ import type { Row, RowData, TableFeatures } from "@tanstack/react-table";
|
|
|
5
5
|
*
|
|
6
6
|
* TanStack resolves `filterFn` statically per column, so the operator travels
|
|
7
7
|
* inside the filter *value* instead. That keeps the whole filter model plain,
|
|
8
|
-
* serialisable JSON
|
|
8
|
+
* serialisable JSON - which is what makes it portable to a server-side
|
|
9
9
|
* `manualFiltering` table (just forward `columnFilters` to the API).
|
|
10
10
|
*
|
|
11
11
|
* `value` is a string array under `isAnyOf` / `isNoneOf` (the set the cell is
|
|
12
12
|
* tested against), a `[min, max]` pair under `between` (an empty string means
|
|
13
|
-
* that end is open), and a single string everywhere else
|
|
13
|
+
* that end is open), and a single string everywhere else - dates travel as
|
|
14
14
|
* ISO `YYYY-MM-DD` strings, booleans as `"true"` / `"false"`. Still plain
|
|
15
15
|
* JSON.
|
|
16
16
|
*/
|
|
@@ -176,21 +176,21 @@ export function operatorNeedsValue(operator: TMDataGridFilterOperator): boolean
|
|
|
176
176
|
return !VALUELESS_OPERATORS.includes(operator);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/** Whether the operator's value is a string array
|
|
179
|
+
/** Whether the operator's value is a string array - `isAnyOf` / `isNoneOf`. */
|
|
180
180
|
export function operatorTakesArrayValue(
|
|
181
181
|
operator: TMDataGridFilterOperator,
|
|
182
182
|
): boolean {
|
|
183
183
|
return ARRAY_OPERATORS.includes(operator);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/** Whether the operator's value is a `[min, max]` pair
|
|
186
|
+
/** Whether the operator's value is a `[min, max]` pair - `between`. */
|
|
187
187
|
export function operatorTakesRangeValue(
|
|
188
188
|
operator: TMDataGridFilterOperator,
|
|
189
189
|
): boolean {
|
|
190
190
|
return RANGE_OPERATORS.includes(operator);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/** The untouched value a fresh filter starts with
|
|
193
|
+
/** The untouched value a fresh filter starts with - the operator's shape, empty. */
|
|
194
194
|
export function emptyValueForOperator(
|
|
195
195
|
operator: TMDataGridFilterOperator,
|
|
196
196
|
): string | ReadonlyArray<string> {
|
|
@@ -228,8 +228,8 @@ export function isFilterActive(value: unknown): boolean {
|
|
|
228
228
|
/**
|
|
229
229
|
* One-line description of a single filter, as shown on a filter pill.
|
|
230
230
|
*
|
|
231
|
-
* The type's default operator is left implicit
|
|
232
|
-
* way a person would say it
|
|
231
|
+
* The type's default operator is left implicit - "First name: Sofia" reads the
|
|
232
|
+
* way a person would say it - while any other operator is spelled out, since
|
|
233
233
|
* that is the part a reader cannot guess.
|
|
234
234
|
*/
|
|
235
235
|
export function formatFilterLabel({
|
|
@@ -241,7 +241,7 @@ export function formatFilterLabel({
|
|
|
241
241
|
label: string;
|
|
242
242
|
type: TMDataGridColumnType;
|
|
243
243
|
filter: TMDataGridFilterValue;
|
|
244
|
-
/** Localized operator names
|
|
244
|
+
/** Localized operator names - `labels.operators`. Defaults to English. */
|
|
245
245
|
operatorLabels?: Record<TMDataGridFilterOperator, string>;
|
|
246
246
|
}): string {
|
|
247
247
|
const { operator, value } = filter;
|
|
@@ -260,8 +260,8 @@ export function formatFilterLabel({
|
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* A value's calendar day as `YYYY-MM-DD`, or `null` for anything that has no
|
|
263
|
-
* day to speak of. A `Date` is read in local time
|
|
264
|
-
* which is the shape `<input type="date">` produced on the other side
|
|
263
|
+
* day to speak of. A `Date` is read in local time - `sv-SE` formats as ISO,
|
|
264
|
+
* which is the shape `<input type="date">` produced on the other side - and a
|
|
265
265
|
* string contributes its leading ISO day, so both `2026-07-31` and a full
|
|
266
266
|
* timestamp compare by day.
|
|
267
267
|
*/
|
|
@@ -317,8 +317,8 @@ export function matchesFilter(
|
|
|
317
317
|
if (operator === "isEmpty") return isEmptyCell(cellValue);
|
|
318
318
|
if (operator === "isNotEmpty") return !isEmptyCell(cellValue);
|
|
319
319
|
|
|
320
|
-
// Set membership. The cell side is also a list
|
|
321
|
-
// array
|
|
320
|
+
// Set membership. The cell side is also a list - a multiSelect cell holds an
|
|
321
|
+
// array - so "any of" is an intersection test and "none of" its complement.
|
|
322
322
|
if (operator === "isAnyOf" || operator === "isNoneOf") {
|
|
323
323
|
const set = (Array.isArray(value) ? value : [value])
|
|
324
324
|
.map((entry) => String(entry).toLowerCase())
|
|
@@ -332,7 +332,7 @@ export function matchesFilter(
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
// A closed or half-open interval. Each present end must hold; an empty end
|
|
335
|
-
// is open
|
|
335
|
+
// is open - and a fully empty pair matches everything, the same way a blank
|
|
336
336
|
// needle does. Ends compare the way the single-ended operators would:
|
|
337
337
|
// numerically when both sides are numbers, by calendar day otherwise.
|
|
338
338
|
if (operator === "between") {
|
|
@@ -349,7 +349,7 @@ export function matchesFilter(
|
|
|
349
349
|
if (filterText === "") return true;
|
|
350
350
|
|
|
351
351
|
// Day ordering. ISO days compare correctly as strings, which keeps this
|
|
352
|
-
// free of timezone arithmetic
|
|
352
|
+
// free of timezone arithmetic - the whole comparison lives in local days.
|
|
353
353
|
switch (operator) {
|
|
354
354
|
case "before":
|
|
355
355
|
case "after":
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./filterOperators";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Every user-facing string in the grid
|
|
7
|
+
* Every user-facing string in the grid - menu items, panels, tooltips, the
|
|
8
8
|
* pager, and the `aria-label`s a screen reader speaks. One flat object, so an
|
|
9
9
|
* override is a spread rather than a deep merge; `operators` is the single
|
|
10
10
|
* nested record and {@link mergeLabels} folds it separately.
|
|
@@ -17,9 +17,9 @@ export type TMDataGridLabels = {
|
|
|
17
17
|
operators: Record<TMDataGridFilterOperator, string>;
|
|
18
18
|
|
|
19
19
|
// Toolbar
|
|
20
|
-
/** "Manage columns"
|
|
20
|
+
/** "Manage columns" - the burger button and the header menu item. */
|
|
21
21
|
manageColumns: string;
|
|
22
|
-
/** "Filters"
|
|
22
|
+
/** "Filters" - the funnel button and the filter panel's title. */
|
|
23
23
|
filters: string;
|
|
24
24
|
searchPlaceholder: string;
|
|
25
25
|
searchLabel: string;
|
|
@@ -35,7 +35,7 @@ export type TMDataGridLabels = {
|
|
|
35
35
|
/** Menu/panel name of the generated row-number gutter. */
|
|
36
36
|
rowNumberColumnLabel: string;
|
|
37
37
|
/**
|
|
38
|
-
* The truly-empty message
|
|
38
|
+
* The truly-empty message - no data and no filters. Filtered-empty says
|
|
39
39
|
* {@link noResults} instead; only one of the two is the user's own doing.
|
|
40
40
|
*/
|
|
41
41
|
noRows: string;
|
|
@@ -51,7 +51,7 @@ export type TMDataGridLabels = {
|
|
|
51
51
|
filterTo: string;
|
|
52
52
|
/** `DgTriStateFilter`'s no-filter segment. */
|
|
53
53
|
filterAll: string;
|
|
54
|
-
/** What a boolean column's `true` reads as
|
|
54
|
+
/** What a boolean column's `true` reads as - its filter choice and cell text. */
|
|
55
55
|
booleanTrue: string;
|
|
56
56
|
booleanFalse: string;
|
|
57
57
|
addFilter: string;
|
|
@@ -115,9 +115,9 @@ export type TMDataGridLabels = {
|
|
|
115
115
|
saveAllEdits: (rows: number) => string;
|
|
116
116
|
/** `EditActions`' Discard. */
|
|
117
117
|
discardAllEdits: string;
|
|
118
|
-
/** The entry row's ✓
|
|
118
|
+
/** The entry row's ✓ - commit the add. */
|
|
119
119
|
confirmNewRow: string;
|
|
120
|
-
/** The entry row's ✕
|
|
120
|
+
/** The entry row's ✕ - drop the entry. */
|
|
121
121
|
discardNewRow: string;
|
|
122
122
|
/** The lane's trash can. */
|
|
123
123
|
deleteRow: string;
|
|
@@ -152,7 +152,7 @@ export type TMDataGridLabels = {
|
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* What the `labels` option accepts: any subset, merged over the English
|
|
155
|
-
* defaults
|
|
155
|
+
* defaults - so `{ noResults: "Inga rader" }` is a complete configuration.
|
|
156
156
|
*/
|
|
157
157
|
export type TMDataGridLabelsOverride = Partial<
|
|
158
158
|
Omit<TMDataGridLabels, "operators">
|
|
@@ -9,7 +9,7 @@ import type { TMDataGridTable } from "../useTMDataGrid";
|
|
|
9
9
|
export type TMDataGridMatchRange = { start: number; end: number };
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* The operators whose match is a contiguous substring
|
|
12
|
+
* The operators whose match is a contiguous substring - the ones highlighting
|
|
13
13
|
* can point at. Equality highlights nothing: marking the whole cell says
|
|
14
14
|
* nothing the filter did not already say.
|
|
15
15
|
*/
|
|
@@ -20,14 +20,14 @@ const HIGHLIGHTABLE_OPERATORS: ReadonlyArray<TMDataGridFilterOperator> = [
|
|
|
20
20
|
];
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Every needle that could highlight in a given column, keyed by column id
|
|
23
|
+
* Every needle that could highlight in a given column, keyed by column id -
|
|
24
24
|
* the quick search for the columns it searches, plus any contains-family
|
|
25
25
|
* column filter. `null` while nothing highlightable is active, which is the
|
|
26
26
|
* common case and the cheap one.
|
|
27
27
|
*
|
|
28
28
|
* The fuzzy quick search still contributes its raw text: when the needle
|
|
29
29
|
* occurs contiguously it is highlighted, and a typo-match simply shows no
|
|
30
|
-
* highlight
|
|
30
|
+
* highlight - the honest answer to what a non-contiguous match "is".
|
|
31
31
|
*/
|
|
32
32
|
export function buildMatchNeedles<TData extends RowData>(
|
|
33
33
|
table: TMDataGridTable<TData>,
|
|
@@ -62,7 +62,7 @@ export function buildMatchNeedles<TData extends RowData>(
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* Where the needles occur in the text, case-insensitively, merged where they
|
|
65
|
-
* overlap or touch
|
|
65
|
+
* overlap or touch - so two needles covering "St" and "tock" come back as one
|
|
66
66
|
* range rather than nested marks. `null` when nothing matches.
|
|
67
67
|
*/
|
|
68
68
|
export function findMatchRanges(
|