@jielga/tmdatagrid 2.0.0-beta.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1281 -768
  3. package/dist/index.js +4607 -3250
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/adding-rows.md +132 -0
  7. package/docs/anatomy.md +119 -0
  8. package/docs/card-view.md +108 -0
  9. package/docs/cell-selection.md +194 -0
  10. package/docs/column-layout.md +182 -0
  11. package/docs/column-menu.md +66 -0
  12. package/docs/columns.md +268 -0
  13. package/docs/components.md +311 -0
  14. package/docs/draft-store.md +242 -0
  15. package/docs/editing.md +303 -0
  16. package/docs/editors.md +250 -0
  17. package/docs/export.md +319 -0
  18. package/docs/filtering.md +362 -0
  19. package/docs/getting-started.md +123 -0
  20. package/docs/grouping.md +165 -0
  21. package/docs/loading-and-empty.md +92 -0
  22. package/docs/localization.md +79 -0
  23. package/docs/menu.md +143 -0
  24. package/docs/migrating-to-2.md +163 -0
  25. package/docs/pagination.md +144 -0
  26. package/docs/persistence.md +114 -0
  27. package/docs/portfolio-rebalancer.md +94 -0
  28. package/docs/query-builder.md +179 -0
  29. package/docs/quick-search.md +84 -0
  30. package/docs/row-details.md +115 -0
  31. package/docs/row-interaction.md +149 -0
  32. package/docs/row-pinning.md +132 -0
  33. package/docs/row-selection.md +136 -0
  34. package/docs/row-styling.md +133 -0
  35. package/docs/scrolling.md +112 -0
  36. package/docs/server-query.md +246 -0
  37. package/docs/server-side.md +206 -0
  38. package/docs/sorting.md +101 -0
  39. package/docs/styling.md +126 -0
  40. package/docs/summary-row.md +76 -0
  41. package/docs/testing.md +744 -0
  42. package/docs/toolbar.md +161 -0
  43. package/docs/use-tm-data-grid.md +361 -0
  44. package/package.json +22 -46
  45. package/skills/appearance/SKILL.md +72 -19
  46. package/skills/cell-selection/SKILL.md +46 -47
  47. package/skills/columns/SKILL.md +90 -34
  48. package/skills/data/SKILL.md +86 -16
  49. package/skills/editing/SKILL.md +67 -40
  50. package/skills/editing/references/common-mistakes.md +77 -69
  51. package/skills/editing/references/editing-api.md +22 -19
  52. package/skills/editing/references/editors-and-validation.md +24 -17
  53. package/skills/filtering/SKILL.md +148 -40
  54. package/skills/getting-started/SKILL.md +17 -15
  55. package/skills/grouping/SKILL.md +31 -16
  56. package/skills/options/SKILL.md +7 -7
  57. package/skills/rows/SKILL.md +22 -18
  58. package/skills/server-side/SKILL.md +170 -17
  59. package/skills/testing/SKILL.md +150 -32
  60. package/skills/testing-components/SKILL.md +230 -0
  61. package/skills/testing-editing/SKILL.md +240 -0
  62. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +38 -21
  63. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +54 -8
  64. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  65. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  66. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +5 -51
  67. package/src/{tmdatagrid/components → components}/TMDataGridDraftActions.tsx +41 -24
  68. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +12 -59
  69. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +164 -92
  70. package/src/components/TMDataGridExportPicker.module.css +77 -0
  71. package/src/components/TMDataGridExportPicker.tsx +234 -0
  72. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  73. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  74. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +15 -8
  75. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  76. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  77. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -90
  78. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +5 -69
  79. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +12 -2
  80. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +58 -21
  81. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  82. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  83. package/src/components/TMDataGridMenu.tsx +357 -0
  84. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +11 -48
  85. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +69 -56
  86. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +240 -138
  87. package/src/{tmdatagrid/components → components}/TMDataGridToolbar.module.css +5 -0
  88. package/src/components/TMDataGridToolbar.tsx +181 -0
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  91. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  92. package/src/{tmdatagrid/components → components}/editors/TMDataGridNumberEditor.tsx +3 -3
  93. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  94. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  95. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +16 -2
  96. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  97. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  98. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  99. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  100. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  101. package/src/components/filters/controlLayout.ts +32 -0
  102. package/src/components/filters/filterControlFor.ts +65 -0
  103. package/src/components/generatedColumns.tsx +187 -0
  104. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  105. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  106. package/src/components/useHideableColumns.ts +52 -0
  107. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  108. package/src/{tmdatagrid/core → core}/columnOptions.ts +60 -8
  109. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  110. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  111. package/src/core/controlledStateSync.ts +108 -0
  112. package/src/core/deletedRows.ts +34 -0
  113. package/src/core/dom.ts +74 -0
  114. package/src/{tmdatagrid/core → core}/editEngine.ts +1107 -460
  115. package/src/core/export.ts +704 -0
  116. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  117. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  118. package/src/core/filterSurface.ts +99 -0
  119. package/src/{tmdatagrid/core → core}/grouping.ts +21 -0
  120. package/src/{tmdatagrid/core → core}/labels.ts +51 -6
  121. package/src/{tmdatagrid/core → core}/labelsSv.ts +27 -6
  122. package/src/core/pageReset.ts +120 -0
  123. package/src/core/pagination.ts +81 -0
  124. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  125. package/src/{tmdatagrid/core → core}/summary.ts +20 -4
  126. package/src/{tmdatagrid/index.ts → index.ts} +69 -35
  127. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +428 -109
  128. package/src/useTMDataGridExport.ts +78 -0
  129. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  130. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  131. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -161
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. /package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +0 -0
  134. /package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +0 -0
  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}/capabilities.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/controlledState.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/editorFocus.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/resizePreview.ts +0 -0
  151. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  152. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  153. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
  154. /package/src/{tmdatagrid/core → core}/useSettledTableState.ts +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  useBodyControlTabIndex,
4
4
  useTMDataGridContext,
5
5
  } from "../TMDataGridContext";
6
- import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
6
+ import type { Row, RowData } from "@tanstack/react-table";
7
7
  import { useSelector } from "@tanstack/react-store";
8
8
  import classes from "./TMDataGridDetailsColumn.module.css";
9
9
  import { ChevronRightIcon } from "./icons";
@@ -20,7 +20,7 @@ export const DETAILS_COLUMN_ID = "__details__";
20
20
  * expand, so the React Compiler would cache the call along with it and the
21
21
  * chevron would never turn. Same reason the select checkbox subscribes.
22
22
  */
23
- function DetailsCell<TData extends RowData>({
23
+ export function DetailsCell<TData extends RowData>({
24
24
  row,
25
25
  }: {
26
26
  row: Row<TMDataGridFeatures, TData>;
@@ -51,7 +51,7 @@ function DetailsCell<TData extends RowData>({
51
51
  row.toggleExpanded();
52
52
  }}
53
53
  >
54
- <span className={classes.chevron} data-expanded={expanded}>
54
+ <span className={classes.chevron} data-expanded={expanded || undefined}>
55
55
  <ChevronRightIcon size={16} stroke={1.6} />
56
56
  </span>
57
57
  </UnstyledButton>
@@ -67,7 +67,7 @@ function DetailsCell<TData extends RowData>({
67
67
  * so it would unfold every group as well. Only the data rows are touched here;
68
68
  * whatever the tree was showing, it goes on showing. See resolveExpandAll.
69
69
  */
70
- function DetailsHeader<TData extends RowData>({
70
+ export function DetailsHeader<TData extends RowData>({
71
71
  table,
72
72
  }: {
73
73
  table: TMDataGridTable<TData>;
@@ -106,55 +106,9 @@ function DetailsHeader<TData extends RowData>({
106
106
  )
107
107
  }
108
108
  >
109
- <span className={classes.chevron} data-expanded={allExpanded}>
109
+ <span className={classes.chevron} data-expanded={allExpanded || undefined}>
110
110
  <ChevronRightIcon size={16} stroke={1.6} />
111
111
  </span>
112
112
  </UnstyledButton>
113
113
  );
114
114
  }
115
-
116
- /**
117
- * The generated details lane, prepended whenever `renderDetails` is set.
118
- *
119
- * Structural, like the checkbox and tree columns: fixed width, pinned to the
120
- * left after both of them, not hideable, not movable and not resizable. Moving
121
- * or hiding the toggle would leave rows with panels that cannot be opened.
122
- *
123
- * Last of the three because it acts on a single record.
124
- *
125
- * A second toggle elsewhere is supported: `row.toggleExpanded()` is the entire
126
- * interface, and this lane is only the control the grid ships.
127
- */
128
- export function createDetailsColumn<TData extends RowData>(
129
- label = "Details",
130
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
131
- return {
132
- id: DETAILS_COLUMN_ID,
133
- meta: {
134
- label,
135
- align: "center",
136
- // Structurally the last of the generated lanes.
137
- enableOrdering: false,
138
- },
139
- // A system lane: as wide as the control it holds and no wider. Fixed at
140
- // every scale - the control does not grow with the font size, so neither
141
- // should its track.
142
- size: 36,
143
- minSize: 36,
144
- maxSize: 36,
145
- enableResizing: false,
146
- enableSorting: false,
147
- enableColumnFilter: false,
148
- enableGlobalFilter: false,
149
- // Its visibility is not the user's to set: hiding it would strand every
150
- // panel behind a control that is no longer there.
151
- enableHiding: false,
152
- // Structurally pinned to the left; users shouldn't be able to move it.
153
- enablePinning: false,
154
- header: ({ table }) => <DetailsHeader table={table} />,
155
- cell: ({ row }) => <DetailsCell row={row} />,
156
- // Deliberately no `aggregatedCell`: on a group row every cell outside the
157
- // grouped column counts as aggregated, and blank is the right answer here -
158
- // groups expand into their rows, not into a panel.
159
- };
160
- }
@@ -3,7 +3,11 @@ import { useSelector } from "@tanstack/react-store";
3
3
  import { shallow } from "@tanstack/store";
4
4
  import type { ReactNode } from "react";
5
5
  import { useTMDataGridContext } from "../TMDataGridContext";
6
- import { getOpenRowIds } from "../core/editEngine";
6
+ import {
7
+ getOpenRowIds,
8
+ type TMDataGridCommitAllResult,
9
+ type TMDataGridSaveDraftsResult,
10
+ } from "../core/editEngine";
7
11
  import { getDisplayedRows } from "../core/rowSelection";
8
12
  import type {
9
13
  TMDataGridScrollAlign,
@@ -17,13 +21,20 @@ import type {
17
21
  */
18
22
  function useDraftCount(): number {
19
23
  const { edit } = useTMDataGridContext();
20
- return useSelector(
21
- edit.store,
22
- (state) =>
23
- state.committedRowIds.length +
24
+ return useSelector(edit.store, (state) => {
25
+ const deleted = new Set(state.deletedRowIds);
26
+ // An edited row that was then marked counts once: Save sends the
27
+ // deletion and leaves the edit behind.
28
+ const edited =
29
+ deleted.size === 0
30
+ ? state.committedRowIds.length
31
+ : state.committedRowIds.filter((rowId) => !deleted.has(rowId)).length;
32
+ return (
33
+ edited +
24
34
  state.newRows.filter((newRow) => newRow.committed).length +
25
- state.deletedRowIds.length,
26
- );
35
+ deleted.size
36
+ );
37
+ });
27
38
  }
28
39
 
29
40
  /**
@@ -55,15 +66,15 @@ function useOpenRowIds(): ReadonlyArray<string> {
55
66
  function EditSaveButton() {
56
67
  const { edit, labels } = useTMDataGridContext();
57
68
  const draftCount = useDraftCount();
58
- const isSubmitting = useSelector(edit.store, (state) =>
59
- state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
60
- );
69
+ // Save sends the draft store, so its loading state is the save in flight,
70
+ // not an open row's submit.
71
+ const isSaving = useSelector(edit.store, (state) => state.isSaving);
61
72
 
62
73
  return (
63
74
  <Button
64
75
  size="compact-sm"
65
76
  disabled={draftCount === 0}
66
- loading={isSubmitting}
77
+ loading={isSaving}
67
78
  data-dg-part="save-all"
68
79
  data-draft-count={draftCount}
69
80
  onClick={() => void edit.saveDrafts()}
@@ -94,14 +105,14 @@ function EditDiscardButton() {
94
105
  const { edit, labels } = useTMDataGridContext();
95
106
  // Discard drops everything the grid is holding, open rows included, so it
96
107
  // stays live while anything is uncommitted.
97
- const pendingCount = useDraftCount() + useOpenCount();
108
+ const uncommittedCount = useDraftCount() + useOpenCount();
98
109
 
99
110
  return (
100
111
  <Button
101
112
  variant="subtle"
102
113
  color="gray"
103
114
  size="compact-sm"
104
- disabled={pendingCount === 0}
115
+ disabled={uncommittedCount === 0}
105
116
  data-dg-part="discard-all"
106
117
  onClick={() => edit.cancelAll()}
107
118
  >
@@ -139,21 +150,26 @@ export type TMDataGridDraftActionsState = {
139
150
  * in display order, so it need not be `openRowIds[0]`.
140
151
  */
141
152
  openRowIds: ReadonlyArray<string>;
142
- /**
143
- * @deprecated Was "everything uncommitted", which Save no longer sends.
144
- * Reads as `draftCount + openCount`; use whichever you meant.
145
- */
146
- pendingCount: number;
147
- /** Whether a submit is in flight. */
153
+ /** Whether a submit is in flight - any open row is submitting. */
148
154
  isSubmitting: boolean;
155
+ /** `true` while `saveDrafts` is in flight. */
156
+ isSaving: boolean;
149
157
  };
150
158
 
151
159
  /** What the edit chrome can do. */
152
160
  export type TMDataGridDraftActionsActions = {
153
- /** Saves the draft store. Open rows are left alone. */
154
- save: () => Promise<boolean>;
155
- /** Submits every open row, committing the ones that validate. */
156
- commitAll: () => Promise<boolean>;
161
+ /**
162
+ * Saves the draft store - `edit.saveDrafts()`. Open rows are left alone.
163
+ * Resolves which ids were saved, kept in the draft store for the next
164
+ * save, or reopened with an error; see {@link TMDataGridSaveDraftsResult}.
165
+ */
166
+ save: () => Promise<TMDataGridSaveDraftsResult>;
167
+ /**
168
+ * Submits every open row, committing the ones that validate -
169
+ * `edit.commitAll()`. Resolves which rows committed and which are still
170
+ * open; see {@link TMDataGridCommitAllResult}.
171
+ */
172
+ commitAll: () => Promise<TMDataGridCommitAllResult>;
157
173
  /** Drops everything - open form state and the draft store alike. */
158
174
  discard: () => void;
159
175
  /**
@@ -242,6 +258,7 @@ export function TMDataGridDraftActions({
242
258
  const isSubmitting = useSelector(edit.store, (state) =>
243
259
  state.openRowIds.some((rowId) => state.rows[rowId]?.isSubmitting === true),
244
260
  );
261
+ const isSaving = useSelector(edit.store, (state) => state.isSaving);
245
262
 
246
263
  if (!features.editing) return null;
247
264
 
@@ -251,8 +268,8 @@ export function TMDataGridDraftActions({
251
268
  draftCount,
252
269
  openCount: openRowIds.length,
253
270
  openRowIds,
254
- pendingCount: draftCount + openRowIds.length,
255
271
  isSubmitting,
272
+ isSaving,
256
273
  },
257
274
  actions: {
258
275
  save: () => edit.saveDrafts(),
@@ -1,6 +1,6 @@
1
1
  import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
2
2
  import { useSelector } from "@tanstack/react-store";
3
- import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
3
+ import type { Row, RowData } from "@tanstack/react-table";
4
4
  import classes from "./TMDataGridTable.module.css";
5
5
  import {
6
6
  useBodyControlTabIndex,
@@ -65,16 +65,16 @@ function RowStateIndicator({
65
65
  * | open | the mode's own controls: Save and Cancel | `editing.mode` |
66
66
  * | neither | the pencil (row mode) and the trash | `editing.mode` |
67
67
  *
68
- * A row error turns the Save red with the message in its tooltip - a
69
- * pathless `.refine()` has nowhere else to land, and a field error outlives
70
- * the editor that found it. A parked row never offers a save: it has had its
71
- * submit, and `TMDataGrid.DraftActions` is what sends it.
68
+ * A row error turns the Save - or an entry row's ✓ - red with the message in
69
+ * its tooltip: a pathless `.refine()` has nowhere else to land, and a field
70
+ * error outlives the editor that found it. A parked row never offers a save:
71
+ * it has had its submit, and `TMDataGrid.DraftActions` is what sends it.
72
72
  *
73
73
  * The lane's controls take the tab index every body control does - see
74
74
  * useBodyControlTabIndex. The open row's save and cancel are the exception,
75
75
  * see where they are built.
76
76
  */
77
- function EditLaneCell<TData extends RowData>({
77
+ export function EditLaneCell<TData extends RowData>({
78
78
  row,
79
79
  }: {
80
80
  row: Row<TMDataGridFeatures, TData>;
@@ -186,10 +186,10 @@ function EditLaneCell<TData extends RowData>({
186
186
  }
187
187
  return (
188
188
  <Group gap={0} wrap="nowrap">
189
- <Tooltip label={labels.confirmNewRow} withArrow>
189
+ <Tooltip label={rowError ?? labels.confirmNewRow} withArrow>
190
190
  <ActionIcon
191
191
  variant="subtle"
192
- color="green"
192
+ color={hasErrors ? "red" : "green"}
193
193
  size="sm"
194
194
  tabIndex={tabIndex}
195
195
  aria-label={labels.confirmNewRow}
@@ -239,7 +239,7 @@ function EditLaneCell<TData extends RowData>({
239
239
  data-row-id={rowId}
240
240
  onClick={(event) => {
241
241
  event.stopPropagation();
242
- edit.deleteRow(rowId);
242
+ edit.restoreRow(rowId);
243
243
  }}
244
244
  >
245
245
  <RestoreIcon size={16} stroke={1.6} />
@@ -339,15 +339,9 @@ function EditLaneCell<TData extends RowData>({
339
339
 
340
340
  return (
341
341
  <Group gap={0} wrap="nowrap">
342
- {rowError === null ? (
343
- <Tooltip label={labels.saveRow} withArrow>
344
- {save}
345
- </Tooltip>
346
- ) : (
347
- <Tooltip label={rowError} withArrow>
348
- {save}
349
- </Tooltip>
350
- )}
342
+ <Tooltip label={rowError ?? labels.saveRow} withArrow>
343
+ {save}
344
+ </Tooltip>
351
345
  <Tooltip label={labels.cancelRowEdit} withArrow>
352
346
  <ActionIcon
353
347
  variant="subtle"
@@ -368,44 +362,3 @@ function EditLaneCell<TData extends RowData>({
368
362
  </Group>
369
363
  );
370
364
  }
371
-
372
- /**
373
- * The generated edit lane, appended and pinned right - the row's Save at the
374
- * end of the row under `mode: "row"`, the state marker and revert under
375
- * `editing.draft`, mirroring the checkbox lane's build on the left.
376
- */
377
- export function createEditColumn<TData extends RowData>(
378
- label = "Edit",
379
- /** A draft lane holds three controls where the rest hold two. */
380
- wide = false,
381
- ): ColumnDef<TMDataGridFeatures, TData, unknown> {
382
- const width = wide ? 88 : 64;
383
- return {
384
- id: EDIT_COLUMN_ID,
385
- meta: {
386
- label,
387
- align: "center",
388
- enableOrdering: false,
389
- },
390
- // Wide enough for the pair (or draft's trio) it holds while editing.
391
- size: width,
392
- minSize: width,
393
- maxSize: width,
394
- enableResizing: false,
395
- enableSorting: false,
396
- enableColumnFilter: false,
397
- enableGlobalFilter: false,
398
- // The row's Save, Cancel and Delete live here, so hiding the lane would
399
- // strand an open row with no way to commit or discard it. Same rule as the
400
- // checkbox lane: chrome the grid generates is not a user setting.
401
- enableHiding: false,
402
- // Structurally pinned to the right; not movable.
403
- enablePinning: false,
404
- header: () => null,
405
- cell: ({ row }) => <EditLaneCell row={row} />,
406
- // Group rows: same reasoning as the checkbox lane - without this the
407
- // cell renders blank on aggregated rows, but here blank is also correct,
408
- // so the aggregated cell renders the same (null for groups).
409
- aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
410
- };
411
- }
@@ -1,10 +1,12 @@
1
1
  import {
2
2
  flexRender,
3
3
  useTable,
4
+ type Cell,
4
5
  type Column,
5
6
  type Row,
6
7
  } from "@tanstack/react-table";
7
8
  import { useSelector } from "@tanstack/react-store";
9
+ import { shallow } from "@tanstack/store";
8
10
  import { useLayoutEffect, useMemo, useRef } from "react";
9
11
  import classes from "./TMDataGridTable.module.css";
10
12
  import sticky from "./sticky.module.css";
@@ -14,7 +16,8 @@ import {
14
16
  isColumnEditableForRow,
15
17
  isControlColumn,
16
18
  } from "../core/columnUtils";
17
- import type { TMDataGridEditApi } from "../core/editEngine";
19
+ import { draftCellContext } from "../core/draftCellContext";
20
+ import { getEditFieldName, type TMDataGridEditApi } from "../core/editEngine";
18
21
  import { focusEditorContent } from "../core/editorFocus";
19
22
  import {
20
23
  tmDataGridFeatures,
@@ -25,6 +28,8 @@ import { EDIT_COLUMN_ID } from "./TMDataGridEditColumn";
25
28
  import type { TMDataGridColumnLayout } from "./TMDataGridTable";
26
29
 
27
30
  type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
31
+ type EntryRow = Row<TMDataGridFeatures, TMDataGridRowData>;
32
+ type EntryCellApi = Cell<TMDataGridFeatures, TMDataGridRowData, unknown>;
28
33
 
29
34
  /**
30
35
  * Whether an entry cell edits. `edit.canEditCell` asks the main table for
@@ -40,6 +45,115 @@ function isEntryCellEditable(
40
45
  return isColumnEditableForRow(column, row);
41
46
  }
42
47
 
48
+ /**
49
+ * One cell of an entry row: the lane's controls, an editor for a cell the
50
+ * open row edits, or a value through the column's own renderer. The invalid
51
+ * marker subscribes here, per cell, as a body cell's does - a refused ✓
52
+ * paints the cells its issues name and nothing else. No dirty marker:
53
+ * everything in an entry row is new.
54
+ */
55
+ function EntryCell({
56
+ entryRow,
57
+ column,
58
+ cell,
59
+ committed,
60
+ layout,
61
+ rowHeight,
62
+ pinnedZ,
63
+ }: {
64
+ entryRow: EntryRow;
65
+ column: ErasedColumn;
66
+ cell: EntryCellApi | undefined;
67
+ committed: boolean;
68
+ layout: TMDataGridColumnLayout;
69
+ rowHeight: number;
70
+ pinnedZ: string;
71
+ }) {
72
+ const { edit } = useTMDataGridContext();
73
+ const fieldName = getEditFieldName(column);
74
+ const isInvalid = useSelector(edit.store, (state) => {
75
+ if (fieldName === null) return false;
76
+ const projection = state.rows[entryRow.id];
77
+ return projection !== undefined && projection.errorFields.includes(fieldName);
78
+ });
79
+ const editable =
80
+ cell !== undefined && isEntryCellEditable(entryRow, column, edit);
81
+ // An open row's cells outside the editors follow the form as it is typed,
82
+ // as a body row's do: the column's renderer over a context reading the
83
+ // form's values - the whole row, so a computed cell repaints with any
84
+ // field. `undefined` for a committed row, whose values the entry table
85
+ // already holds as data, and for the lanes.
86
+ const draftValues = useSelector(edit.store, (state) =>
87
+ committed || isControlColumn(column.id)
88
+ ? undefined
89
+ : state.rows[entryRow.id]?.values,
90
+ );
91
+ return (
92
+ <div
93
+ role="cell"
94
+ data-column-id={column.id}
95
+ data-align={getColumnAlign(column)}
96
+ data-control-column={isControlColumn(column.id) || undefined}
97
+ data-invalid={isInvalid || undefined}
98
+ // A committed row re-opens where it is double-clicked, the same
99
+ // gesture a body cell answers - and, like a body cell, a cell that
100
+ // takes no edit answers nothing.
101
+ onDoubleClick={
102
+ committed && editable
103
+ ? () => edit.begin({ rowId: entryRow.id, columnId: column.id })
104
+ : undefined
105
+ }
106
+ className={[
107
+ classes.entryCell,
108
+ layout.isBoundary && layout.pinnedAt === "left" ? sticky.stickyLeft : "",
109
+ layout.isBoundary && layout.pinnedAt === "right"
110
+ ? sticky.stickyRight
111
+ : "",
112
+ ]
113
+ .filter(Boolean)
114
+ .join(" ")}
115
+ style={{
116
+ minHeight: rowHeight,
117
+ left: layout.pinnedAt === "left" ? layout.offset : undefined,
118
+ right: layout.pinnedAt === "right" ? layout.offset : undefined,
119
+ position: layout.pinnedAt ? "sticky" : undefined,
120
+ zIndex: layout.pinnedAt ? pinnedZ : undefined,
121
+ }}
122
+ >
123
+ {cell === undefined ? null : column.id === EDIT_COLUMN_ID ? (
124
+ // The lane's cell - the entry row's controls.
125
+ flexRender(cell.column.columnDef.cell, cell.getContext())
126
+ ) : !committed && editable ? (
127
+ <TMDataGridCellEditor
128
+ cell={cell}
129
+ row={entryRow}
130
+ takeSeedText={() => undefined}
131
+ onClose={() => {}}
132
+ inEntryBlock
133
+ />
134
+ ) : committed || !isControlColumn(column.id) ? (
135
+ // A value through the column's own renderer. Every cell of a row
136
+ // entered and awaiting Save all, over the draft the entry table was
137
+ // fed - and, while the row is open, every cell it does not open (a
138
+ // display column, `meta.edit.enabled` off), over the form as it is
139
+ // typed, as a body row in row mode shows them. Blank was how a
140
+ // calculated column or a button cell vanished the moment a
141
+ // committed row was reopened. The generated lanes stay blank while
142
+ // the row is open: a checkbox or a chevron there would act on the
143
+ // entry table's own state, which reaches nothing.
144
+ <span className={classes.cellContent}>
145
+ {flexRender(
146
+ cell.column.columnDef.cell,
147
+ draftValues !== undefined
148
+ ? draftCellContext(cell, draftValues)
149
+ : cell.getContext(),
150
+ )}
151
+ </span>
152
+ ) : null}
153
+ </div>
154
+ );
155
+ }
156
+
43
157
  /**
44
158
  * The sticky entry block - one row of open editors per `edit.addRow()`,
45
159
  * pinned under the header. The one place stickiness is genuinely required:
@@ -66,7 +180,28 @@ export function TMDataGridEntryRows({
66
180
  rowHeight: number;
67
181
  }) {
68
182
  const { table, edit, features } = useTMDataGridContext();
69
- const newRows = useSelector(edit.store, (state) => state.newRows);
183
+ const sticky = features.editNewRowsSticky;
184
+ // A row being *typed* into is always here - it exists nowhere else to
185
+ // scroll back to. A committed row is a body row, sorted and filtered with
186
+ // the rest (the hook feeds it to the table as `data`), unless
187
+ // `newRowsSticky` keeps it pinned here until the save. Only the rows the
188
+ // block renders go into its table: after an import `newRows` runs to
189
+ // thousands, every one of them committed and in the body.
190
+ const newRows = useSelector(
191
+ edit.store,
192
+ (state) =>
193
+ sticky
194
+ ? state.newRows
195
+ : state.newRows.filter((newRow) => !newRow.committed),
196
+ { compare: shallow },
197
+ );
198
+ // What a committed row renders from. It has no form - its values are the
199
+ // draft store's - and this is the same snapshot the table feeds a
200
+ // committed row in the body.
201
+ const committedValues = useSelector(
202
+ edit.store,
203
+ (state) => state.committedValues,
204
+ );
70
205
  // The reopen gesture: `begin` on a committed entry row flips it back to
71
206
  // editors and names the cell double-clicked - where the caret goes.
72
207
  const activeEntry = useSelector(edit.store, (state) =>
@@ -134,21 +269,22 @@ export function TMDataGridEntryRows({
134
269
  focusEditorContent(editor);
135
270
  });
136
271
 
137
- // Open (uncommitted) rows keep the seed values frozen at addRow - the live values
138
- // belong to the forms, which the editors read directly, and this table only
139
- // provides row and cell identity. A committed row's cells render values, so
140
- // there the draft itself is the row; the memo recomputes on every
141
- // commit/reopen because either flips `newRows`' identity.
272
+ // What the rows render over. A committed row is data: its values are the
273
+ // draft store's, which stand through a reopen. An open row's cells read
274
+ // the form itself - the editors directly, the rest through EntryCell's
275
+ // draft overlay - so its entry here gives the row an identity and a
276
+ // fallback: the committed values it was reopened from, else the seed
277
+ // frozen at addRow (the form's `defaultValues`, which a reopen keeps at
278
+ // the seed and writes the committed values over).
142
279
  const data = useMemo(
143
280
  () =>
144
281
  newRows.map(
145
- ({ tempId, committed }) =>
146
- ((committed
147
- ? edit.getForm(tempId)?.state.values
148
- : edit.getForm(tempId)?.options.defaultValues) ??
282
+ ({ tempId }) =>
283
+ (committedValues[tempId] ??
284
+ edit.getForm(tempId)?.options.defaultValues ??
149
285
  {}) as TMDataGridRowData,
150
286
  ),
151
- [newRows, edit],
287
+ [newRows, committedValues, edit],
152
288
  );
153
289
 
154
290
  const entryTable = useTable({
@@ -160,23 +296,12 @@ export function TMDataGridEntryRows({
160
296
 
161
297
  if (newRows.length === 0) return null;
162
298
 
163
- const entryRows = entryTable.getCoreRowModel().rows;
299
+ const stickyRows = entryTable.getCoreRowModel().rows;
164
300
  const committedById = new Map(
165
301
  newRows.map((newRow) => [newRow.tempId, newRow.committed]),
166
302
  );
167
- // A row being *typed* into is always here - it exists nowhere else to
168
- // scroll back to. A committed row is a body row, sorted and filtered with
169
- // the rest (the hook feeds it to the table as `data`), unless
170
- // `newRowsSticky` keeps it pinned here until the save.
171
- const stickyRows = entryRows.filter(
172
- (entryRow) =>
173
- features.editNewRowsSticky || committedById.get(entryRow.id) !== true,
174
- );
175
303
 
176
- const renderEntryRow = (
177
- entryRow: (typeof entryRows)[number],
178
- pinnedZ: string,
179
- ) => {
304
+ const renderEntryRow = (entryRow: EntryRow, pinnedZ: string) => {
180
305
  const cellsById = new Map(
181
306
  entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
182
307
  );
@@ -188,77 +313,24 @@ export function TMDataGridEntryRows({
188
313
  data-dg-part="entry-row"
189
314
  data-row-id={entryRow.id}
190
315
  data-new
191
- data-committed={committed}
316
+ data-committed={committed || undefined}
192
317
  // The same marker body rows carry once committed, so one selector
193
318
  // reaches everything parked in the draft store.
194
- data-draft={committed}
319
+ data-draft={committed || undefined}
195
320
  className={classes.entryRow}
196
321
  >
197
- {orderedColumns.map((column) => {
198
- const cell = cellsById.get(column.id);
199
- const layout = layoutFor(column.id);
200
- const editable =
201
- cell !== undefined && isEntryCellEditable(entryRow, column, edit);
202
- return (
203
- <div
204
- key={column.id}
205
- role="cell"
206
- data-column-id={column.id}
207
- data-align={getColumnAlign(column)}
208
- data-control-column={isControlColumn(column.id)}
209
- // A committed row re-opens where it is double-clicked, the
210
- // same gesture a body cell answers - and, like a body cell, a
211
- // cell that takes no edit answers nothing.
212
- onDoubleClick={
213
- committed && editable
214
- ? () =>
215
- edit.begin({
216
- rowId: entryRow.id,
217
- columnId: column.id,
218
- })
219
- : undefined
220
- }
221
- className={[
222
- classes.entryCell,
223
- layout.isBoundary && layout.pinnedAt === "left"
224
- ? sticky.stickyLeft
225
- : "",
226
- layout.isBoundary && layout.pinnedAt === "right"
227
- ? sticky.stickyRight
228
- : "",
229
- ]
230
- .filter(Boolean)
231
- .join(" ")}
232
- style={{
233
- minHeight: rowHeight,
234
- left: layout.pinnedAt === "left" ? layout.offset : undefined,
235
- right: layout.pinnedAt === "right" ? layout.offset : undefined,
236
- position: layout.pinnedAt ? "sticky" : undefined,
237
- zIndex: layout.pinnedAt ? pinnedZ : undefined,
238
- }}
239
- >
240
- {cell !== undefined && column.id === EDIT_COLUMN_ID ? (
241
- // The lane's cell - the entry row's controls.
242
- flexRender(cell.column.columnDef.cell, cell.getContext())
243
- ) : committed && cell !== undefined ? (
244
- // Entered, awaiting Save all: a value row through the
245
- // columns' own renderers, over the draft the memo above
246
- // fed this table.
247
- <span className={classes.cellContent}>
248
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
249
- </span>
250
- ) : editable && cell !== undefined ? (
251
- <TMDataGridCellEditor
252
- cell={cell}
253
- row={entryRow}
254
- takeSeedText={() => undefined}
255
- onClose={() => {}}
256
- inEntryBlock
257
- />
258
- ) : null}
259
- </div>
260
- );
261
- })}
322
+ {orderedColumns.map((column) => (
323
+ <EntryCell
324
+ key={column.id}
325
+ entryRow={entryRow}
326
+ column={column}
327
+ cell={cellsById.get(column.id)}
328
+ committed={committed}
329
+ layout={layoutFor(column.id)}
330
+ rowHeight={rowHeight}
331
+ pinnedZ={pinnedZ}
332
+ />
333
+ ))}
262
334
  </div>
263
335
  );
264
336
  };