@dxos/react-ui-table 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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