@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.70d3990
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-5XWSB3JP.mjs +71 -0
- package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-KNC7XYT6.mjs} +573 -541
- package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +625 -698
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +16 -24
- 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-Y66YLSTC.mjs → chunk-CWSGS3Y2.mjs} +573 -541
- package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-MKTMOY46.mjs +72 -0
- package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +625 -698
- 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 +16 -24
- 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 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +108 -0
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
- package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +8 -4
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +105 -4
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
- package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +3 -5
- 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 +3 -2
- package/dist/types/src/hooks/useAddRow.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 +5 -6
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +0 -7
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +14 -14
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +2 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/model/table-sorting.d.ts +9 -6
- 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 +21 -34
- 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/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts +1 -1
- package/dist/types/src/util/schema.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +61 -59
- package/src/components/Table/ColumnActionsMenu.tsx +1 -1
- package/src/components/Table/ColumnSettings.tsx +1 -1
- package/src/components/Table/DynamicTable.stories.tsx +21 -17
- package/src/components/Table/DynamicTable.tsx +19 -13
- package/src/components/Table/Relations.stories.tsx +237 -0
- package/src/components/Table/RowActionsMenu.tsx +23 -15
- package/src/components/Table/Table.stories.tsx +103 -86
- package/src/components/Table/Table.tsx +140 -115
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +28 -66
- package/src/components/TableCellEditor/TableCellEditor.tsx +75 -225
- package/src/components/TableCellEditor/index.ts +1 -0
- package/src/components/TableToolbar/TableToolbar.tsx +34 -24
- package/src/components/testing.ts +4 -4
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useAddRow.ts +7 -5
- package/src/hooks/useProjectionModel.ts +19 -0
- package/src/hooks/useTableModel.ts +12 -14
- package/src/model/modal-controller.ts +1 -29
- package/src/model/selection-model.ts +3 -2
- package/src/model/table-model.test.ts +21 -12
- package/src/model/table-model.ts +62 -56
- package/src/model/table-presentation.test.ts +25 -23
- package/src/model/table-presentation.ts +82 -41
- package/src/model/table-sorting.ts +32 -14
- package/src/playwright/TableManager.ts +3 -3
- package/src/playwright/smoke.spec.ts +132 -38
- package/src/testing/useTestTableModel.ts +34 -22
- package/src/translations.ts +2 -2
- package/src/types/Table.ts +89 -0
- package/src/types/index.ts +1 -1
- package/src/util/dynamic-table.ts +20 -24
- package/src/util/index.ts +0 -1
- package/src/util/schema.ts +4 -4
- package/src/util/table-buttons.ts +4 -3
- package/src/util/table-controls.ts +4 -1
- package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
- package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
- package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
- package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
- package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
- package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
- package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
- package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
- package/dist/types/src/types/schema.d.ts +0 -8
- package/dist/types/src/types/schema.d.ts.map +0 -1
- package/dist/types/src/util/table.d.ts +0 -11
- package/dist/types/src/util/table.d.ts.map +0 -1
- package/src/components/Table/CreateRefPanel.tsx +0 -86
- package/src/components/Table/relations.stories.tsx +0 -116
- package/src/components/TableCellEditor/extension.ts +0 -61
- package/src/types/schema.ts +0 -16
- package/src/util/table.ts +0 -49
|
@@ -2,49 +2,47 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import * as String from 'effect/String';
|
|
6
7
|
import React, {
|
|
7
8
|
type MouseEvent,
|
|
8
9
|
type PropsWithChildren,
|
|
9
10
|
type WheelEvent,
|
|
10
11
|
forwardRef,
|
|
11
12
|
useCallback,
|
|
13
|
+
useEffect,
|
|
12
14
|
useImperativeHandle,
|
|
13
|
-
useState,
|
|
14
15
|
useMemo,
|
|
15
|
-
|
|
16
|
+
useState,
|
|
16
17
|
} from 'react';
|
|
17
18
|
|
|
18
|
-
import { type Client } from '@dxos/client';
|
|
19
|
-
import { Filter } from '@dxos/echo';
|
|
20
|
-
import { getValue } from '@dxos/echo-schema';
|
|
21
|
-
import { invariant } from '@dxos/invariant';
|
|
22
19
|
// TODO(wittjosiah): Remove dependency on react-client.
|
|
23
|
-
import {
|
|
24
|
-
import { useTranslation } from '@dxos/react-ui';
|
|
20
|
+
import { type Client } from '@dxos/react-client';
|
|
25
21
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
26
22
|
import {
|
|
27
|
-
closestCell,
|
|
28
23
|
type DxGridElement,
|
|
29
|
-
type DxGridPosition,
|
|
30
|
-
type GridContentProps,
|
|
31
|
-
Grid,
|
|
32
24
|
type DxGridPlane,
|
|
33
25
|
type DxGridPlaneRange,
|
|
34
|
-
|
|
26
|
+
type DxGridPosition,
|
|
27
|
+
Grid,
|
|
28
|
+
type GridContentProps,
|
|
29
|
+
closestCell,
|
|
35
30
|
gridSeparatorBlockEnd,
|
|
31
|
+
gridSeparatorInlineEnd,
|
|
36
32
|
} from '@dxos/react-ui-grid';
|
|
33
|
+
import { DxEditRequest } from '@dxos/react-ui-grid';
|
|
37
34
|
import { mx } from '@dxos/react-ui-theme';
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
|
|
37
|
+
import { tableButtons, tableControls } from '../../util';
|
|
38
|
+
import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
|
|
39
39
|
|
|
40
40
|
import { ColumnActionsMenu } from './ColumnActionsMenu';
|
|
41
41
|
import { ColumnSettings } from './ColumnSettings';
|
|
42
|
-
import { CreateRefPanel } from './CreateRefPanel';
|
|
43
42
|
import { RowActionsMenu } from './RowActionsMenu';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
|
|
43
|
+
|
|
44
|
+
const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
|
|
45
|
+
const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
|
|
48
46
|
|
|
49
47
|
//
|
|
50
48
|
// Table.Root
|
|
@@ -76,27 +74,34 @@ const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
|
|
|
76
74
|
|
|
77
75
|
export type TableController = {
|
|
78
76
|
update?: (cell?: DxGridPosition) => void;
|
|
77
|
+
handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
|
|
79
78
|
};
|
|
80
79
|
|
|
81
80
|
export type TableMainProps = {
|
|
82
81
|
model?: TableModel;
|
|
83
82
|
presentation?: TablePresentation;
|
|
84
|
-
schema?: Schema.AnyNoContext;
|
|
83
|
+
schema?: Schema.Schema.AnyNoContext;
|
|
85
84
|
client?: Client;
|
|
86
85
|
// TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
|
|
87
86
|
ignoreAttention?: boolean;
|
|
87
|
+
onCreate?: OnCreateHandler;
|
|
88
88
|
onRowClick?: (row: any) => void;
|
|
89
|
+
testId?: string;
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
const TableMain = forwardRef<TableController, TableMainProps>(
|
|
92
|
-
({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
|
|
93
|
+
({ model, presentation, ignoreAttention, schema, client, onCreate, onRowClick, testId }, forwardedRef) => {
|
|
93
94
|
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
94
95
|
const { hasAttention } = useAttention(model?.id ?? 'table');
|
|
95
|
-
const { t } = useTranslation(translationKey);
|
|
96
96
|
const modals = useMemo(() => new ModalController(), []);
|
|
97
97
|
|
|
98
98
|
const draftRowCount = model?.getDraftRowCount() ?? 0;
|
|
99
99
|
|
|
100
|
+
const handleSave = useCallback(() => {
|
|
101
|
+
dxGrid?.updateCells(true);
|
|
102
|
+
dxGrid?.requestUpdate();
|
|
103
|
+
}, [dxGrid]);
|
|
104
|
+
|
|
100
105
|
const frozen = useMemo(() => {
|
|
101
106
|
const noActionColumn =
|
|
102
107
|
model?.features.dataEditable === false &&
|
|
@@ -105,7 +110,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
105
110
|
|
|
106
111
|
return {
|
|
107
112
|
frozenRowsStart: 1,
|
|
108
|
-
frozenRowsEnd: draftRowCount,
|
|
113
|
+
frozenRowsEnd: Math.max(1, draftRowCount),
|
|
109
114
|
frozenColsStart: model?.features.selection.enabled ? 1 : 0,
|
|
110
115
|
frozenColsEnd: noActionColumn ? 0 : 1,
|
|
111
116
|
};
|
|
@@ -123,6 +128,23 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
123
128
|
dxGrid.getCells = getCells;
|
|
124
129
|
}, [presentation, dxGrid, getCells]);
|
|
125
130
|
|
|
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 {
|
|
139
|
+
requestAnimationFrame(() => {
|
|
140
|
+
dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
|
|
141
|
+
dxGrid?.refocus();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
[model, dxGrid],
|
|
146
|
+
);
|
|
147
|
+
|
|
126
148
|
/**
|
|
127
149
|
* Provides an external controller that can be called to repaint the table.
|
|
128
150
|
*/
|
|
@@ -140,16 +162,36 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
140
162
|
dxGrid.requestUpdate();
|
|
141
163
|
}
|
|
142
164
|
},
|
|
165
|
+
handleInsertRowResult,
|
|
143
166
|
};
|
|
144
|
-
}, [presentation, dxGrid]);
|
|
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
|
+
);
|
|
145
182
|
|
|
146
183
|
const handleGridClick = useCallback(
|
|
147
184
|
(event: MouseEvent) => {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
185
|
+
const cell = closestCell(event.target as HTMLElement);
|
|
186
|
+
if (cell) {
|
|
187
|
+
const { row: rowIndex, plane } = cell;
|
|
150
188
|
if (onRowClick) {
|
|
151
|
-
|
|
152
|
-
|
|
189
|
+
if (plane === 'grid') {
|
|
190
|
+
const row = model?.getRowAt(rowIndex);
|
|
191
|
+
row && onRowClick(row);
|
|
192
|
+
} else {
|
|
193
|
+
onRowClick(cell);
|
|
194
|
+
}
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
|
|
@@ -192,14 +234,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
192
234
|
break;
|
|
193
235
|
}
|
|
194
236
|
case 'saveDraftRow': {
|
|
195
|
-
|
|
196
|
-
const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
|
|
197
|
-
if (dxGrid && didCommitSuccessfully) {
|
|
198
|
-
requestAnimationFrame(() => {
|
|
199
|
-
dxGrid.scrollToEndRow();
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
237
|
+
handleSaveDraftRow();
|
|
203
238
|
break;
|
|
204
239
|
}
|
|
205
240
|
}
|
|
@@ -240,32 +275,19 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
240
275
|
);
|
|
241
276
|
|
|
242
277
|
const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
|
|
243
|
-
(increment, delta) => {
|
|
244
|
-
if (dxGrid) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
(cell) => {
|
|
253
|
-
if (!model?.features.dataEditable) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// TODO(burdon): Insert row only if bottom row isn't completely blank already.
|
|
258
|
-
if (model && cell.row === model.getRowCount() - 1) {
|
|
259
|
-
model.insertRow();
|
|
260
|
-
if (dxGrid) {
|
|
261
|
-
requestAnimationFrame(() => {
|
|
262
|
-
dxGrid?.scrollToRow(cell.row + 1);
|
|
263
|
-
dxGrid?.refocus('row', 1);
|
|
264
|
-
});
|
|
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);
|
|
265
287
|
}
|
|
266
288
|
}
|
|
267
289
|
},
|
|
268
|
-
[
|
|
290
|
+
[dxGrid, model],
|
|
269
291
|
);
|
|
270
292
|
|
|
271
293
|
const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
|
|
@@ -279,12 +301,64 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
279
301
|
return;
|
|
280
302
|
}
|
|
281
303
|
|
|
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;
|
|
318
|
+
}
|
|
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;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
282
355
|
switch (event.key) {
|
|
283
356
|
case 'Backspace':
|
|
284
357
|
case 'Delete': {
|
|
285
358
|
try {
|
|
286
359
|
model.setCellData(cell, undefined);
|
|
287
360
|
event.preventDefault();
|
|
361
|
+
handleSave();
|
|
288
362
|
} catch {
|
|
289
363
|
// Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
|
|
290
364
|
}
|
|
@@ -292,7 +366,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
292
366
|
}
|
|
293
367
|
}
|
|
294
368
|
},
|
|
295
|
-
[model],
|
|
369
|
+
[model, dxGrid, handleSave],
|
|
296
370
|
);
|
|
297
371
|
|
|
298
372
|
const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
|
|
@@ -321,57 +395,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
321
395
|
}
|
|
322
396
|
}, [model, dxGrid]);
|
|
323
397
|
|
|
324
|
-
// TODO(burdon): Factor out?
|
|
325
|
-
// TODO(burdon): Generalize to handle other value types (e.g., enums).
|
|
326
|
-
const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
|
|
327
|
-
async ({ field, props }, text) => {
|
|
328
|
-
if (model && props.referenceSchema && field.referencePath) {
|
|
329
|
-
const space = getSpace(model.view);
|
|
330
|
-
invariant(space);
|
|
331
|
-
|
|
332
|
-
let schema;
|
|
333
|
-
if (client) {
|
|
334
|
-
schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
|
|
335
|
-
}
|
|
336
|
-
if (!schema) {
|
|
337
|
-
schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (schema) {
|
|
341
|
-
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
342
|
-
const options = objects
|
|
343
|
-
.map((obj) => {
|
|
344
|
-
const value = getValue(obj, field.referencePath!);
|
|
345
|
-
if (!value || typeof value !== 'string') {
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return {
|
|
350
|
-
label: value,
|
|
351
|
-
data: obj,
|
|
352
|
-
};
|
|
353
|
-
})
|
|
354
|
-
.filter(isNotFalsy);
|
|
355
|
-
|
|
356
|
-
return [
|
|
357
|
-
...options,
|
|
358
|
-
{
|
|
359
|
-
label: t('create new object label', { text }),
|
|
360
|
-
data: createOption(text),
|
|
361
|
-
},
|
|
362
|
-
];
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return [];
|
|
367
|
-
},
|
|
368
|
-
[model, client, t],
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
const handleSave = useCallback(() => {
|
|
372
|
-
dxGrid?.updateCells(true);
|
|
373
|
-
}, [dxGrid]);
|
|
374
|
-
|
|
375
398
|
if (!model || !modals) {
|
|
376
399
|
return <span role='none' className='attention-surface' />;
|
|
377
400
|
}
|
|
@@ -382,15 +405,17 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
382
405
|
model={model}
|
|
383
406
|
modals={modals}
|
|
384
407
|
schema={schema}
|
|
385
|
-
onEnter={handleEnter}
|
|
386
408
|
onFocus={handleFocus}
|
|
387
|
-
onQuery={handleQuery}
|
|
388
409
|
onSave={handleSave}
|
|
410
|
+
onCreate={onCreate}
|
|
411
|
+
client={client}
|
|
389
412
|
/>
|
|
390
413
|
<Grid.Content
|
|
391
414
|
className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
392
415
|
frozen={frozen}
|
|
393
416
|
columns={model.columnMeta.value}
|
|
417
|
+
columnDefault={columnDefault}
|
|
418
|
+
rowDefault={rowDefault}
|
|
394
419
|
limitRows={model.getRowCount() ?? 0}
|
|
395
420
|
limitColumns={model.projection.fields.length}
|
|
396
421
|
overscroll='trap'
|
|
@@ -398,12 +423,12 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
398
423
|
onClick={handleGridClick}
|
|
399
424
|
onKeyDownCapture={handleKeyDown}
|
|
400
425
|
onWheelCapture={handleWheel}
|
|
426
|
+
{...(testId && { 'data-testid': testId })}
|
|
401
427
|
ref={setDxGrid}
|
|
402
428
|
/>
|
|
403
429
|
<RowActionsMenu model={model} modals={modals} />
|
|
404
430
|
<ColumnActionsMenu model={model} modals={modals} />
|
|
405
431
|
<ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
|
|
406
|
-
<CreateRefPanel model={model} modals={modals} />
|
|
407
432
|
</Grid.Root>
|
|
408
433
|
);
|
|
409
434
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { type GridScopedProps, useGridContext } from '@dxos/react-ui-grid';
|
|
8
8
|
|
|
9
9
|
export type CellValidationMessageProps = {
|
|
10
10
|
validationError: string | null;
|
|
@@ -2,33 +2,124 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import * as Schema from 'effect/Schema';
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Filter, Obj } from '@dxos/echo';
|
|
9
|
+
import { Format, Ref, type TypeAnnotation, getValue } from '@dxos/echo/internal';
|
|
9
10
|
import { invariant } from '@dxos/invariant';
|
|
11
|
+
import { getSnapshot } from '@dxos/live-object';
|
|
12
|
+
import { type Client } from '@dxos/react-client';
|
|
13
|
+
import { getSpace } from '@dxos/react-client/echo';
|
|
10
14
|
import { Popover } from '@dxos/react-ui';
|
|
11
|
-
import { Form } from '@dxos/react-ui-form';
|
|
15
|
+
import { Form, type FormProps } from '@dxos/react-ui-form';
|
|
12
16
|
import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
|
|
13
17
|
import { type FieldProjection } from '@dxos/schema';
|
|
14
|
-
import { getDeep, setDeep } from '@dxos/util';
|
|
18
|
+
import { getDeep, isTruthy, setDeep } from '@dxos/util';
|
|
15
19
|
|
|
16
|
-
import { type TableModel } from '../../model';
|
|
20
|
+
import { type ModalController, type TableModel } from '../../model';
|
|
21
|
+
import { translationKey } from '../../translations';
|
|
17
22
|
import { narrowSchema } from '../../util';
|
|
18
23
|
|
|
19
|
-
type
|
|
24
|
+
export type OnCreateHandler = (schema: Schema.Schema.AnyNoContext, values: any) => Parameters<typeof Ref.make>[0];
|
|
25
|
+
|
|
26
|
+
export type FormCellEditorProps = {
|
|
20
27
|
fieldProjection: FieldProjection;
|
|
21
28
|
model?: TableModel;
|
|
22
29
|
schema?: Schema.Schema.AnyNoContext;
|
|
23
30
|
onSave?: () => void;
|
|
31
|
+
onCreate?: OnCreateHandler;
|
|
32
|
+
client?: Client;
|
|
33
|
+
modals?: ModalController;
|
|
24
34
|
__gridScope: any;
|
|
25
|
-
}
|
|
35
|
+
} & Omit<FormProps<any>, 'values' | 'schema' | 'onCreate'>;
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
|
|
38
|
+
|
|
39
|
+
export const FormCellEditor = ({
|
|
40
|
+
fieldProjection,
|
|
41
|
+
model,
|
|
42
|
+
schema,
|
|
43
|
+
onSave,
|
|
44
|
+
client,
|
|
45
|
+
modals,
|
|
46
|
+
onCreate,
|
|
47
|
+
__gridScope,
|
|
48
|
+
...formProps
|
|
49
|
+
}: FormCellEditorProps) => {
|
|
50
|
+
const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
|
|
29
51
|
const [editing, setLocalEditing] = useState(false);
|
|
30
52
|
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
|
31
53
|
|
|
54
|
+
const getSchema = useCallback(
|
|
55
|
+
(typeAnnotation: Pick<TypeAnnotation, 'typename'>) => {
|
|
56
|
+
const space = getSpace(model!.view);
|
|
57
|
+
invariant(space);
|
|
58
|
+
|
|
59
|
+
let schema;
|
|
60
|
+
if (client) {
|
|
61
|
+
schema = client.graph.schemaRegistry.getSchema(typeAnnotation.typename);
|
|
62
|
+
}
|
|
63
|
+
if (!schema) {
|
|
64
|
+
schema = space.db.schemaRegistry.getSchema(typeAnnotation.typename);
|
|
65
|
+
}
|
|
66
|
+
return { space, schema };
|
|
67
|
+
},
|
|
68
|
+
[client, model],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleQueryRefOptions = useCallback(
|
|
72
|
+
async (typeAnnotation: TypeAnnotation) => {
|
|
73
|
+
const { schema, space } = getSchema(typeAnnotation);
|
|
74
|
+
if (model && schema && space) {
|
|
75
|
+
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
76
|
+
return objects
|
|
77
|
+
.map((obj) => {
|
|
78
|
+
return {
|
|
79
|
+
dxn: Obj.getDXN(obj),
|
|
80
|
+
label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
|
|
81
|
+
};
|
|
82
|
+
})
|
|
83
|
+
.filter(isTruthy);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [];
|
|
87
|
+
},
|
|
88
|
+
[client, model],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const originalRow = useMemo(() => {
|
|
92
|
+
if (model && contextEditing) {
|
|
93
|
+
const cell = parseCellIndex(contextEditing.index);
|
|
94
|
+
|
|
95
|
+
// Check if this is a draft cell and get the appropriate row data
|
|
96
|
+
if (model.isDraftCell(cell)) {
|
|
97
|
+
const draftRow = model.draftRows.value[cell.row];
|
|
98
|
+
invariant(draftRow);
|
|
99
|
+
return draftRow.data;
|
|
100
|
+
} else {
|
|
101
|
+
const row = model.getRowAt(cell.row);
|
|
102
|
+
invariant(row);
|
|
103
|
+
return row;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return undefined;
|
|
108
|
+
}, [model, contextEditing]);
|
|
109
|
+
|
|
110
|
+
const handleSave = useCallback(
|
|
111
|
+
(values: any) => {
|
|
112
|
+
const path = fieldProjection.field.path;
|
|
113
|
+
const value = getDeep(values, [path]);
|
|
114
|
+
setDeep(originalRow, [path], value);
|
|
115
|
+
contextEditing?.cellElement?.focus();
|
|
116
|
+
setEditing(null);
|
|
117
|
+
setLocalEditing(false);
|
|
118
|
+
onSave?.();
|
|
119
|
+
},
|
|
120
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow],
|
|
121
|
+
);
|
|
122
|
+
|
|
32
123
|
useEffect(() => {
|
|
33
124
|
if (contextEditing && contextEditing.cellElement) {
|
|
34
125
|
anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
|
|
@@ -48,45 +139,49 @@ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridS
|
|
|
48
139
|
return narrowSchema(schema, [fieldProjection.field.path]);
|
|
49
140
|
}, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
|
|
50
141
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const cell = parseCellIndex(contextEditing.index);
|
|
54
|
-
const row = model.getRowAt(cell.row);
|
|
55
|
-
invariant(row);
|
|
142
|
+
// NOTE: Important to get a snapshot to eject from the live object.
|
|
143
|
+
const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
|
|
56
144
|
|
|
57
|
-
|
|
145
|
+
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
|
146
|
+
if (nextOpen === false) {
|
|
147
|
+
setEditing(null);
|
|
148
|
+
onSave?.();
|
|
58
149
|
}
|
|
150
|
+
setLocalEditing(nextOpen);
|
|
151
|
+
}, []);
|
|
59
152
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return getSnapshot(originalRow);
|
|
153
|
+
const refSchema = useMemo(() => {
|
|
154
|
+
if (fieldProjection.props.format === Format.TypeFormat.Ref && fieldProjection.props.referenceSchema) {
|
|
155
|
+
const { schema: refSchema } = getSchema({
|
|
156
|
+
typename: fieldProjection.props.referenceSchema,
|
|
157
|
+
});
|
|
158
|
+
return refSchema;
|
|
67
159
|
}
|
|
68
|
-
|
|
160
|
+
return null;
|
|
161
|
+
}, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
|
|
69
162
|
|
|
70
|
-
const
|
|
163
|
+
const createSchema = useMemo(() => {
|
|
164
|
+
return refSchema ? Schema.omit<any, any, ['id']>('id')(refSchema) : null;
|
|
165
|
+
}, [refSchema]);
|
|
166
|
+
|
|
167
|
+
const handleCreate = useCallback(
|
|
71
168
|
(values: any) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
169
|
+
if (refSchema && onCreate) {
|
|
170
|
+
const objectWithId = onCreate(refSchema, values);
|
|
171
|
+
if (objectWithId) {
|
|
172
|
+
const ref = Ref.make(objectWithId);
|
|
173
|
+
const path = fieldProjection.field.path;
|
|
174
|
+
setDeep(originalRow, [path], ref);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
contextEditing?.cellElement?.focus();
|
|
75
178
|
setEditing(null);
|
|
76
179
|
setLocalEditing(false);
|
|
77
180
|
onSave?.();
|
|
78
181
|
},
|
|
79
|
-
[fieldProjection.field.path, originalRow],
|
|
182
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow, refSchema, onCreate],
|
|
80
183
|
);
|
|
81
184
|
|
|
82
|
-
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
|
83
|
-
if (nextOpen === false) {
|
|
84
|
-
setEditing(null);
|
|
85
|
-
onSave?.();
|
|
86
|
-
}
|
|
87
|
-
setLocalEditing(nextOpen);
|
|
88
|
-
}, []);
|
|
89
|
-
|
|
90
185
|
if (!editing) {
|
|
91
186
|
return null;
|
|
92
187
|
}
|
|
@@ -94,12 +189,28 @@ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridS
|
|
|
94
189
|
return (
|
|
95
190
|
<Popover.Root open={editing} onOpenChange={handleOpenChange}>
|
|
96
191
|
<Popover.VirtualTrigger virtualRef={anchorRef} />
|
|
97
|
-
<Popover.
|
|
98
|
-
<Popover.
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
192
|
+
<Popover.Portal>
|
|
193
|
+
<Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
|
|
194
|
+
<Popover.Arrow />
|
|
195
|
+
<Popover.Viewport>
|
|
196
|
+
<Form
|
|
197
|
+
autoFocus
|
|
198
|
+
values={formValues}
|
|
199
|
+
schema={narrowedSchema as any}
|
|
200
|
+
onSave={handleSave}
|
|
201
|
+
{...formProps}
|
|
202
|
+
onQueryRefOptions={handleQueryRefOptions}
|
|
203
|
+
{...(createSchema && {
|
|
204
|
+
onCreate: handleCreate,
|
|
205
|
+
createSchema,
|
|
206
|
+
createInitialValuePath: fieldProjection.field.referencePath,
|
|
207
|
+
createOptionIcon: 'ph--plus--regular',
|
|
208
|
+
createOptionLabel,
|
|
209
|
+
})}
|
|
210
|
+
/>
|
|
211
|
+
</Popover.Viewport>
|
|
212
|
+
</Popover.Content>
|
|
213
|
+
</Popover.Portal>
|
|
103
214
|
</Popover.Root>
|
|
104
215
|
);
|
|
105
216
|
};
|