@dxos/react-ui-table 0.8.4-main.1da679c → 0.8.4-main.21d9917

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 (140) hide show
  1. package/dist/lib/browser/chunk-MNZKOFMA.mjs +1746 -0
  2. package/dist/lib/browser/chunk-MNZKOFMA.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1134 -1150
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +29 -39
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/chunk-2ATAIIAO.mjs +1748 -0
  12. package/dist/lib/node-esm/chunk-2ATAIIAO.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1134 -1150
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +29 -39
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +5 -6
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +6 -20
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +77 -10
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/Table.d.ts +11 -8
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +76 -11
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -12
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -7
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +5 -6
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  44. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  45. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  47. package/dist/types/src/components/testing.d.ts +23 -16
  48. package/dist/types/src/components/testing.d.ts.map +1 -1
  49. package/dist/types/src/hooks/index.d.ts +2 -1
  50. package/dist/types/src/hooks/index.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  52. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  54. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -8
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/model/modal-controller.d.ts +5 -8
  58. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  59. package/dist/types/src/model/selection-model.d.ts +17 -8
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +103 -24
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +3 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  66. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  67. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  68. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  69. package/dist/types/src/translations.d.ts +4 -4
  70. package/dist/types/src/translations.d.ts.map +1 -1
  71. package/dist/types/src/types/Table.d.ts +31 -0
  72. package/dist/types/src/types/Table.d.ts.map +1 -0
  73. package/dist/types/src/types/index.d.ts +1 -1
  74. package/dist/types/src/types/index.d.ts.map +1 -1
  75. package/dist/types/src/util/dynamic-table.d.ts +13 -12
  76. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  77. package/dist/types/src/util/index.d.ts +2 -0
  78. package/dist/types/src/util/index.d.ts.map +1 -1
  79. package/dist/types/src/util/query-ast.d.ts +7 -0
  80. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  81. package/dist/types/src/util/schema.d.ts +1 -1
  82. package/dist/types/src/util/schema.d.ts.map +1 -1
  83. package/dist/types/src/util/sort.d.ts +12 -0
  84. package/dist/types/src/util/sort.d.ts.map +1 -0
  85. package/dist/types/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +56 -56
  87. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  88. package/src/components/Table/ColumnSettings.tsx +6 -6
  89. package/src/components/Table/DynamicTable.stories.tsx +18 -18
  90. package/src/components/Table/DynamicTable.tsx +23 -19
  91. package/src/components/Table/Relations.stories.tsx +143 -39
  92. package/src/components/Table/RowActionsMenu.tsx +24 -15
  93. package/src/components/Table/Table.stories.tsx +92 -95
  94. package/src/components/Table/Table.tsx +339 -307
  95. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  96. package/src/components/TableCellEditor/FormCellEditor.tsx +116 -129
  97. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +23 -47
  98. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -122
  99. package/src/components/TableCellEditor/index.ts +1 -0
  100. package/src/components/TableToolbar/TableToolbar.tsx +47 -31
  101. package/src/components/testing.ts +39 -13
  102. package/src/hooks/index.ts +2 -1
  103. package/src/hooks/useAddRow.ts +8 -10
  104. package/src/hooks/useProjectionModel.ts +40 -0
  105. package/src/hooks/useTableModel.ts +35 -33
  106. package/src/model/modal-controller.ts +23 -40
  107. package/src/model/selection-model.ts +75 -44
  108. package/src/model/table-model.test.ts +91 -58
  109. package/src/model/table-model.ts +429 -163
  110. package/src/model/table-presentation.test.ts +64 -57
  111. package/src/model/table-presentation.ts +34 -30
  112. package/src/playwright/TableManager.ts +5 -5
  113. package/src/playwright/playwright.config.ts +1 -1
  114. package/src/playwright/smoke.spec.ts +11 -8
  115. package/src/testing/useTestTableModel.ts +44 -35
  116. package/src/translations.ts +3 -3
  117. package/src/types/Table.ts +90 -0
  118. package/src/types/index.ts +1 -1
  119. package/src/util/dynamic-table.ts +41 -30
  120. package/src/util/index.ts +2 -0
  121. package/src/util/query-ast.ts +19 -0
  122. package/src/util/schema.ts +4 -4
  123. package/src/util/sort.ts +90 -0
  124. package/dist/lib/browser/chunk-63UUF4IG.mjs +0 -66
  125. package/dist/lib/browser/chunk-63UUF4IG.mjs.map +0 -7
  126. package/dist/lib/browser/chunk-BBXDTWCX.mjs +0 -1822
  127. package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-K734DJB7.mjs +0 -67
  129. package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +0 -7
  130. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs +0 -1823
  131. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +0 -7
  132. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  133. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  134. package/dist/types/src/model/table-sorting.d.ts +0 -52
  135. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  136. package/dist/types/src/types/schema.d.ts +0 -28
  137. package/dist/types/src/types/schema.d.ts.map +0 -1
  138. package/src/components/Table/CreateRefPanel.tsx +0 -103
  139. package/src/model/table-sorting.ts +0 -225
  140. package/src/types/schema.ts +0 -73
@@ -2,24 +2,28 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect/Schema';
6
- import { trim } from 'effect/String';
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import * as String from 'effect/String';
7
7
  import React, {
8
+ type JSX,
8
9
  type MouseEvent,
9
10
  type PropsWithChildren,
11
+ type Ref,
10
12
  type WheelEvent,
11
13
  forwardRef,
12
14
  useCallback,
15
+ useContext,
13
16
  useEffect,
14
17
  useImperativeHandle,
15
18
  useMemo,
16
19
  useState,
17
20
  } from 'react';
18
21
 
19
- import { type Client } from '@dxos/react-client';
20
- // TODO(wittjosiah): Remove dependency on react-client.
22
+ import { type Type } from '@dxos/echo';
23
+ import { log } from '@dxos/log';
21
24
  import { useAttention } from '@dxos/react-ui-attention';
22
25
  import {
26
+ type DxGridAxisMeta,
23
27
  type DxGridElement,
24
28
  type DxGridPlane,
25
29
  type DxGridPlaneRange,
@@ -31,19 +35,19 @@ import {
31
35
  gridSeparatorInlineEnd,
32
36
  } from '@dxos/react-ui-grid';
33
37
  import { DxEditRequest } from '@dxos/react-ui-grid';
34
- import { mx } from '@dxos/react-ui-theme';
38
+ import { mx } from '@dxos/ui-theme';
35
39
 
36
40
  import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
37
41
  import { tableButtons, tableControls } from '../../util';
38
- import { type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
42
+ import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
39
43
 
40
44
  import { ColumnActionsMenu } from './ColumnActionsMenu';
41
45
  import { ColumnSettings } from './ColumnSettings';
42
- import { CreateRefPanel } from './CreateRefPanel';
43
46
  import { RowActionsMenu } from './RowActionsMenu';
44
47
 
45
48
  const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
46
49
  const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
50
+ const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
47
51
 
48
52
  //
49
53
  // Table.Root
@@ -78,359 +82,387 @@ export type TableController = {
78
82
  handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
79
83
  };
80
84
 
81
- export type TableMainProps = {
85
+ export type TableMainProps<T extends Type.Entity.Any = Type.Entity.Any> = {
86
+ schema?: T;
82
87
  model?: TableModel;
83
88
  presentation?: TablePresentation;
84
- schema?: Schema.AnyNoContext;
85
- client?: Client;
86
89
  // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
87
90
  ignoreAttention?: boolean;
91
+ onCreate?: OnCreateHandler;
88
92
  onRowClick?: (row: any) => void;
93
+ testId?: string;
89
94
  };
90
95
 
91
- const TableMain = forwardRef<TableController, TableMainProps>(
92
- ({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
93
- const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
94
- const { hasAttention } = useAttention(model?.id ?? 'table');
95
- const modals = useMemo(() => new ModalController(), []);
96
-
97
- const draftRowCount = model?.getDraftRowCount() ?? 0;
98
-
99
- const handleSave = useCallback(() => {
100
- dxGrid?.updateCells(true);
101
- dxGrid?.requestUpdate();
102
- }, [dxGrid]);
103
-
104
- const frozen = useMemo(() => {
105
- const noActionColumn =
106
- model?.features.dataEditable === false &&
107
- model?.features.schemaEditable === false &&
108
- model.rowActions.length === 0;
109
-
110
- return {
111
- frozenRowsStart: 1,
112
- frozenRowsEnd: Math.max(1, draftRowCount),
113
- frozenColsStart: model?.features.selection.enabled ? 1 : 0,
114
- frozenColsEnd: noActionColumn ? 0 : 1,
115
- };
116
- }, [model, draftRowCount]);
117
-
118
- const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
119
- (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
120
- [presentation],
121
- );
122
-
123
- useEffect(() => {
124
- if (!presentation || !dxGrid) {
125
- return;
96
+ const emptyRowsAtom = Atom.make<unknown[]>([]);
97
+ const emptyCellUpdateAtom = Atom.make<number>(0);
98
+
99
+ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
100
+ { schema, model, presentation, ignoreAttention, onCreate, onRowClick, testId }: TableMainProps<T>,
101
+ forwardedRef: Ref<TableController>,
102
+ ) => {
103
+ const registry = useContext(RegistryContext);
104
+ const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
105
+ const { hasAttention } = useAttention(model?.id ?? 'table');
106
+ const modals = useMemo(() => new ModalController(registry), [registry]);
107
+ const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
108
+ // Subscribe to rows atom to trigger re-render when rows change.
109
+ const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
110
+ // Subscribe to cell update counter to trigger re-render when individual cells change.
111
+ const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
112
+ // Derive column count from columnMeta (reactive) with fallback.
113
+ const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
114
+
115
+ const draftRowCount = model?.getDraftRowCount() ?? 0;
116
+
117
+ const frozen = useMemo(() => {
118
+ const noActionColumn =
119
+ model?.features.dataEditable === false &&
120
+ model?.features.schemaEditable === false &&
121
+ model.rowActions.length === 0;
122
+
123
+ return {
124
+ frozenRowsStart: 1,
125
+ frozenRowsEnd: Math.max(1, draftRowCount),
126
+ frozenColsStart: model?.features.selection.enabled ? 1 : 0,
127
+ frozenColsEnd: noActionColumn ? 0 : 1,
128
+ };
129
+ }, [model, draftRowCount]);
130
+
131
+ const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
132
+ (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
133
+ [presentation],
134
+ );
135
+
136
+ useEffect(() => {
137
+ if (!dxGrid || !presentation) {
138
+ return;
139
+ }
140
+
141
+ dxGrid.getCells = getCells;
142
+ }, [dxGrid, presentation, getCells]);
143
+
144
+ // Trigger grid update when rows or cell values change.
145
+ useEffect(() => {
146
+ if (!dxGrid) {
147
+ return;
148
+ }
149
+
150
+ dxGrid.updateCells(true);
151
+ dxGrid.requestUpdate();
152
+ }, [dxGrid, rows, cellUpdateCounter]);
153
+
154
+ const handleInsertRowResult = useCallback(
155
+ (insertResult?: InsertRowResult) => {
156
+ if (insertResult === 'draft') {
157
+ requestAnimationFrame(() => {
158
+ dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
159
+ dxGrid?.refocus();
160
+ });
161
+ } else {
162
+ requestAnimationFrame(() => {
163
+ dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
164
+ dxGrid?.refocus();
165
+ });
126
166
  }
127
- dxGrid.getCells = getCells;
128
- }, [presentation, dxGrid, getCells]);
167
+ },
168
+ [model, dxGrid],
169
+ );
129
170
 
130
- const handleInsertRowResult = useCallback(
131
- (insertResult?: InsertRowResult) => {
132
- if (insertResult === 'draft') {
133
- requestAnimationFrame(() => {
134
- dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
135
- dxGrid?.refocus();
136
- });
137
- } else {
171
+ const handleSave = useCallback(() => {
172
+ dxGrid?.updateCells(true);
173
+ dxGrid?.requestUpdate();
174
+ }, [dxGrid]);
175
+
176
+ const handleSaveDraftRow = useCallback(
177
+ (rowIndex = 0) => {
178
+ if (model && dxGrid) {
179
+ const didCommitSuccessfully = model.commitDraftRow(rowIndex);
180
+ if (didCommitSuccessfully) {
138
181
  requestAnimationFrame(() => {
139
- dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
140
- dxGrid?.refocus();
182
+ dxGrid.scrollToEndRow();
141
183
  });
142
184
  }
143
- },
144
- [model, dxGrid],
145
- );
146
-
147
- /**
148
- * Provides an external controller that can be called to repaint the table.
149
- */
150
- useImperativeHandle<TableController, TableController>(forwardedRef, () => {
151
- if (!presentation || !dxGrid) {
152
- return {};
153
185
  }
186
+ },
187
+ [model, dxGrid],
188
+ );
154
189
 
155
- return {
156
- update: (cell) => {
157
- if (cell) {
158
- dxGrid.updateIfWithinBounds(cell, true);
190
+ const handleGridClick = useCallback(
191
+ (event: MouseEvent) => {
192
+ const cell = closestCell(event.target as HTMLElement);
193
+ if (cell) {
194
+ const { row: rowIndex, plane } = cell;
195
+ if (onRowClick) {
196
+ if (plane === 'grid') {
197
+ const row = model?.getRowAt(rowIndex);
198
+ row && onRowClick(row);
159
199
  } else {
160
- dxGrid.updateCells(true);
161
- dxGrid.requestUpdate();
162
- }
163
- },
164
- handleInsertRowResult,
165
- };
166
- }, [presentation, dxGrid, model]);
167
-
168
- const handleSaveDraftRow = useCallback(
169
- (rowIndex = 0) => {
170
- if (model && dxGrid) {
171
- const didCommitSuccessfully = model.commitDraftRow(rowIndex);
172
- if (didCommitSuccessfully) {
173
- requestAnimationFrame(() => {
174
- dxGrid.scrollToEndRow();
175
- });
200
+ onRowClick(cell);
176
201
  }
177
202
  }
178
- },
179
- [model, dxGrid],
180
- );
181
203
 
182
- const handleGridClick = useCallback(
183
- (event: MouseEvent) => {
184
- const cell = closestCell(event.target as HTMLElement);
185
- if (cell) {
186
- const { row: rowIndex, plane } = cell;
187
- if (onRowClick) {
188
- if (plane === 'grid') {
189
- const row = model?.getRowAt(rowIndex);
190
- row && onRowClick(row);
191
- } else {
192
- onRowClick(cell);
193
- }
194
- }
195
-
196
- if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
197
- model.selection.toggleSelectionForRowIndex(rowIndex);
198
- }
199
- }
200
-
201
- if (!modals) {
202
- return;
204
+ if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
205
+ model.selection.toggleSelectionForRowIndex(rowIndex);
203
206
  }
207
+ }
204
208
 
205
- const buttonSelector = Object.values(tableButtons)
206
- .map((button) => `button[${button.attr}]`)
207
- .join(',');
209
+ if (!modals) {
210
+ return;
211
+ }
208
212
 
209
- const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
210
- if (tableButtonElement) {
211
- const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
212
- if (!button) {
213
- return;
214
- }
213
+ const buttonSelector = Object.values(tableButtons)
214
+ .map((button) => `button[${button.attr}]`)
215
+ .join(',');
215
216
 
216
- modals.setTrigger(tableButtonElement);
217
- const data = button.getData(tableButtonElement);
218
- switch (data.type) {
219
- case 'rowMenu': {
220
- modals.showRowMenu(data.rowIndex);
221
- break;
222
- }
223
- case 'columnSettings': {
224
- modals.showColumnMenu(data.fieldId);
225
- break;
226
- }
227
- case 'newColumn': {
228
- modals.showColumnCreator();
229
- break;
230
- }
231
- case 'sort': {
232
- model?.sorting?.toggleSort(data.fieldId);
233
- break;
234
- }
235
- case 'saveDraftRow': {
236
- handleSaveDraftRow();
237
- break;
238
- }
239
- }
217
+ const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
218
+ if (tableButtonElement) {
219
+ const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
220
+ if (!button) {
240
221
  return;
241
222
  }
242
223
 
243
- const controlSelector = Object.values(tableControls)
244
- .map((control) => `input[${control.attr}]`)
245
- .join(',');
246
-
247
- const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
248
- if (controlElement) {
249
- const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
250
- if (!control) {
251
- return;
224
+ modals.setTrigger(tableButtonElement);
225
+ const data = button.getData(tableButtonElement);
226
+ switch (data.type) {
227
+ case 'rowMenu': {
228
+ modals.showRowMenu(data.rowIndex);
229
+ break;
252
230
  }
253
-
254
- const data = control.getData(controlElement);
255
- switch (data.type) {
256
- case 'checkbox': {
257
- if (data.header) {
258
- model?.selection.setSelection(model.selection.allRowsSeleted.value ? 'none' : 'all');
259
- } else {
260
- model?.selection.toggleSelectionForRowIndex(data.rowIndex);
261
- }
262
- break;
263
- }
264
- case 'switch': {
265
- if (model) {
266
- model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
267
- }
268
- break;
269
- }
231
+ case 'columnSettings': {
232
+ modals.showColumnMenu(data.fieldId);
233
+ break;
270
234
  }
271
- }
272
- },
273
- [model, modals, dxGrid],
274
- );
275
-
276
- const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
277
- (increment, delta, cell) => {
278
- if (dxGrid && model) {
279
- if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1) {
280
- handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
281
- } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
282
- handleSaveDraftRow(cell.row);
283
- handleInsertRowResult(model.insertRow());
284
- } else {
285
- dxGrid.refocus(increment, delta);
235
+ case 'newColumn': {
236
+ modals.showColumnCreator();
237
+ break;
238
+ }
239
+ case 'sort': {
240
+ model?.toggleSort(data.fieldId);
241
+ break;
242
+ }
243
+ case 'saveDraftRow': {
244
+ handleSaveDraftRow();
245
+ break;
286
246
  }
287
247
  }
288
- },
289
- [dxGrid, model],
290
- );
248
+ return;
249
+ }
291
250
 
292
- const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
293
- (event) => {
294
- if (!model?.features.dataEditable) {
295
- return;
296
- }
251
+ const controlSelector = Object.values(tableControls)
252
+ .map((control) => `input[${control.attr}]`)
253
+ .join(',');
297
254
 
298
- const cell = closestCell(event.target);
299
- if (!model || !cell) {
255
+ const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
256
+ if (controlElement) {
257
+ const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
258
+ if (!control) {
300
259
  return;
301
260
  }
302
261
 
303
- // Handle Meta+C (Copy) and Meta+V (Paste) commands
304
- if (event.metaKey || event.ctrlKey) {
305
- switch (event.key) {
306
- case 'c': {
307
- // Copy focused cell's text content to clipboard
308
- try {
309
- const cellData = model.getCellData(cell);
310
- const textContent = cellData?.toString() ?? '';
311
- void navigator.clipboard.writeText(textContent);
312
- event.preventDefault();
313
- } catch (error) {
314
- console.warn('Failed to copy cell content:', error);
315
- }
316
- break;
262
+ const data = control.getData(controlElement);
263
+ switch (data.type) {
264
+ case 'checkbox': {
265
+ if (data.header) {
266
+ model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
267
+ } else {
268
+ model?.selection.toggleSelectionForRowIndex(data.rowIndex);
317
269
  }
318
- case 'v': {
319
- // Paste clipboard content to focused cell
320
- event.preventDefault();
321
- void navigator.clipboard.readText().then((clipboardText) => {
322
- try {
323
- // Attempt to set the cell's content to clipboard content
324
- model.setCellData(cell, trim(clipboardText));
325
- handleSave();
326
- } catch {
327
- // If validation fails, emit a DxEditRequest event with initialContent from clipboard
328
- // TODO(thure): Should `dx-grid` expose a method like this?
329
- const cellElement = (event.target as HTMLElement).closest(
330
- '[data-dx-grid-action="cell"]',
331
- ) as HTMLElement;
332
- if (cellElement) {
333
- const rect = cellElement.getBoundingClientRect();
334
- const editRequest = new DxEditRequest({
335
- cellIndex: `${cell.plane},${cell.col},${cell.row}`,
336
- cellBox: {
337
- insetInlineStart: rect.left,
338
- insetBlockStart: rect.top,
339
- inlineSize: rect.width,
340
- blockSize: rect.height,
341
- },
342
- cellElement,
343
- initialContent: clipboardText,
344
- });
345
- cellElement.dispatchEvent(editRequest);
346
- }
347
- }
348
- });
349
- break;
270
+ break;
271
+ }
272
+ case 'switch': {
273
+ if (model) {
274
+ model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
350
275
  }
276
+ break;
351
277
  }
352
278
  }
279
+ }
280
+ },
281
+ [model, modals, dxGrid],
282
+ );
353
283
 
284
+ const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
285
+ (increment, delta, cell) => {
286
+ if (dxGrid && model) {
287
+ if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
288
+ handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
289
+ } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
290
+ handleSaveDraftRow(cell.row);
291
+ handleInsertRowResult(model.insertRow());
292
+ } else {
293
+ dxGrid.refocus(increment, delta);
294
+ }
295
+ }
296
+ },
297
+ [dxGrid, model],
298
+ );
299
+
300
+ const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
301
+ (event) => {
302
+ if (!model?.features.dataEditable) {
303
+ return;
304
+ }
305
+
306
+ const cell = closestCell(event.target);
307
+ if (!model || !cell) {
308
+ return;
309
+ }
310
+
311
+ // Handle Meta+C (Copy) and Meta+V (Paste) commands
312
+ if (event.metaKey || event.ctrlKey) {
354
313
  switch (event.key) {
355
- case 'Backspace':
356
- case 'Delete': {
314
+ case 'c': {
315
+ // Copy focused cell's text content to clipboard
357
316
  try {
358
- model.setCellData(cell, undefined);
317
+ const cellData = model.getCellData(cell);
318
+ const textContent = cellData?.toString() ?? '';
319
+ void navigator.clipboard.writeText(textContent);
359
320
  event.preventDefault();
360
- handleSave();
361
- } catch {
362
- // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
321
+ } catch (error) {
322
+ log.catch(error);
363
323
  }
364
324
  break;
365
325
  }
326
+ case 'v': {
327
+ // Paste clipboard content to focused cell
328
+ event.preventDefault();
329
+ void navigator.clipboard.readText().then((clipboardText) => {
330
+ try {
331
+ // Attempt to set the cell's content to clipboard content
332
+ model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
333
+ handleSave();
334
+ } catch {
335
+ // If validation fails, emit a DxEditRequest event with initialContent from clipboard
336
+ // TODO(thure): Should `dx-grid` expose a method like this?
337
+ const cellElement = (event.target as HTMLElement).closest(
338
+ '[data-dx-grid-action="cell"]',
339
+ ) as HTMLElement;
340
+ if (cellElement) {
341
+ const rect = cellElement.getBoundingClientRect();
342
+ const editRequest = new DxEditRequest({
343
+ cellIndex: `${cell.plane},${cell.col},${cell.row}`,
344
+ cellBox: {
345
+ insetInlineStart: rect.left,
346
+ insetBlockStart: rect.top,
347
+ inlineSize: rect.width,
348
+ blockSize: rect.height,
349
+ },
350
+ cellElement,
351
+ initialContent: clipboardText,
352
+ });
353
+ cellElement.dispatchEvent(editRequest);
354
+ }
355
+ }
356
+ });
357
+ break;
358
+ }
366
359
  }
367
- },
368
- [model, dxGrid, handleSave],
369
- );
370
-
371
- const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
372
- (event) => {
373
- if (event.axis === 'col') {
374
- const columnIndex = parseInt(event.index, 10);
375
- model?.setColumnWidth(columnIndex, event.size);
376
- }
377
- },
378
- [model],
379
- );
360
+ }
380
361
 
381
- const handleWheel = useCallback(
382
- (event: WheelEvent) => {
383
- if (!ignoreAttention && !hasAttention) {
384
- event.stopPropagation();
362
+ switch (event.key) {
363
+ case 'Backspace':
364
+ case 'Delete': {
365
+ try {
366
+ model.setCellData(cell, undefined);
367
+ event.preventDefault();
368
+ handleSave();
369
+ } catch {
370
+ // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
371
+ }
372
+ break;
385
373
  }
386
- },
387
- [hasAttention, ignoreAttention],
388
- );
374
+ }
375
+ },
376
+ [model, dxGrid, handleSave],
377
+ );
389
378
 
390
- const handleNewColumn = useCallback(() => {
391
- const columns = model?.getColumnCount();
392
- if (dxGrid && columns) {
393
- dxGrid.scrollToColumn(columns - 1);
379
+ const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
380
+ (event) => {
381
+ if (event.axis === 'col') {
382
+ const columnIndex = parseInt(event.index, 10);
383
+ model?.setColumnWidth(columnIndex, event.size);
394
384
  }
395
- }, [model, dxGrid]);
385
+ },
386
+ [model],
387
+ );
396
388
 
397
- if (!model || !modals) {
398
- return <span role='none' className='attention-surface' />;
389
+ const handleWheel = useCallback(
390
+ (event: WheelEvent) => {
391
+ if (!ignoreAttention && !hasAttention) {
392
+ event.stopPropagation();
393
+ }
394
+ },
395
+ [hasAttention, ignoreAttention],
396
+ );
397
+
398
+ const handleNewColumn = useCallback(() => {
399
+ const columns = model?.getColumnCount();
400
+ if (dxGrid && columns) {
401
+ dxGrid.scrollToColumn(columns - 1);
399
402
  }
403
+ }, [model, dxGrid]);
404
+
405
+ /**
406
+ * Provides an external controller that can be called to repaint the table.
407
+ */
408
+ useImperativeHandle<TableController, TableController>(forwardedRef, () => {
409
+ if (!dxGrid || !presentation) {
410
+ return {};
411
+ }
412
+
413
+ return {
414
+ update: (cell) => {
415
+ if (cell) {
416
+ dxGrid.updateIfWithinBounds(cell, true);
417
+ } else {
418
+ dxGrid.updateCells(true);
419
+ dxGrid.requestUpdate();
420
+ }
421
+ },
422
+ handleInsertRowResult,
423
+ };
424
+ }, [dxGrid, model, presentation]);
425
+
426
+ if (!model || !modals) {
427
+ return <span role='none' className='attention-surface' />;
428
+ }
429
+
430
+ return (
431
+ <Grid.Root id={model.id ?? 'table-grid'}>
432
+ <TableValueEditor<T>
433
+ model={model}
434
+ modals={modals}
435
+ schema={schema}
436
+ onFocus={handleFocus}
437
+ onSave={handleSave}
438
+ onCreate={onCreate}
439
+ />
440
+ <Grid.Content
441
+ className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
442
+ frozen={frozen}
443
+ columns={columnMeta}
444
+ columnDefault={columnDefault}
445
+ rowDefault={rowDefault}
446
+ limitRows={rows.length}
447
+ limitColumns={columnCount}
448
+ overscroll='trap'
449
+ onAxisResize={handleAxisResize}
450
+ onClick={handleGridClick}
451
+ onKeyDownCapture={handleKeyDown}
452
+ onWheelCapture={handleWheel}
453
+ {...(testId && { 'data-testid': testId })}
454
+ ref={setDxGrid}
455
+ />
456
+ <RowActionsMenu model={model} modals={modals} />
457
+ <ColumnActionsMenu model={model} modals={modals} />
458
+ <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
459
+ </Grid.Root>
460
+ );
461
+ };
400
462
 
401
- return (
402
- <Grid.Root id={model.id ?? 'table-grid'}>
403
- <TableValueEditor
404
- model={model}
405
- modals={modals}
406
- schema={schema}
407
- onFocus={handleFocus}
408
- onSave={handleSave}
409
- client={client}
410
- />
411
- <Grid.Content
412
- className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
413
- frozen={frozen}
414
- columns={model.columnMeta.value}
415
- columnDefault={columnDefault}
416
- rowDefault={rowDefault}
417
- limitRows={model.getRowCount() ?? 0}
418
- limitColumns={model.projection.fields.length}
419
- overscroll='trap'
420
- onAxisResize={handleAxisResize}
421
- onClick={handleGridClick}
422
- onKeyDownCapture={handleKeyDown}
423
- onWheelCapture={handleWheel}
424
- ref={setDxGrid}
425
- />
426
- <RowActionsMenu model={model} modals={modals} />
427
- <ColumnActionsMenu model={model} modals={modals} />
428
- <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
429
- <CreateRefPanel model={model} modals={modals} />
430
- </Grid.Root>
431
- );
432
- },
433
- );
463
+ const TableMain = forwardRef(TableMainInner) as <T extends Type.Entity.Any = Type.Entity.Any>(
464
+ props: TableMainProps<T> & { ref?: Ref<TableController> },
465
+ ) => JSX.Element;
434
466
 
435
467
  export const Table = {
436
468
  Root: TableRoot,