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