@jielga/tmdatagrid 0.3.0 → 1.0.0-beta.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 (70) hide show
  1. package/README.md +25 -2
  2. package/dist/index.d.ts +1789 -64
  3. package/dist/index.js +4432 -1561
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/dist/styles.layer.css +1 -0
  7. package/package.json +13 -3
  8. package/skills/columns/SKILL.md +1 -1
  9. package/skills/features/SKILL.md +61 -11
  10. package/skills/getting-started/SKILL.md +1 -1
  11. package/skills/options/SKILL.md +12 -2
  12. package/skills/server-side/SKILL.md +1 -1
  13. package/skills/testing/SKILL.md +198 -0
  14. package/src/tmdatagrid/TMDataGridContext.ts +26 -0
  15. package/src/tmdatagrid/components/TMDataGrid.module.css +21 -0
  16. package/src/tmdatagrid/components/TMDataGrid.tsx +57 -1
  17. package/src/tmdatagrid/components/TMDataGridCellEditor.tsx +256 -0
  18. package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +38 -15
  19. package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
  20. package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +163 -0
  21. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +62 -0
  22. package/src/tmdatagrid/components/TMDataGridEditColumn.tsx +237 -0
  23. package/src/tmdatagrid/components/TMDataGridEntryRows.tsx +164 -0
  24. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +131 -38
  25. package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +11 -4
  26. package/src/tmdatagrid/components/TMDataGridFooter.tsx +21 -22
  27. package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +25 -11
  28. package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -3
  29. package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +191 -53
  30. package/src/tmdatagrid/components/TMDataGridRowNumberColumn.tsx +44 -0
  31. package/src/tmdatagrid/components/TMDataGridSearch.tsx +124 -0
  32. package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +26 -13
  33. package/src/tmdatagrid/components/TMDataGridTable.module.css +333 -6
  34. package/src/tmdatagrid/components/TMDataGridTable.tsx +1819 -62
  35. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +41 -13
  36. package/src/tmdatagrid/components/editors/TMDataGridBooleanEditor.tsx +29 -0
  37. package/src/tmdatagrid/components/editors/TMDataGridDateEditor.tsx +57 -0
  38. package/src/tmdatagrid/components/editors/TMDataGridMultiSelectEditor.tsx +45 -0
  39. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +38 -0
  40. package/src/tmdatagrid/components/editors/TMDataGridSelectEditor.tsx +53 -0
  41. package/src/tmdatagrid/components/editors/TMDataGridStringEditor.tsx +31 -0
  42. package/src/tmdatagrid/components/editors/editorShared.ts +48 -0
  43. package/src/tmdatagrid/components/filters/DgAutocompleteFilter.tsx +46 -0
  44. package/src/tmdatagrid/components/filters/DgDateRangeFilter.tsx +43 -0
  45. package/src/tmdatagrid/components/filters/DgRangeSliderFilter.tsx +52 -0
  46. package/src/tmdatagrid/components/filters/DgTriStateFilter.tsx +38 -0
  47. package/src/tmdatagrid/components/filters/TMDataGridFilterValueInput.tsx +122 -0
  48. package/src/tmdatagrid/components/icons.ts +5 -0
  49. package/src/tmdatagrid/components/sticky.module.css +77 -16
  50. package/src/tmdatagrid/core/autosize.ts +125 -0
  51. package/src/tmdatagrid/core/capabilities.ts +77 -0
  52. package/src/tmdatagrid/core/cellExport.ts +321 -0
  53. package/src/tmdatagrid/core/cellNavigation.ts +144 -0
  54. package/src/tmdatagrid/core/cellRange.ts +118 -0
  55. package/src/tmdatagrid/core/columnOptions.ts +123 -0
  56. package/src/tmdatagrid/core/columnUtils.ts +44 -1
  57. package/src/tmdatagrid/core/editEngine.ts +857 -0
  58. package/src/tmdatagrid/core/expanding.ts +93 -0
  59. package/src/tmdatagrid/core/filterControls.ts +45 -0
  60. package/src/tmdatagrid/core/filterOperators.ts +243 -14
  61. package/src/tmdatagrid/core/labels.ts +278 -0
  62. package/src/tmdatagrid/core/labelsSv.ts +131 -0
  63. package/src/tmdatagrid/core/matchHighlight.ts +96 -0
  64. package/src/tmdatagrid/core/persistence.ts +97 -3
  65. package/src/tmdatagrid/core/quickSearch.ts +144 -0
  66. package/src/tmdatagrid/core/rowPinning.ts +37 -0
  67. package/src/tmdatagrid/core/rowSelection.ts +10 -1
  68. package/src/tmdatagrid/core/summary.ts +35 -0
  69. package/src/tmdatagrid/index.ts +121 -0
  70. package/src/tmdatagrid/useTMDataGrid.tsx +782 -25
@@ -0,0 +1,237 @@
1
+ import { ActionIcon, Group, Loader, Tooltip } from "@mantine/core";
2
+ import { useSelector } from "@tanstack/react-store";
3
+ import type { ColumnDef, Row, RowData } from "@tanstack/react-table";
4
+ import {
5
+ useCellControlTabIndex,
6
+ useTMDataGridContext,
7
+ } from "../TMDataGridContext";
8
+ import type { TMDataGridFeatures } from "../useTMDataGrid";
9
+ import { firstErrorText } from "./editors/editorShared";
10
+ import {
11
+ CheckIcon,
12
+ CloseIcon,
13
+ PencilIcon,
14
+ RestoreIcon,
15
+ TrashIcon,
16
+ } from "./icons";
17
+
18
+ export const EDIT_COLUMN_ID = "__edit__";
19
+
20
+ /**
21
+ * One row's slot in the edit lane. Three states, driven off the edit store:
22
+ * a pencil while idle, Save/Cancel while the row's form is open, a loader
23
+ * while its commit is in flight. A row error turns the Save red and puts the
24
+ * message in its tooltip — the pathless `.refine()` has nowhere else to land.
25
+ */
26
+ function EditLaneCell<TData extends RowData>({
27
+ row,
28
+ }: {
29
+ row: Row<TMDataGridFeatures, TData>;
30
+ }) {
31
+ const { edit, features, labels } = useTMDataGridContext();
32
+ const tabIndex = useCellControlTabIndex();
33
+ const rowId = row.id;
34
+ const isOpen = useSelector(edit.store, (state) =>
35
+ state.openRowIds.includes(rowId),
36
+ );
37
+ const projection = useSelector(edit.store, (state) => state.rows[rowId]);
38
+ const isNew = useSelector(edit.store, (state) =>
39
+ state.newRows.some((newRow) => newRow.tempId === rowId),
40
+ );
41
+ const isMarkedDeleted = useSelector(edit.store, (state) =>
42
+ state.deletedRowIds.includes(rowId),
43
+ );
44
+
45
+ if (row.getIsGrouped()) return null;
46
+
47
+ // An entry row: ✓ adds it (not under batch, where submitAll does), ✕
48
+ // discards the entry.
49
+ if (isNew) {
50
+ return (
51
+ <Group gap={0} wrap="nowrap">
52
+ {features.editMode !== "batch" && (
53
+ <ActionIcon
54
+ variant="subtle"
55
+ color="green"
56
+ size="sm"
57
+ tabIndex={tabIndex}
58
+ aria-label={labels.confirmNewRow}
59
+ data-dg-part="confirm-new-row"
60
+ data-row-id={rowId}
61
+ onClick={() => void edit.commit(rowId)}
62
+ >
63
+ <CheckIcon size={16} stroke={2} />
64
+ </ActionIcon>
65
+ )}
66
+ <ActionIcon
67
+ variant="subtle"
68
+ color="gray"
69
+ size="sm"
70
+ tabIndex={tabIndex}
71
+ aria-label={labels.discardNewRow}
72
+ data-dg-part="discard-new-row"
73
+ data-row-id={rowId}
74
+ onClick={() => edit.cancel(rowId)}
75
+ >
76
+ <CloseIcon size={16} stroke={1.6} />
77
+ </ActionIcon>
78
+ </Group>
79
+ );
80
+ }
81
+
82
+ // A batch deletion mark is a draft like any other — restorable in place.
83
+ if (isMarkedDeleted) {
84
+ return (
85
+ <ActionIcon
86
+ variant="subtle"
87
+ color="gray"
88
+ size="sm"
89
+ tabIndex={tabIndex}
90
+ aria-label={labels.restoreRow}
91
+ data-dg-part="restore-row"
92
+ data-row-id={rowId}
93
+ onClick={(event) => {
94
+ event.stopPropagation();
95
+ edit.deleteRow(rowId);
96
+ }}
97
+ >
98
+ <RestoreIcon size={16} stroke={1.6} />
99
+ </ActionIcon>
100
+ );
101
+ }
102
+
103
+ // The pencil means "this row edits"; a row that does not gets nothing.
104
+ if (!edit.canEditRow(row as never)) return null;
105
+
106
+ if (!isOpen) {
107
+ return (
108
+ <Group gap={0} wrap="nowrap">
109
+ {features.editMode === "row" && (
110
+ <ActionIcon
111
+ variant="subtle"
112
+ color="gray"
113
+ size="sm"
114
+ tabIndex={tabIndex}
115
+ aria-label={labels.editRow}
116
+ data-dg-part="edit-row"
117
+ data-row-id={rowId}
118
+ onClick={(event) => {
119
+ event.stopPropagation();
120
+ edit.begin({ rowId, columnId: null });
121
+ }}
122
+ >
123
+ <PencilIcon size={16} stroke={1.6} />
124
+ </ActionIcon>
125
+ )}
126
+ {edit.canDeleteRows() && (
127
+ <ActionIcon
128
+ variant="subtle"
129
+ color="gray"
130
+ size="sm"
131
+ tabIndex={tabIndex}
132
+ aria-label={labels.deleteRow}
133
+ data-dg-part="delete-row"
134
+ data-row-id={rowId}
135
+ onClick={(event) => {
136
+ event.stopPropagation();
137
+ edit.deleteRow(rowId);
138
+ }}
139
+ >
140
+ <TrashIcon size={16} stroke={1.6} />
141
+ </ActionIcon>
142
+ )}
143
+ </Group>
144
+ );
145
+ }
146
+
147
+ if (projection?.isSubmitting) {
148
+ return <Loader size="xs" aria-label={labels.loading} />;
149
+ }
150
+
151
+ const hasErrors =
152
+ projection !== undefined &&
153
+ (projection.hasRowError || projection.errorFields.length > 0);
154
+ const rowError = hasErrors
155
+ ? (firstErrorText(edit.getForm(rowId)?.state.errors ?? []) ??
156
+ labels.editRowErrors)
157
+ : null;
158
+
159
+ const save = (
160
+ <ActionIcon
161
+ variant="subtle"
162
+ color={hasErrors ? "red" : "green"}
163
+ size="sm"
164
+ tabIndex={tabIndex}
165
+ aria-label={labels.saveRow}
166
+ data-dg-part="save-row"
167
+ data-row-id={rowId}
168
+ onClick={(event) => {
169
+ event.stopPropagation();
170
+ void edit.commit(rowId);
171
+ }}
172
+ >
173
+ <CheckIcon size={16} stroke={2} />
174
+ </ActionIcon>
175
+ );
176
+
177
+ return (
178
+ <Group gap={0} wrap="nowrap">
179
+ {rowError === null ? (
180
+ save
181
+ ) : (
182
+ <Tooltip label={rowError} withArrow>
183
+ {save}
184
+ </Tooltip>
185
+ )}
186
+ <ActionIcon
187
+ variant="subtle"
188
+ color="gray"
189
+ size="sm"
190
+ tabIndex={tabIndex}
191
+ aria-label={labels.cancelRowEdit}
192
+ data-dg-part="cancel-row"
193
+ data-row-id={rowId}
194
+ onClick={(event) => {
195
+ event.stopPropagation();
196
+ edit.cancel(rowId);
197
+ }}
198
+ >
199
+ <CloseIcon size={16} stroke={1.6} />
200
+ </ActionIcon>
201
+ </Group>
202
+ );
203
+ }
204
+
205
+ /**
206
+ * The generated edit lane, appended and pinned right under
207
+ * `editMode: "row"` — the row's Save at the end of the row, mirroring the
208
+ * checkbox lane's build on the left.
209
+ */
210
+ export function createEditColumn<TData extends RowData>(
211
+ label = "Edit",
212
+ ): ColumnDef<TMDataGridFeatures, TData, unknown> {
213
+ return {
214
+ id: EDIT_COLUMN_ID,
215
+ meta: {
216
+ label,
217
+ align: "center",
218
+ enableOrdering: false,
219
+ },
220
+ // Wide enough for the Save/Cancel pair it holds while editing.
221
+ size: 64,
222
+ minSize: 64,
223
+ maxSize: 64,
224
+ enableResizing: false,
225
+ enableSorting: false,
226
+ enableColumnFilter: false,
227
+ enableGlobalFilter: false,
228
+ // Structurally pinned to the right; not movable.
229
+ enablePinning: false,
230
+ header: () => null,
231
+ cell: ({ row }) => <EditLaneCell row={row} />,
232
+ // Group rows: same reasoning as the checkbox lane — without this the
233
+ // cell renders blank on aggregated rows, but here blank is also correct,
234
+ // so the aggregated cell renders the same (null for groups).
235
+ aggregatedCell: ({ row }) => <EditLaneCell row={row} />,
236
+ };
237
+ }
@@ -0,0 +1,164 @@
1
+ import {
2
+ flexRender,
3
+ useTable,
4
+ type Column,
5
+ type Row,
6
+ } from "@tanstack/react-table";
7
+ import { useSelector } from "@tanstack/react-store";
8
+ import { useMemo } from "react";
9
+ import classes from "./TMDataGridTable.module.css";
10
+ import sticky from "./sticky.module.css";
11
+ import { type TMDataGridRowData, useTMDataGridContext } from "../TMDataGridContext";
12
+ import { getColumnAlign, isControlColumn } from "../core/columnUtils";
13
+ import { getEditFieldName } from "../core/editEngine";
14
+ import {
15
+ tmDataGridFeatures,
16
+ type TMDataGridFeatures,
17
+ } from "../useTMDataGrid";
18
+ import { TMDataGridCellEditor } from "./TMDataGridCellEditor";
19
+ import { EDIT_COLUMN_ID } from "./TMDataGridEditColumn";
20
+ import type { TMDataGridColumnLayout } from "./TMDataGridTable";
21
+
22
+ type ErasedColumn = Column<TMDataGridFeatures, TMDataGridRowData, unknown>;
23
+
24
+ /**
25
+ * Whether an entry cell edits. `edit.canEditCell` asks the main table for
26
+ * the row, and an entry row is precisely not there yet — so the structural
27
+ * half of the same rule is applied to the entry table's own row.
28
+ */
29
+ function isEntryCellEditable(
30
+ row: Row<TMDataGridFeatures, TMDataGridRowData>,
31
+ column: ErasedColumn,
32
+ ): boolean {
33
+ if (isControlColumn(column.id)) return false;
34
+ if (getEditFieldName(column) === null) return false;
35
+ const editable = column.columnDef.meta?.editable;
36
+ if (editable === false) return false;
37
+ if (typeof editable === "function" && !editable(row)) return false;
38
+ return true;
39
+ }
40
+
41
+ /**
42
+ * The sticky entry block — one row of open editors per `edit.addRow()`,
43
+ * pinned under the header. The one place stickiness is genuinely required:
44
+ * an existing row that scrolls away has a place to scroll back to, a row
45
+ * being typed into does not exist anywhere else.
46
+ *
47
+ * A second, tiny table instance over the new rows' seed values — same
48
+ * columns, its own `useTable`, no sorting or filtering ever exercised — so
49
+ * these are real `Cell`s and the editor host, the typed editors and the
50
+ * validators all apply unchanged. The forms live in the same engine map as
51
+ * every other draft, keyed by their temporary ids.
52
+ *
53
+ * Enter commits the entry (`onRowAdd`) under the immediate modes and parks
54
+ * it under batch, where `submitAll` reports it in `added`; Escape discards
55
+ * it. The edit lane, when present, offers the same pair as buttons.
56
+ */
57
+ export function TMDataGridEntryRows({
58
+ orderedColumns,
59
+ layoutFor,
60
+ rowHeight,
61
+ }: {
62
+ orderedColumns: ReadonlyArray<ErasedColumn>;
63
+ layoutFor: (columnId: string) => TMDataGridColumnLayout;
64
+ rowHeight: number;
65
+ }) {
66
+ const { table, edit } = useTMDataGridContext();
67
+ const newRows = useSelector(edit.store, (state) => state.newRows);
68
+
69
+ // The seed values, frozen at addRow: the live values belong to the forms,
70
+ // which the editors read directly — this table only provides row and cell
71
+ // identity.
72
+ const data = useMemo(
73
+ () =>
74
+ newRows.map(
75
+ ({ tempId }) =>
76
+ (edit.getForm(tempId)?.options.defaultValues ??
77
+ {}) as TMDataGridRowData,
78
+ ),
79
+ [newRows, edit],
80
+ );
81
+
82
+ const entryTable = useTable({
83
+ features: tmDataGridFeatures,
84
+ columns: table.options.columns,
85
+ data,
86
+ getRowId: (_row, index) => newRows[index]?.tempId ?? String(index),
87
+ });
88
+
89
+ if (newRows.length === 0) return null;
90
+
91
+ const entryRows = entryTable.getCoreRowModel().rows;
92
+
93
+ return (
94
+ <div role="rowgroup" data-dg-entry-block className={classes.entryBlock}>
95
+ {entryRows.map((entryRow) => {
96
+ const cellsById = new Map(
97
+ entryRow.getAllCells().map((cell) => [cell.column.id, cell]),
98
+ );
99
+ const firstEditableColumnId = orderedColumns.find((column) =>
100
+ isEntryCellEditable(entryRow, column),
101
+ )?.id;
102
+ return (
103
+ <div
104
+ key={entryRow.id}
105
+ role="row"
106
+ data-dg-part="entry-row"
107
+ data-row-id={entryRow.id}
108
+ className={classes.entryRow}
109
+ >
110
+ {orderedColumns.map((column) => {
111
+ const cell = cellsById.get(column.id);
112
+ const layout = layoutFor(column.id);
113
+ const editable =
114
+ cell !== undefined && isEntryCellEditable(entryRow, column);
115
+ return (
116
+ <div
117
+ key={column.id}
118
+ role="cell"
119
+ data-column-id={column.id}
120
+ data-align={getColumnAlign(column)}
121
+ data-control-column={isControlColumn(column.id)}
122
+ className={[
123
+ classes.entryCell,
124
+ layout.isBoundary && layout.pinnedAt === "left"
125
+ ? sticky.stickyLeft
126
+ : "",
127
+ layout.isBoundary && layout.pinnedAt === "right"
128
+ ? sticky.stickyRight
129
+ : "",
130
+ ]
131
+ .filter(Boolean)
132
+ .join(" ")}
133
+ style={{
134
+ minHeight: rowHeight,
135
+ left: layout.pinnedAt === "left" ? layout.offset : undefined,
136
+ right:
137
+ layout.pinnedAt === "right" ? layout.offset : undefined,
138
+ position: layout.pinnedAt ? "sticky" : undefined,
139
+ zIndex: layout.pinnedAt
140
+ ? "var(--dg-z-pinned-row-pinned-cell, 5)"
141
+ : undefined,
142
+ }}
143
+ >
144
+ {editable && cell !== undefined ? (
145
+ <TMDataGridCellEditor
146
+ cell={cell}
147
+ row={entryRow}
148
+ takeSeedText={() => undefined}
149
+ autoFocus={column.id === firstEditableColumnId}
150
+ onClose={() => {}}
151
+ />
152
+ ) : cell !== undefined && column.id === EDIT_COLUMN_ID ? (
153
+ // The lane's cell — the entry row's ✓/✕ pair.
154
+ flexRender(cell.column.columnDef.cell, cell.getContext())
155
+ ) : null}
156
+ </div>
157
+ );
158
+ })}
159
+ </div>
160
+ );
161
+ })}
162
+ </div>
163
+ );
164
+ }