@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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.
Files changed (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -1,1006 +0,0 @@
1
- import {
2
- FormApi,
3
- getBy,
4
- type AnyFieldApi,
5
- type AnyFormApi,
6
- type StandardSchemaV1,
7
- } from "@tanstack/react-form";
8
- import { Store } from "@tanstack/store";
9
- import type { Cell, Column, Row, RowData } from "@tanstack/react-table";
10
- import type { ComponentType } from "react";
11
- import type { TMDataGridRowData } from "../TMDataGridContext";
12
- import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
13
- import {
14
- isColumnEditableForRow,
15
- isColumnEditSwitchedOff,
16
- isControlColumn,
17
- } from "./columnUtils";
18
- import type { TMDataGridColumnType } from "./filterOperators";
19
- import type { TMDataGridSize } from "./sizes";
20
-
21
- /**
22
- * How commits happen - one axis, each mode a thin policy over the same
23
- * engine. See `editMode` on `UseTMDataGridOptions`.
24
- */
25
- export type TMDataGridEditMode = "cell" | "cellConfirm" | "row" | "draft";
26
-
27
- /**
28
- * One editing row's live form. TanStack Form's `FormApi`, not a wrapper: the
29
- * engine is a form library, and everything mid-edit - values, dirty state,
30
- * errors, async validation, submit lifecycle - is form state, read off
31
- * `form.store` with the same selector idiom as the rest of the grid.
32
- *
33
- * The alias erases Form's validator generics the way `TMDataGridRowData`
34
- * erases the row type: the concrete types lived at the `useTMDataGrid` call.
35
- */
36
- export type TMDataGridRowEditForm = AnyFormApi;
37
-
38
- /** The live field a cell editor binds to - TanStack Form's own `FieldApi`. */
39
- export type TMDataGridEditField = AnyFieldApi;
40
-
41
- /**
42
- * A validator as TanStack Form takes it: a Standard Schema (Zod, Valibot,
43
- * ArkType…) or a plain function returning an error or nothing. Forwarded
44
- * untouched - the vocabulary is Form's, not a grid re-implementation.
45
- */
46
- export type TMDataGridValidator =
47
- | StandardSchemaV1<unknown, unknown>
48
- | ((args: { value: never; fieldApi: never }) => unknown);
49
-
50
- /**
51
- * `meta.edit.validate` - per-column field validators. A bare schema or function is
52
- * shorthand for `{ onChange: it }`; the object form takes every trigger
53
- * TanStack Form defines, `Async` variants and debounce included.
54
- */
55
- export type TMDataGridFieldValidate =
56
- | TMDataGridValidator
57
- | {
58
- onMount?: TMDataGridValidator;
59
- onChange?: TMDataGridValidator;
60
- onChangeAsync?: TMDataGridValidator;
61
- onChangeAsyncDebounceMs?: number;
62
- onBlur?: TMDataGridValidator;
63
- onBlurAsync?: TMDataGridValidator;
64
- onSubmit?: TMDataGridValidator;
65
- onSubmitAsync?: TMDataGridValidator;
66
- };
67
-
68
- /**
69
- * `rowValidators` - form-level validators for the whole row, where
70
- * cross-field rules live. Passed into each row form's `validators` untouched;
71
- * pathed issues land on the matching fields, pathless ones on the row.
72
- */
73
- export type TMDataGridRowValidators = {
74
- onMount?: TMDataGridValidator;
75
- onChange?: TMDataGridValidator;
76
- onChangeAsync?: TMDataGridValidator;
77
- onChangeAsyncDebounceMs?: number;
78
- onBlur?: TMDataGridValidator;
79
- onBlurAsync?: TMDataGridValidator;
80
- onSubmit?: TMDataGridValidator;
81
- onSubmitAsync?: TMDataGridValidator;
82
- onSubmitAsyncDebounceMs?: number;
83
- };
84
-
85
- /** One field's before/after across a commit. */
86
- export type TMDataGridEditChange = {
87
- /** Column the field maps back to, for consumers thinking in columns. */
88
- columnId: string;
89
- /** The data path - `getEditFieldName` of the column. */
90
- field: string;
91
- previous: unknown;
92
- next: unknown;
93
- };
94
-
95
- export type TMDataGridEditCommitArgs<TData extends RowData> = {
96
- rowId: string;
97
- /** The whole row as edited - for the consumer who saves records. */
98
- value: TData;
99
- /** The row as it was when editing began. */
100
- original: TData;
101
- /** Per-field diff - for the consumer who PATCHes. One entry in cell mode. */
102
- changes: Array<TMDataGridEditChange>;
103
- /** Which policy committed. */
104
- source: TMDataGridEditMode;
105
- };
106
-
107
- /** What one open row's form looks like from outside - the cell markers. */
108
- export type TMDataGridEditRowProjection = {
109
- /** Field names whose value differs from the original. */
110
- dirtyFields: ReadonlyArray<string>;
111
- /** Field names carrying a validation error. */
112
- errorFields: ReadonlyArray<string>;
113
- /** A row-level error - a pathless `.refine()`, or a rejected commit. */
114
- hasRowError: boolean;
115
- isSubmitting: boolean;
116
- /**
117
- * The row as drafted - the form's `state.values`. Reference-stable while no
118
- * value changes, so a cell may subscribe to it and repaint only when its
119
- * row's draft actually moves.
120
- */
121
- values: TMDataGridRowData;
122
- };
123
-
124
- /**
125
- * The grid-facing index of everything mid-edit. A projection synced from the
126
- * open forms' stores, so body cells subscribe to this one store for their
127
- * dirty/error markers instead of one store per form. Only the editor host
128
- * reads a form's store directly - it is mounted for one cell at a time.
129
- */
130
- export type TMDataGridEditState = {
131
- /**
132
- * The cell the last open gesture named - where the caret goes. In row mode
133
- * the whole row is editing whatever this says, so there it is only the
134
- * caret's cell, and `null` for the lane's pencil, which names no column.
135
- */
136
- active: { rowId: string; columnId: string | null } | null;
137
- /**
138
- * Rows with a live form. In cell mode at most one; in row, cellConfirm and
139
- * draft, as many as the user opened - which rows are editing.
140
- */
141
- openRowIds: ReadonlyArray<string>;
142
- rows: Record<string, TMDataGridEditRowProjection>;
143
- /**
144
- * Rows being created, not yet in `data`. `confirmed` is draft mode's
145
- * "entered, awaiting Save all": the entry row renders as a value row until
146
- * `begin` re-opens it. Under the immediate modes a confirm commits through
147
- * `onRowAdd` and the entry is dropped, so there it never turns `true`.
148
- */
149
- newRows: ReadonlyArray<{ tempId: string; confirmed: boolean }>;
150
- /** Rows marked deleted under draft mode. */
151
- deletedRowIds: ReadonlyArray<string>;
152
- };
153
-
154
- const EMPTY_EDIT_STATE: TMDataGridEditState = {
155
- active: null,
156
- openRowIds: [],
157
- rows: {},
158
- newRows: [],
159
- deletedRowIds: [],
160
- };
161
-
162
- type ErasedRow = Row<TMDataGridFeatures, TMDataGridRowData>;
163
- type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
164
-
165
- /**
166
- * What a cell editor is handed - deliberately both vocabularies at once. The
167
- * form side is TanStack Form's real `field` API (`field.state.value`,
168
- * `field.state.meta.errors`, `field.handleChange`, `field.handleBlur`), so a
169
- * custom calendar, slider or async combobox binds to it exactly as it would
170
- * inside any TanStack Form. The table side is where the editor is standing.
171
- *
172
- * The built-ins are implemented against this same contract, so
173
- * `meta.edit.editor` is not a special case - it is the slot the defaults
174
- * fill, and the exported built-ins can be wrapped instead of replaced.
175
- */
176
- export type TMDataGridEditorArgs = {
177
- /** The live TanStack Form field for this cell. */
178
- field: TMDataGridEditField;
179
- /** The whole row form, for the rare editor that reads sibling fields. */
180
- form: TMDataGridRowEditForm;
181
- cell: Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
182
- row: ErasedRow;
183
- column: ErasedColumn;
184
- table: TMDataGridTable<TMDataGridRowData>;
185
- /** What Enter would do - commit the edit. For the editor's own UI. */
186
- commit: () => Promise<boolean>;
187
- /** What Escape would do - drop the draft. */
188
- cancel: () => void;
189
- size: TMDataGridSize;
190
- /**
191
- * Set when typing opened the editor (the Sheets gesture) - the built-ins
192
- * replace the value with it and keep typing. A custom editor may ignore it.
193
- */
194
- seedText?: string;
195
- };
196
-
197
- /**
198
- * `meta.edit.editor` - replaces the built-in editor for this column. Rendered
199
- * as JSX, never invoked as a bare function, so hooks are legal inside. Define
200
- * it at module scope: a new identity per render remounts the editor mid-edit.
201
- */
202
- export type TMDataGridEditorComponent = ComponentType<TMDataGridEditorArgs>;
203
-
204
- /** What `meta.edit.mapValue` is handed for one write. */
205
- export type TMDataGridEditValueMapArgs = {
206
- /** The value the editor just wrote. */
207
- value: unknown;
208
- /** What the field held before this write - for length-aware masks. */
209
- previous: unknown;
210
- row: ErasedRow;
211
- column: ErasedColumn;
212
- table: TMDataGridTable<TMDataGridRowData>;
213
- };
214
-
215
- /**
216
- * `meta.edit.mapValue` - the value on its way into the draft, mapped.
217
- *
218
- * Runs on every write an editor makes, which for a text input is every
219
- * keystroke and for a select is every pick, so the mapped value is what the
220
- * user sees, what validators judge and what commits.
221
- */
222
- export type TMDataGridEditValueMap = (
223
- args: TMDataGridEditValueMapArgs,
224
- ) => unknown;
225
-
226
- /**
227
- * `meta.edit` - everything about how this column is edited, in one place,
228
- * mirroring the `edit.*` engine the grid exposes at runtime.
229
- *
230
- * ```tsx
231
- * meta: {
232
- * type: "string",
233
- * edit: {
234
- * enabled: (row) => row.original.status !== "Locked",
235
- * validate: z.string().min(2, "Too short"),
236
- * mapValue: ({ value }) =>
237
- * typeof value === "string" ? value.toUpperCase() : value,
238
- * },
239
- * }
240
- * ```
241
- *
242
- * `meta.type` and `meta.options` stay outside this namespace on purpose: one
243
- * declaration of each feeds the cell editor and the filter panel alike.
244
- */
245
- export type TMDataGridColumnEditOptions = {
246
- /**
247
- * Whether this column's cells take edits, once `editMode` is on. `false`
248
- * switches the column off outright; a predicate decides per row. Defaults
249
- * to editable for any column that maps to a field - see {@link field}.
250
- */
251
- enabled?:
252
- | boolean
253
- | ((row: Row<TMDataGridFeatures, TMDataGridRowData>) => boolean);
254
- /**
255
- * The data path this column edits, when it is not the `accessorKey` - the
256
- * only way a column built on `accessorFn` becomes editable. Dot paths reach
257
- * into nested records: `"address.city"`.
258
- */
259
- field?: string;
260
- /**
261
- * Replaces the built-in editor for this column. See
262
- * {@link TMDataGridEditorComponent}.
263
- */
264
- editor?: TMDataGridEditorComponent;
265
- /**
266
- * Field-level validators, in TanStack Form's own vocabulary. A bare Zod
267
- * schema (or any Standard Schema, or a plain function) means
268
- * `{ onChange: it }`; the object form takes every trigger Form defines.
269
- */
270
- validate?: TMDataGridFieldValidate;
271
- /**
272
- * Maps every value an editor writes before it reaches the draft - uppercase
273
- * a code, strip spaces from an IBAN, clamp a number into range. See
274
- * {@link TMDataGridEditValueMap}.
275
- */
276
- mapValue?: TMDataGridEditValueMap;
277
- };
278
-
279
- /** A new row being committed - `onRowAdd`, and `submitAll`'s `added`. */
280
- export type TMDataGridRowAddArgs<TData extends RowData> = {
281
- /** The engine's placeholder id; the real id is the consumer's to mint. */
282
- tempId: string;
283
- value: TData;
284
- };
285
-
286
- /** A deletion - `onRowDelete`. */
287
- export type TMDataGridRowDeleteArgs<TData extends RowData> = {
288
- rowId: string;
289
- row: Row<TMDataGridFeatures, TData>;
290
- };
291
-
292
- /** What `submitAll` hands `onEditCommitDrafts` - everything pending at once. */
293
- export type TMDataGridEditCommitDraftsArgs<TData extends RowData> = {
294
- /** Every valid dirty existing row. */
295
- rows: Array<TMDataGridEditCommitArgs<TData>>;
296
- /** Every valid new row from the entry block. */
297
- added: Array<TMDataGridRowAddArgs<TData>>;
298
- /** Ids marked deleted while the drafts accumulated. */
299
- deleted: Array<string>;
300
- };
301
-
302
- /** What the engine reads fresh on every call - see `createEditEngine`. */
303
- export type TMDataGridEditEngineContext = {
304
- table: TMDataGridTable<TMDataGridRowData>;
305
- editMode: TMDataGridEditMode;
306
- rowValidators?: TMDataGridRowValidators;
307
- isRowEditable?: (row: ErasedRow) => boolean;
308
- onEditCommit?: (
309
- args: TMDataGridEditCommitArgs<TMDataGridRowData>,
310
- ) => void | Promise<void>;
311
- onEditCommitDrafts?: (
312
- args: TMDataGridEditCommitDraftsArgs<TMDataGridRowData>,
313
- ) => void | Promise<void>;
314
- /** Seed values for `addRow`. A function is called per added row. */
315
- newRowDefaults?: TMDataGridRowData | (() => TMDataGridRowData);
316
- onRowAdd?: (
317
- args: TMDataGridRowAddArgs<TMDataGridRowData>,
318
- ) => void | Promise<void>;
319
- onRowDelete?: (
320
- args: TMDataGridRowDeleteArgs<TMDataGridRowData>,
321
- ) => void | Promise<void>;
322
- };
323
-
324
- /**
325
- * The data path a column edits, or `null` for a column that has none.
326
- *
327
- * `accessorKey` is the true path and Form addresses fields by dot-path, so
328
- * nested rows need no extra handling: `accessorKey: "address.city"` edits
329
- * `values.address.city`. A column built on `accessorFn` has no path and is not
330
- * editable unless `meta.edit.field` names one. TanStack's default column id
331
- * turns dots into underscores, which is why this starts from `accessorKey` and
332
- * never from `id`.
333
- */
334
- export function getEditFieldName(column: {
335
- columnDef: { meta?: { edit?: { field?: string } } };
336
- }): string | null {
337
- const fromMeta = column.columnDef.meta?.edit?.field;
338
- if (fromMeta) return fromMeta;
339
- const accessorKey = (column.columnDef as Record<string, unknown>)[
340
- "accessorKey"
341
- ];
342
- return typeof accessorKey === "string" ? accessorKey : null;
343
- }
344
-
345
- /** What Delete writes into a cell - the type's honest empty value. */
346
- export function clearedValueForType(type: TMDataGridColumnType): unknown {
347
- switch (type) {
348
- case "string":
349
- return "";
350
- case "multiSelect":
351
- return [];
352
- case "boolean":
353
- return false;
354
- default:
355
- return null;
356
- }
357
- }
358
-
359
- /**
360
- * The engine plus its store - `api.edit`.
361
- *
362
- * "One row, one form": `getForm` hands out the same `FormApi` the inline
363
- * editors write through, so a consumer can render it in a drawer or a detail
364
- * panel and share values, dirty state and errors with the cells.
365
- */
366
- export type TMDataGridEditApi = {
367
- /** The projection store - subscribe with `useSelector(edit.store, …)`. */
368
- store: Store<TMDataGridEditState>;
369
- /** Current snapshot, for reads outside React. */
370
- readonly state: TMDataGridEditState;
371
- /** rowId → live form. The source of truth for everything mid-edit. */
372
- getForm: (rowId: string) => TMDataGridRowEditForm | undefined;
373
- /**
374
- * Whether this cell may open an editor: the column maps to a field, nothing
375
- * switched it off, and the row takes edits at all.
376
- */
377
- canEditCell: (row: ErasedRow, column: ErasedColumn) => boolean;
378
- /** Whether the row takes edits at all - the edit lane's pencil gate. */
379
- canEditRow: (row: ErasedRow) => boolean;
380
- /**
381
- * Opens an editor. In row mode the whole row opens either way and `columnId`
382
- * only says which cell takes the caret; `null` (the lane's pencil) leaves it
383
- * to the row's first editable cell.
384
- */
385
- begin: (target: { rowId: string; columnId: string | null }) => void;
386
- /**
387
- * Commits one row - `form.handleSubmit` under the hood. Resolves `true`
388
- * when the form is gone: validation passed and `onEditCommit` resolved, or
389
- * there was nothing to save. `false` keeps the form open with its errors.
390
- */
391
- commit: (rowId: string) => Promise<boolean>;
392
- /** Drops one row's draft. */
393
- cancel: (rowId: string) => void;
394
- /**
395
- * Closes the editor without touching the draft - what blur does under
396
- * `"cellConfirm"`, where the dirty cell keeps waiting for its ✓.
397
- */
398
- deactivate: () => void;
399
- /** Drops every draft. */
400
- cancelAll: () => void;
401
- /** Commits every open row - draft mode's save. `true` when all landed. */
402
- submitAll: () => Promise<boolean>;
403
- /** Writes the type's empty value into a cell and commits it - Delete. */
404
- clearCell: (rowId: string, columnId: string) => Promise<boolean>;
405
- /**
406
- * Opens a new entry row (the sticky block under the header) seeded from
407
- * `newRowDefaults`. Returns its temporary id - a form with no backing row
408
- * yet. Committing it calls `onRowAdd` (immediate modes) or joins
409
- * `submitAll`'s `added` (draft).
410
- */
411
- addRow: () => string;
412
- /**
413
- * Deletes a row: `onRowDelete` straight away under the immediate modes;
414
- * under draft it toggles the id in `deletedRowIds` - the row renders
415
- * struck through until `submitAll` reports it. On an uncommitted entry
416
- * row it just discards the entry.
417
- */
418
- deleteRow: (rowId: string) => void;
419
- /** Whether delete chrome makes sense - the lane's trash gate. */
420
- canDeleteRows: () => boolean;
421
- };
422
-
423
- function clearFormSourcedFieldErrors(form: TMDataGridRowEditForm): void {
424
- const fieldMeta = form.state.fieldMeta as Record<
425
- string,
426
- { errorSourceMap?: Record<string, unknown> }
427
- >;
428
- for (const [name, meta] of Object.entries(fieldMeta)) {
429
- const staleKeys = Object.entries(meta.errorSourceMap ?? {})
430
- .filter(([, source]) => source === "form")
431
- .map(([key]) => key);
432
- if (staleKeys.length === 0) continue;
433
- form.setFieldMeta(name as never, (previous) => ({
434
- ...previous,
435
- errorMap: {
436
- ...previous.errorMap,
437
- ...Object.fromEntries(staleKeys.map((key) => [key, undefined])),
438
- },
439
- }));
440
- }
441
- }
442
-
443
- /** Arrays compare by element so a multiSelect edit-back-to-same is clean. */
444
- function sameValue(a: unknown, b: unknown): boolean {
445
- if (Object.is(a, b)) return true;
446
- if (Array.isArray(a) && Array.isArray(b)) {
447
- return a.length === b.length && a.every((entry, i) => Object.is(entry, b[i]));
448
- }
449
- if (a instanceof Date && b instanceof Date) {
450
- return a.getTime() === b.getTime();
451
- }
452
- return false;
453
- }
454
-
455
- function hasAnyError(errors: ReadonlyArray<unknown>): boolean {
456
- return errors.some((error) => error !== undefined && error !== null);
457
- }
458
-
459
- /**
460
- * Builds the edit engine. A factory over React so it is headless-testable;
461
- * `getContext` is read fresh on every call, which is how the engine always
462
- * sees the latest table, mode and consumer callbacks without being rebuilt.
463
- *
464
- * Virtualization, per the plan: nothing here is DOM. A form is a plain object
465
- * in a map keyed by rowId - scroll its row away and the editor unmounts, the
466
- * form keeps its values, meta and errors; scroll back and the editor
467
- * re-mounts over the same form.
468
- */
469
- export function createEditEngine(
470
- getContext: () => TMDataGridEditEngineContext,
471
- ): TMDataGridEditApi {
472
- const store = new Store<TMDataGridEditState>(EMPTY_EDIT_STATE);
473
-
474
- type FormEntry = {
475
- form: TMDataGridRowEditForm;
476
- original: TMDataGridRowData;
477
- /** An entry-block row - a form with no backing row yet. */
478
- isNew: boolean;
479
- /** Set by the wrapped onSubmit when the consumer's commit resolved. */
480
- lastSubmitOk: boolean;
481
- /**
482
- * Draft mode's park: this submit validates and holds the draft in the
483
- * grid instead of calling the consumer. Set by `commit` per attempt -
484
- * `true` only under draft outside `submitAll`, which is what closes the
485
- * per-row escape hatches (the lane's ✓, Delete-to-clear) at the engine.
486
- */
487
- parkOnly: boolean;
488
- /** A commit already running - Enter and blur race on the same edit. */
489
- pendingCommit: Promise<boolean> | null;
490
- unsubscribe: () => void;
491
- unmount: () => void;
492
- };
493
- const forms = new Map<string, FormEntry>();
494
- let newRowCounter = 0;
495
- /** Lets `commit` tell `submitAll`'s per-row loop apart from a lone call. */
496
- let submitAllRunning = false;
497
-
498
- /**
499
- * While `submitAll` runs with an `onEditCommitDrafts`, each row's wrapped
500
- * onSubmit contributes its args here instead of calling `onEditCommit` -
501
- * validation stays per row (Form's), the consumer call becomes one.
502
- */
503
- let draftCollector: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> | null =
504
- null;
505
- let draftAddCollector: Array<TMDataGridRowAddArgs<TMDataGridRowData>> | null =
506
- null;
507
-
508
- const editableColumns = (): Array<ErasedColumn> =>
509
- getContext()
510
- .table.getAllLeafColumns()
511
- .filter(
512
- (column) =>
513
- !isControlColumn(column.id) &&
514
- !isColumnEditSwitchedOff(column) &&
515
- getEditFieldName(column) !== null,
516
- );
517
-
518
- const diff = (
519
- entry: FormEntry,
520
- values: TMDataGridRowData,
521
- ): Array<TMDataGridEditChange> => {
522
- const changes: Array<TMDataGridEditChange> = [];
523
- for (const column of editableColumns()) {
524
- const field = getEditFieldName(column);
525
- if (field === null) continue;
526
- const previous: unknown = getBy(entry.original, field);
527
- const next: unknown = getBy(values, field);
528
- if (!sameValue(previous, next)) {
529
- changes.push({ columnId: column.id, field, previous, next });
530
- }
531
- }
532
- return changes;
533
- };
534
-
535
- const project = (entry: FormEntry): TMDataGridEditRowProjection => {
536
- const state = entry.form.state;
537
- const errorFields = Object.entries(state.fieldMeta)
538
- .filter(
539
- ([name, meta]) =>
540
- // The empty path is a pathless (row-level) issue's landing spot -
541
- // it belongs to `hasRowError`, not to any cell's marker.
542
- name !== "" &&
543
- hasAnyError((meta as { errors: ReadonlyArray<unknown> }).errors),
544
- )
545
- .map(([name]) => name);
546
- return {
547
- dirtyFields: diff(entry, state.values as TMDataGridRowData).map(
548
- (change) => change.field,
549
- ),
550
- errorFields,
551
- hasRowError: hasAnyError(state.errors),
552
- isSubmitting: state.isSubmitting,
553
- values: state.values as TMDataGridRowData,
554
- };
555
- };
556
-
557
- const publishRow = (rowId: string) => {
558
- const entry = forms.get(rowId);
559
- store.setState((prev) => {
560
- const rows = { ...prev.rows };
561
- if (entry === undefined) delete rows[rowId];
562
- else rows[rowId] = project(entry);
563
- return { ...prev, rows, openRowIds: [...forms.keys()] };
564
- });
565
- };
566
-
567
- const setActive = (active: TMDataGridEditState["active"]) => {
568
- store.setState((prev) => ({ ...prev, active }));
569
- };
570
-
571
- const setNewRowConfirmed = (tempId: string, confirmed: boolean) => {
572
- store.setState((prev) => {
573
- const target = prev.newRows.find((newRow) => newRow.tempId === tempId);
574
- if (target === undefined || target.confirmed === confirmed) return prev;
575
- return {
576
- ...prev,
577
- newRows: prev.newRows.map((newRow) =>
578
- newRow.tempId === tempId ? { ...newRow, confirmed } : newRow,
579
- ),
580
- };
581
- });
582
- };
583
-
584
- const drop = (rowId: string) => {
585
- const entry = forms.get(rowId);
586
- if (entry === undefined) return;
587
- entry.unsubscribe();
588
- entry.unmount();
589
- forms.delete(rowId);
590
- store.setState((prev) => {
591
- const rows = { ...prev.rows };
592
- delete rows[rowId];
593
- return {
594
- ...prev,
595
- rows,
596
- openRowIds: [...forms.keys()],
597
- newRows: entry.isNew
598
- ? prev.newRows.filter((newRow) => newRow.tempId !== rowId)
599
- : prev.newRows,
600
- active: prev.active?.rowId === rowId ? null : prev.active,
601
- };
602
- });
603
- };
604
-
605
- const createForm = (
606
- rowId: string,
607
- original: TMDataGridRowData,
608
- isNew: boolean,
609
- ): FormEntry => {
610
- const context = getContext();
611
- const entry: FormEntry = {
612
- original,
613
- isNew,
614
- lastSubmitOk: false,
615
- parkOnly: false,
616
- pendingCommit: null,
617
- unsubscribe: () => {},
618
- unmount: () => {},
619
- form: new FormApi({
620
- defaultValues: original,
621
- // The consumer's vocabulary is Form's own; the cast erases the
622
- // validator generics the same way the row type is erased.
623
- validators: context.rowValidators as never,
624
- onSubmit: async ({ value }) => {
625
- // A new row commits whole: it is an add, not a diff against
626
- // anything that exists.
627
- if (entry.isNew) {
628
- const addArgs: TMDataGridRowAddArgs<TMDataGridRowData> = {
629
- tempId: rowId,
630
- value: value as TMDataGridRowData,
631
- };
632
- if (draftAddCollector !== null) {
633
- draftAddCollector.push(addArgs);
634
- entry.lastSubmitOk = true;
635
- return;
636
- }
637
- // Parked: validated and held for Save all - no consumer call.
638
- if (entry.parkOnly) {
639
- entry.lastSubmitOk = true;
640
- return;
641
- }
642
- try {
643
- await getContext().onRowAdd?.(addArgs);
644
- entry.lastSubmitOk = true;
645
- } catch (error) {
646
- entry.form.setErrorMap({
647
- onSubmit:
648
- error instanceof Error ? error.message : String(error),
649
- } as never);
650
- }
651
- return;
652
- }
653
- const changes = diff(entry, value as TMDataGridRowData);
654
- const args: TMDataGridEditCommitArgs<TMDataGridRowData> = {
655
- rowId,
656
- value: value as TMDataGridRowData,
657
- original,
658
- changes,
659
- source: getContext().editMode,
660
- };
661
- if (draftCollector !== null) {
662
- draftCollector.push(args);
663
- entry.lastSubmitOk = true;
664
- return;
665
- }
666
- // Parked: validated and held for Save all - no consumer call.
667
- if (entry.parkOnly) {
668
- entry.lastSubmitOk = true;
669
- return;
670
- }
671
- try {
672
- await getContext().onEditCommit?.(args);
673
- entry.lastSubmitOk = true;
674
- } catch (error) {
675
- // A rejected save keeps the form open, the message on the row.
676
- entry.form.setErrorMap({
677
- onSubmit:
678
- error instanceof Error ? error.message : String(error),
679
- } as never);
680
- }
681
- },
682
- }) as TMDataGridRowEditForm,
683
- };
684
- entry.unmount = entry.form.mount();
685
- const subscription = entry.form.store.subscribe(() => publishRow(rowId));
686
- entry.unsubscribe = () => subscription.unsubscribe();
687
- forms.set(rowId, entry);
688
- publishRow(rowId);
689
- return entry;
690
- };
691
-
692
- const canEditRow = (row: ErasedRow): boolean => {
693
- if (row.getIsGrouped()) return false;
694
- return getContext().isRowEditable?.(row) !== false;
695
- };
696
-
697
- const canEditCell = (row: ErasedRow, column: ErasedColumn): boolean => {
698
- const context = getContext();
699
- if (row.getIsGrouped()) return false;
700
- if (isControlColumn(column.id)) return false;
701
- if (getEditFieldName(column) === null) return false;
702
- if (!isColumnEditableForRow(column, row)) return false;
703
- if (context.isRowEditable !== undefined && !context.isRowEditable(row)) {
704
- return false;
705
- }
706
- return true;
707
- };
708
-
709
- const getRow = (rowId: string): ErasedRow | undefined => {
710
- try {
711
- return getContext().table.getRow(rowId);
712
- } catch {
713
- // TanStack throws for an id that is not in the current row model.
714
- return undefined;
715
- }
716
- };
717
-
718
- const commit = async (rowId: string): Promise<boolean> => {
719
- const entry = forms.get(rowId);
720
- if (entry === undefined) return true;
721
- // Enter and blur race on the same edit; the second caller joins the
722
- // first's commit instead of submitting the row twice.
723
- if (entry.pendingCommit !== null) return entry.pendingCommit;
724
- // A pristine form has nothing to say - drop it without a consumer call.
725
- // Not for a new row: adding an untouched entry is still an add.
726
- if (
727
- !entry.isNew &&
728
- diff(entry, entry.form.state.values as TMDataGridRowData).length === 0
729
- ) {
730
- drop(rowId);
731
- return true;
732
- }
733
- // Field errors the *form's* validator planted are cleared before the
734
- // submit re-runs it: a field with no mounted editor is never revalidated
735
- // by Form itself, so a pathed issue would otherwise outlive its fix and
736
- // block `canSubmit` forever. The validator puts back whatever still holds.
737
- clearFormSourcedFieldErrors(entry.form);
738
- entry.lastSubmitOk = false;
739
- entry.parkOnly = getContext().editMode === "draft" && !submitAllRunning;
740
- entry.pendingCommit = (async () => {
741
- try {
742
- await entry.form.handleSubmit();
743
- } finally {
744
- entry.pendingCommit = null;
745
- }
746
- if (!entry.lastSubmitOk) return false;
747
- if (entry.parkOnly) {
748
- // The draft stays in the grid until Save all. An entry row becomes
749
- // "entered": rendered as a value row until `begin` re-opens it.
750
- if (entry.isNew) setNewRowConfirmed(rowId, true);
751
- store.setState((prev) =>
752
- prev.active?.rowId === rowId ? { ...prev, active: null } : prev,
753
- );
754
- return true;
755
- }
756
- drop(rowId);
757
- return true;
758
- })();
759
- return entry.pendingCommit;
760
- };
761
-
762
- const beginOn = (rowId: string, columnId: string | null) => {
763
- // An entry row has no backing row in the table; opening one re-arms its
764
- // editors - under draft, that is how a confirmed row is edited again.
765
- const entryForm = forms.get(rowId);
766
- if (entryForm?.isNew === true) {
767
- setNewRowConfirmed(rowId, false);
768
- setActive({ rowId, columnId });
769
- return;
770
- }
771
- const row = getRow(rowId);
772
- if (row === undefined) return;
773
- if (columnId !== null) {
774
- const column = getContext().table.getColumn(columnId);
775
- if (column === undefined || !canEditCell(row, column)) return;
776
- } else if (getContext().isRowEditable?.(row) === false) {
777
- return;
778
- }
779
- if (!forms.has(rowId)) createForm(rowId, row.original, false);
780
- setActive({ rowId, columnId });
781
- };
782
-
783
- const begin: TMDataGridEditApi["begin"] = ({ rowId, columnId }) => {
784
- const { editMode } = getContext();
785
- // Each mode's policy about a row already open elsewhere:
786
- //
787
- // | Mode | Another row open |
788
- // | ---- | ---------------- |
789
- // | cell | committed - Sheets; a failed commit keeps holding the edit |
790
- // | row, cellConfirm, draft | accumulates. Every draft waits for its own
791
- // save, so a second row opening can neither discard the first nor be
792
- // refused by it - nothing about one row's form bears on another's |
793
- if (editMode === "cell") {
794
- const openElsewhere = [...forms.keys()].find((id) => id !== rowId);
795
- if (openElsewhere !== undefined) {
796
- void commit(openElsewhere).then((ok) => {
797
- if (ok) beginOn(rowId, columnId);
798
- });
799
- return;
800
- }
801
- }
802
- beginOn(rowId, columnId);
803
- };
804
-
805
- const cancel = (rowId: string) => {
806
- drop(rowId);
807
- };
808
-
809
- const deactivate = () => {
810
- setActive(null);
811
- };
812
-
813
- const cancelAll = () => {
814
- for (const rowId of [...forms.keys()]) drop(rowId);
815
- store.setState((prev) => ({ ...prev, active: null, deletedRowIds: [] }));
816
- };
817
-
818
- const addRow = (): string => {
819
- const context = getContext();
820
- newRowCounter += 1;
821
- const tempId = `__new__${newRowCounter}`;
822
- const defaults =
823
- typeof context.newRowDefaults === "function"
824
- ? context.newRowDefaults()
825
- : (context.newRowDefaults ?? {});
826
- createForm(tempId, defaults, true);
827
- store.setState((prev) => ({
828
- ...prev,
829
- newRows: [...prev.newRows, { tempId, confirmed: false }],
830
- }));
831
- return tempId;
832
- };
833
-
834
- const deleteRow = (rowId: string) => {
835
- const entry = forms.get(rowId);
836
- // Deleting an unsaved entry row is just discarding the entry.
837
- if (entry?.isNew === true) {
838
- drop(rowId);
839
- return;
840
- }
841
- const context = getContext();
842
- if (context.editMode === "draft") {
843
- // A toggle: the second press unmarks - the mark is a draft too.
844
- store.setState((prev) => ({
845
- ...prev,
846
- deletedRowIds: prev.deletedRowIds.includes(rowId)
847
- ? prev.deletedRowIds.filter((id) => id !== rowId)
848
- : [...prev.deletedRowIds, rowId],
849
- }));
850
- return;
851
- }
852
- const row = getRow(rowId);
853
- if (row === undefined) return;
854
- // Confirmation is the consumer's business, in their onRowDelete.
855
- void context.onRowDelete?.({ rowId, row });
856
- };
857
-
858
- const canDeleteRows = (): boolean => {
859
- const context = getContext();
860
- if (context.editMode === "draft") {
861
- return (
862
- context.onRowDelete !== undefined ||
863
- context.onEditCommitDrafts !== undefined
864
- );
865
- }
866
- return context.onRowDelete !== undefined;
867
- };
868
-
869
- /** The pending deletions, reported and cleared by submitAll. */
870
- const takeDeletedRowIds = (): Array<string> => {
871
- const deleted = [...store.state.deletedRowIds];
872
- if (deleted.length > 0) {
873
- store.setState((prev) => ({ ...prev, deletedRowIds: [] }));
874
- }
875
- return deleted;
876
- };
877
-
878
- const submitAll = async (): Promise<boolean> => {
879
- // While this runs, `commit` really commits - under draft it parks
880
- // otherwise. Single-threaded flag, same idiom as the collectors.
881
- submitAllRunning = true;
882
- try {
883
- return await submitAllInner();
884
- } finally {
885
- submitAllRunning = false;
886
- }
887
- };
888
-
889
- const submitAllInner = async (): Promise<boolean> => {
890
- if (getContext().onEditCommitDrafts === undefined) {
891
- // The default: the per-row loop - edits through `onEditCommit`, entry
892
- // rows through `onRowAdd`, marked deletions through `onRowDelete`.
893
- const results = await Promise.all(
894
- [...forms.keys()].map((rowId) => commit(rowId)),
895
- );
896
- for (const rowId of takeDeletedRowIds()) {
897
- const row = getRow(rowId);
898
- if (row !== undefined) {
899
- await getContext().onRowDelete?.({ rowId, row });
900
- }
901
- }
902
- return results.every(Boolean);
903
- }
904
-
905
- // One consumer call for the lot. Validation stays Form's, per row: rows
906
- // that fail keep their forms and markers; the valid ones travel
907
- // together, and only a resolved save drops them - a rejected save keeps
908
- // every draft, deletions included.
909
- const collected: Array<TMDataGridEditCommitArgs<TMDataGridRowData>> = [];
910
- const added: Array<TMDataGridRowAddArgs<TMDataGridRowData>> = [];
911
- draftCollector = collected;
912
- draftAddCollector = added;
913
- let allValid = true;
914
- try {
915
- for (const rowId of [...forms.keys()]) {
916
- const entry = forms.get(rowId);
917
- if (entry === undefined) continue;
918
- if (
919
- !entry.isNew &&
920
- diff(entry, entry.form.state.values as TMDataGridRowData).length === 0
921
- ) {
922
- drop(rowId);
923
- continue;
924
- }
925
- entry.lastSubmitOk = false;
926
- await entry.form.handleSubmit();
927
- if (!entry.lastSubmitOk) allValid = false;
928
- }
929
- } finally {
930
- draftCollector = null;
931
- draftAddCollector = null;
932
- }
933
- const deleted = [...store.state.deletedRowIds];
934
- if (collected.length > 0 || added.length > 0 || deleted.length > 0) {
935
- try {
936
- await getContext().onEditCommitDrafts?.({ rows: collected, added, deleted });
937
- } catch {
938
- return false;
939
- }
940
- for (const args of collected) drop(args.rowId);
941
- for (const args of added) drop(args.tempId);
942
- if (deleted.length > 0) {
943
- store.setState((prev) => ({
944
- ...prev,
945
- deletedRowIds: prev.deletedRowIds.filter(
946
- (id) => !deleted.includes(id),
947
- ),
948
- }));
949
- }
950
- }
951
- return allValid;
952
- };
953
-
954
- const clearCell = async (
955
- rowId: string,
956
- columnId: string,
957
- ): Promise<boolean> => {
958
- const context = getContext();
959
- const row = getRow(rowId);
960
- const column = context.table.getColumn(columnId);
961
- if (row === undefined || column === undefined) return false;
962
- if (!canEditCell(row, column)) return false;
963
- const field = getEditFieldName(column);
964
- if (field === null) return false;
965
- const entry = forms.get(rowId) ?? createForm(rowId, row.original, false);
966
- entry.form.setFieldValue(
967
- field as never,
968
- clearedValueForType(column.columnDef.meta?.type ?? "string") as never,
969
- );
970
- return commit(rowId);
971
- };
972
-
973
- return {
974
- store,
975
- get state() {
976
- return store.state;
977
- },
978
- getForm: (rowId) => forms.get(rowId)?.form,
979
- canEditCell,
980
- canEditRow,
981
- begin,
982
- commit,
983
- cancel,
984
- deactivate,
985
- cancelAll,
986
- submitAll,
987
- clearCell,
988
- addRow,
989
- deleteRow,
990
- canDeleteRows,
991
- };
992
- }
993
-
994
- /**
995
- * `meta.edit.validate` normalised to the object TanStack Form's `validators`
996
- * option takes - a bare schema or function is shorthand for `{ onChange }`.
997
- */
998
- export function normalizeFieldValidate(
999
- validate: TMDataGridFieldValidate | undefined,
1000
- ): Record<string, unknown> | undefined {
1001
- if (validate === undefined) return undefined;
1002
- if (typeof validate === "function" || "~standard" in validate) {
1003
- return { onChange: validate };
1004
- }
1005
- return validate as Record<string, unknown>;
1006
- }