@dxos/react-ui-table 0.8.4-main.ae835ea → 0.8.4-main.bc674ce
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.
- package/dist/lib/browser/{chunk-G7MGPBSR.mjs → chunk-I5IFW7UF.mjs} +633 -663
- package/dist/lib/browser/chunk-I5IFW7UF.mjs.map +7 -0
- package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
- package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1131 -999
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +29 -41
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +3 -3
- package/dist/lib/node-esm/{chunk-LD5JEBA6.mjs → chunk-DDJKYJFY.mjs} +633 -662
- package/dist/lib/node-esm/chunk-DDJKYJFY.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
- package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1131 -999
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +29 -41
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +3 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +4 -5
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +4 -18
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +12 -9
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +8 -8
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +71 -5
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +4 -4
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +22 -15
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +5 -6
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useTableModel.d.ts +8 -8
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +5 -1
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +17 -8
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +102 -23
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +3 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts +1 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +4 -4
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +31 -0
- package/dist/types/src/types/Table.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +13 -11
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts +7 -0
- package/dist/types/src/util/query-ast.d.ts.map +1 -0
- package/dist/types/src/util/sort.d.ts +12 -0
- package/dist/types/src/util/sort.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +53 -53
- package/src/components/Table/ColumnActionsMenu.tsx +10 -6
- package/src/components/Table/ColumnSettings.tsx +6 -6
- package/src/components/Table/DynamicTable.stories.tsx +12 -13
- package/src/components/Table/DynamicTable.tsx +21 -14
- package/src/components/Table/Relations.stories.tsx +35 -39
- package/src/components/Table/RowActionsMenu.tsx +24 -15
- package/src/components/Table/Table.stories.tsx +71 -75
- package/src/components/Table/Table.tsx +335 -305
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +110 -133
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +16 -12
- package/src/components/TableCellEditor/TableCellEditor.tsx +31 -31
- package/src/components/TableToolbar/TableToolbar.tsx +44 -31
- package/src/components/testing.ts +38 -12
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useAddRow.ts +8 -10
- package/src/hooks/useProjectionModel.ts +39 -0
- package/src/hooks/useTableModel.ts +35 -33
- package/src/model/modal-controller.ts +23 -12
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +82 -54
- package/src/model/table-model.ts +428 -163
- package/src/model/table-presentation.test.ts +59 -49
- package/src/model/table-presentation.ts +31 -26
- package/src/playwright/TableManager.ts +5 -5
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +8 -5
- package/src/testing/useTestTableModel.ts +44 -37
- package/src/translations.ts +3 -3
- package/src/types/Table.ts +90 -0
- package/src/types/index.ts +1 -1
- package/src/util/dynamic-table.ts +39 -27
- package/src/util/index.ts +2 -0
- package/src/util/query-ast.ts +19 -0
- package/src/util/sort.ts +90 -0
- package/dist/lib/browser/chunk-4K7JPALT.mjs +0 -66
- package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
- package/dist/lib/browser/chunk-G7MGPBSR.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LD5JEBA6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
- package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
- package/dist/types/src/model/table-sorting.d.ts +0 -52
- package/dist/types/src/model/table-sorting.d.ts.map +0 -1
- package/dist/types/src/types/schema.d.ts +0 -28
- package/dist/types/src/types/schema.d.ts.map +0 -1
- package/src/model/table-sorting.ts +0 -225
- package/src/types/schema.ts +0 -73
|
@@ -2,24 +2,28 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
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
|
-
|
|
20
|
-
import {
|
|
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,7 +35,7 @@ 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/
|
|
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';
|
|
@@ -43,6 +47,7 @@ import { RowActionsMenu } from './RowActionsMenu';
|
|
|
43
47
|
|
|
44
48
|
const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
|
|
45
49
|
const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
|
|
50
|
+
const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
|
|
46
51
|
|
|
47
52
|
//
|
|
48
53
|
// Table.Root
|
|
@@ -77,11 +82,10 @@ export type TableController = {
|
|
|
77
82
|
handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
|
|
78
83
|
};
|
|
79
84
|
|
|
80
|
-
export type TableMainProps = {
|
|
85
|
+
export type TableMainProps<T extends Type.Entity.Any = Type.Entity.Any> = {
|
|
86
|
+
schema?: T;
|
|
81
87
|
model?: TableModel;
|
|
82
88
|
presentation?: TablePresentation;
|
|
83
|
-
schema?: Schema.Schema.AnyNoContext;
|
|
84
|
-
client?: Client;
|
|
85
89
|
// TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
|
|
86
90
|
ignoreAttention?: boolean;
|
|
87
91
|
onCreate?: OnCreateHandler;
|
|
@@ -89,350 +93,376 @@ export type TableMainProps = {
|
|
|
89
93
|
testId?: string;
|
|
90
94
|
};
|
|
91
95
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
});
|
|
127
166
|
}
|
|
128
|
-
|
|
129
|
-
|
|
167
|
+
},
|
|
168
|
+
[model, dxGrid],
|
|
169
|
+
);
|
|
130
170
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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) {
|
|
139
181
|
requestAnimationFrame(() => {
|
|
140
|
-
dxGrid
|
|
141
|
-
dxGrid?.refocus();
|
|
182
|
+
dxGrid.scrollToEndRow();
|
|
142
183
|
});
|
|
143
184
|
}
|
|
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
185
|
}
|
|
186
|
+
},
|
|
187
|
+
[model, dxGrid],
|
|
188
|
+
);
|
|
155
189
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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);
|
|
160
199
|
} else {
|
|
161
|
-
|
|
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
|
-
});
|
|
200
|
+
onRowClick(cell);
|
|
177
201
|
}
|
|
178
202
|
}
|
|
179
|
-
},
|
|
180
|
-
[model, dxGrid],
|
|
181
|
-
);
|
|
182
203
|
|
|
183
|
-
|
|
184
|
-
|
|
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);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (!modals) {
|
|
203
|
-
return;
|
|
204
|
+
if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
|
|
205
|
+
model.selection.toggleSelectionForRowIndex(rowIndex);
|
|
204
206
|
}
|
|
207
|
+
}
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
if (!modals) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (!button) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
213
|
+
const buttonSelector = Object.values(tableButtons)
|
|
214
|
+
.map((button) => `button[${button.attr}]`)
|
|
215
|
+
.join(',');
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
}
|
|
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) {
|
|
241
221
|
return;
|
|
242
222
|
}
|
|
243
223
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
|
|
251
|
-
if (!control) {
|
|
252
|
-
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;
|
|
253
230
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
}
|
|
231
|
+
case 'columnSettings': {
|
|
232
|
+
modals.showColumnMenu(data.fieldId);
|
|
233
|
+
break;
|
|
271
234
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
handleSaveDraftRow(cell.row);
|
|
284
|
-
handleInsertRowResult(model.insertRow());
|
|
285
|
-
} else {
|
|
286
|
-
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;
|
|
287
246
|
}
|
|
288
247
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
292
250
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
251
|
+
const controlSelector = Object.values(tableControls)
|
|
252
|
+
.map((control) => `input[${control.attr}]`)
|
|
253
|
+
.join(',');
|
|
298
254
|
|
|
299
|
-
|
|
300
|
-
|
|
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) {
|
|
301
259
|
return;
|
|
302
260
|
}
|
|
303
261
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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;
|
|
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);
|
|
318
269
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'switch': {
|
|
273
|
+
if (model) {
|
|
274
|
+
model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
|
|
351
275
|
}
|
|
276
|
+
break;
|
|
352
277
|
}
|
|
353
278
|
}
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
[model, modals, dxGrid],
|
|
282
|
+
);
|
|
354
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) {
|
|
355
313
|
switch (event.key) {
|
|
356
|
-
case '
|
|
357
|
-
|
|
314
|
+
case 'c': {
|
|
315
|
+
// Copy focused cell's text content to clipboard
|
|
358
316
|
try {
|
|
359
|
-
model.
|
|
317
|
+
const cellData = model.getCellData(cell);
|
|
318
|
+
const textContent = cellData?.toString() ?? '';
|
|
319
|
+
void navigator.clipboard.writeText(textContent);
|
|
360
320
|
event.preventDefault();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// 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);
|
|
364
323
|
}
|
|
365
324
|
break;
|
|
366
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
|
+
}
|
|
367
359
|
}
|
|
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
|
-
);
|
|
360
|
+
}
|
|
381
361
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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;
|
|
386
373
|
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
[model, dxGrid, handleSave],
|
|
377
|
+
);
|
|
390
378
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
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);
|
|
395
384
|
}
|
|
396
|
-
},
|
|
385
|
+
},
|
|
386
|
+
[model],
|
|
387
|
+
);
|
|
397
388
|
|
|
398
|
-
|
|
399
|
-
|
|
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);
|
|
400
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
|
+
};
|
|
401
462
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
model={model}
|
|
406
|
-
modals={modals}
|
|
407
|
-
schema={schema}
|
|
408
|
-
onFocus={handleFocus}
|
|
409
|
-
onSave={handleSave}
|
|
410
|
-
onCreate={onCreate}
|
|
411
|
-
client={client}
|
|
412
|
-
/>
|
|
413
|
-
<Grid.Content
|
|
414
|
-
className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
415
|
-
frozen={frozen}
|
|
416
|
-
columns={model.columnMeta.value}
|
|
417
|
-
columnDefault={columnDefault}
|
|
418
|
-
rowDefault={rowDefault}
|
|
419
|
-
limitRows={model.getRowCount() ?? 0}
|
|
420
|
-
limitColumns={model.projection.fields.length}
|
|
421
|
-
overscroll='trap'
|
|
422
|
-
onAxisResize={handleAxisResize}
|
|
423
|
-
onClick={handleGridClick}
|
|
424
|
-
onKeyDownCapture={handleKeyDown}
|
|
425
|
-
onWheelCapture={handleWheel}
|
|
426
|
-
{...(testId && { 'data-testid': testId })}
|
|
427
|
-
ref={setDxGrid}
|
|
428
|
-
/>
|
|
429
|
-
<RowActionsMenu model={model} modals={modals} />
|
|
430
|
-
<ColumnActionsMenu model={model} modals={modals} />
|
|
431
|
-
<ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
|
|
432
|
-
</Grid.Root>
|
|
433
|
-
);
|
|
434
|
-
},
|
|
435
|
-
);
|
|
463
|
+
const TableMain = forwardRef(TableMainInner) as <T extends Type.Entity.Any = Type.Entity.Any>(
|
|
464
|
+
props: TableMainProps<T> & { ref?: Ref<TableController> },
|
|
465
|
+
) => JSX.Element;
|
|
436
466
|
|
|
437
467
|
export const Table = {
|
|
438
468
|
Root: TableRoot,
|