@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.e098934
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-63UUF4IG.mjs +66 -0
- package/dist/lib/browser/chunk-63UUF4IG.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-BBXDTWCX.mjs} +537 -466
- package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +730 -669
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +9 -6
- 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-K734DJB7.mjs +67 -0
- package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-RNQDU7IQ.mjs} +537 -466
- package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +730 -669
- 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 +9 -6
- 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/CreateRefPanel.d.ts +1 -1
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +50 -2
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +44 -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 +3 -2
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +42 -3
- 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 +7 -3
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -15
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -3
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +2 -1
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- 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 +9 -7
- 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 +2 -3
- package/dist/types/src/model/table-sorting.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +2 -21
- 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/index.d.ts +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/types/schema.d.ts +24 -4
- package/dist/types/src/types/schema.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +2 -2
- 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/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 +47 -42
- package/src/components/Table/ColumnActionsMenu.tsx +1 -1
- package/src/components/Table/ColumnSettings.tsx +1 -1
- package/src/components/Table/CreateRefPanel.tsx +25 -8
- package/src/components/Table/DynamicTable.stories.tsx +7 -4
- package/src/components/Table/DynamicTable.tsx +4 -3
- package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +40 -23
- package/src/components/Table/RowActionsMenu.tsx +3 -3
- package/src/components/Table/Table.stories.tsx +39 -28
- package/src/components/Table/Table.tsx +138 -112
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +143 -42
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +12 -24
- package/src/components/TableCellEditor/TableCellEditor.tsx +74 -137
- package/src/components/TableToolbar/TableToolbar.tsx +2 -2
- package/src/components/testing.ts +1 -1
- package/src/hooks/useAddRow.ts +6 -4
- package/src/hooks/useTableModel.ts +1 -1
- package/src/model/modal-controller.ts +1 -1
- package/src/model/selection-model.ts +3 -2
- package/src/model/table-model.test.ts +9 -5
- package/src/model/table-model.ts +35 -24
- package/src/model/table-presentation.test.ts +10 -5
- package/src/model/table-presentation.ts +72 -30
- package/src/model/table-sorting.ts +16 -7
- package/src/playwright/smoke.spec.ts +129 -37
- package/src/testing/useTestTableModel.ts +7 -5
- package/src/translations.ts +2 -2
- package/src/types/index.ts +1 -1
- package/src/types/schema.ts +62 -5
- package/src/util/dynamic-table.ts +10 -9
- package/src/util/index.ts +0 -1
- package/src/util/schema.ts +1 -1
- 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/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/util/table.d.ts +0 -11
- package/dist/types/src/util/table.d.ts.map +0 -1
- package/src/components/TableCellEditor/extension.ts +0 -61
- package/src/util/table.ts +0 -49
|
@@ -9,42 +9,40 @@ import React, {
|
|
|
9
9
|
type WheelEvent,
|
|
10
10
|
forwardRef,
|
|
11
11
|
useCallback,
|
|
12
|
+
useEffect,
|
|
12
13
|
useImperativeHandle,
|
|
13
|
-
useState,
|
|
14
14
|
useMemo,
|
|
15
|
-
|
|
15
|
+
useState,
|
|
16
16
|
} from 'react';
|
|
17
17
|
|
|
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';
|
|
18
|
+
import { type Client } from '@dxos/react-client';
|
|
22
19
|
// TODO(wittjosiah): Remove dependency on react-client.
|
|
23
|
-
import { getSpace } from '@dxos/react-client/echo';
|
|
24
|
-
import { useTranslation } from '@dxos/react-ui';
|
|
25
20
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
26
21
|
import {
|
|
27
|
-
closestCell,
|
|
28
22
|
type DxGridElement,
|
|
29
|
-
type DxGridPosition,
|
|
30
|
-
type GridContentProps,
|
|
31
|
-
Grid,
|
|
32
23
|
type DxGridPlane,
|
|
33
24
|
type DxGridPlaneRange,
|
|
34
|
-
|
|
25
|
+
type DxGridPosition,
|
|
26
|
+
Grid,
|
|
27
|
+
type GridContentProps,
|
|
28
|
+
closestCell,
|
|
35
29
|
gridSeparatorBlockEnd,
|
|
30
|
+
gridSeparatorInlineEnd,
|
|
36
31
|
} from '@dxos/react-ui-grid';
|
|
32
|
+
import { DxEditRequest } from '@dxos/react-ui-grid';
|
|
37
33
|
import { mx } from '@dxos/react-ui-theme';
|
|
38
|
-
|
|
34
|
+
|
|
35
|
+
import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
|
|
36
|
+
import { tableButtons, tableControls } from '../../util';
|
|
37
|
+
import { type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
|
|
39
38
|
|
|
40
39
|
import { ColumnActionsMenu } from './ColumnActionsMenu';
|
|
41
40
|
import { ColumnSettings } from './ColumnSettings';
|
|
42
41
|
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,6 +74,7 @@ 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 = {
|
|
@@ -92,11 +91,15 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
92
91
|
({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
|
|
93
92
|
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
94
93
|
const { hasAttention } = useAttention(model?.id ?? 'table');
|
|
95
|
-
const { t } = useTranslation(translationKey);
|
|
96
94
|
const modals = useMemo(() => new ModalController(), []);
|
|
97
95
|
|
|
98
96
|
const draftRowCount = model?.getDraftRowCount() ?? 0;
|
|
99
97
|
|
|
98
|
+
const handleSave = useCallback(() => {
|
|
99
|
+
dxGrid?.updateCells(true);
|
|
100
|
+
dxGrid?.requestUpdate();
|
|
101
|
+
}, [dxGrid]);
|
|
102
|
+
|
|
100
103
|
const frozen = useMemo(() => {
|
|
101
104
|
const noActionColumn =
|
|
102
105
|
model?.features.dataEditable === false &&
|
|
@@ -105,7 +108,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
105
108
|
|
|
106
109
|
return {
|
|
107
110
|
frozenRowsStart: 1,
|
|
108
|
-
frozenRowsEnd: draftRowCount,
|
|
111
|
+
frozenRowsEnd: Math.max(1, draftRowCount),
|
|
109
112
|
frozenColsStart: model?.features.selection.enabled ? 1 : 0,
|
|
110
113
|
frozenColsEnd: noActionColumn ? 0 : 1,
|
|
111
114
|
};
|
|
@@ -123,6 +126,23 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
123
126
|
dxGrid.getCells = getCells;
|
|
124
127
|
}, [presentation, dxGrid, getCells]);
|
|
125
128
|
|
|
129
|
+
const handleInsertRowResult = useCallback(
|
|
130
|
+
(insertResult?: InsertRowResult) => {
|
|
131
|
+
if (insertResult === 'draft') {
|
|
132
|
+
requestAnimationFrame(() => {
|
|
133
|
+
dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
|
|
134
|
+
dxGrid?.refocus();
|
|
135
|
+
});
|
|
136
|
+
} else {
|
|
137
|
+
requestAnimationFrame(() => {
|
|
138
|
+
dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
|
|
139
|
+
dxGrid?.refocus();
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
[model, dxGrid],
|
|
144
|
+
);
|
|
145
|
+
|
|
126
146
|
/**
|
|
127
147
|
* Provides an external controller that can be called to repaint the table.
|
|
128
148
|
*/
|
|
@@ -140,16 +160,36 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
140
160
|
dxGrid.requestUpdate();
|
|
141
161
|
}
|
|
142
162
|
},
|
|
163
|
+
handleInsertRowResult,
|
|
143
164
|
};
|
|
144
|
-
}, [presentation, dxGrid]);
|
|
165
|
+
}, [presentation, dxGrid, model]);
|
|
166
|
+
|
|
167
|
+
const handleSaveDraftRow = useCallback(
|
|
168
|
+
(rowIndex = 0) => {
|
|
169
|
+
if (model && dxGrid) {
|
|
170
|
+
const didCommitSuccessfully = model.commitDraftRow(rowIndex);
|
|
171
|
+
if (didCommitSuccessfully) {
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
dxGrid.scrollToEndRow();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
[model, dxGrid],
|
|
179
|
+
);
|
|
145
180
|
|
|
146
181
|
const handleGridClick = useCallback(
|
|
147
182
|
(event: MouseEvent) => {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
183
|
+
const cell = closestCell(event.target as HTMLElement);
|
|
184
|
+
if (cell) {
|
|
185
|
+
const { row: rowIndex, plane } = cell;
|
|
150
186
|
if (onRowClick) {
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
if (plane === 'grid') {
|
|
188
|
+
const row = model?.getRowAt(rowIndex);
|
|
189
|
+
row && onRowClick(row);
|
|
190
|
+
} else {
|
|
191
|
+
onRowClick(cell);
|
|
192
|
+
}
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
|
|
@@ -192,14 +232,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
192
232
|
break;
|
|
193
233
|
}
|
|
194
234
|
case 'saveDraftRow': {
|
|
195
|
-
|
|
196
|
-
const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
|
|
197
|
-
if (dxGrid && didCommitSuccessfully) {
|
|
198
|
-
requestAnimationFrame(() => {
|
|
199
|
-
dxGrid.scrollToEndRow();
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
235
|
+
handleSaveDraftRow();
|
|
203
236
|
break;
|
|
204
237
|
}
|
|
205
238
|
}
|
|
@@ -240,32 +273,19 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
240
273
|
);
|
|
241
274
|
|
|
242
275
|
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
|
-
});
|
|
276
|
+
(increment, delta, cell) => {
|
|
277
|
+
if (dxGrid && model) {
|
|
278
|
+
if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1) {
|
|
279
|
+
handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
|
|
280
|
+
} else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
|
|
281
|
+
handleSaveDraftRow(cell.row);
|
|
282
|
+
handleInsertRowResult(model.insertRow());
|
|
283
|
+
} else {
|
|
284
|
+
dxGrid.refocus(increment, delta);
|
|
265
285
|
}
|
|
266
286
|
}
|
|
267
287
|
},
|
|
268
|
-
[
|
|
288
|
+
[dxGrid, model],
|
|
269
289
|
);
|
|
270
290
|
|
|
271
291
|
const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
|
|
@@ -279,15 +299,71 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
279
299
|
return;
|
|
280
300
|
}
|
|
281
301
|
|
|
302
|
+
// Handle Meta+C (Copy) and Meta+V (Paste) commands
|
|
303
|
+
if (event.metaKey || event.ctrlKey) {
|
|
304
|
+
switch (event.key) {
|
|
305
|
+
case 'c': {
|
|
306
|
+
// Copy focused cell's text content to clipboard
|
|
307
|
+
try {
|
|
308
|
+
const cellData = model.getCellData(cell);
|
|
309
|
+
const textContent = cellData?.toString() ?? '';
|
|
310
|
+
void navigator.clipboard.writeText(textContent);
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.warn('Failed to copy cell content:', error);
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case 'v': {
|
|
318
|
+
// Paste clipboard content to focused cell
|
|
319
|
+
event.preventDefault();
|
|
320
|
+
void navigator.clipboard.readText().then((clipboardText) => {
|
|
321
|
+
try {
|
|
322
|
+
// Attempt to set the cell's content to clipboard content
|
|
323
|
+
model.setCellData(cell, clipboardText);
|
|
324
|
+
handleSave();
|
|
325
|
+
} catch {
|
|
326
|
+
// If validation fails, emit a DxEditRequest event with initialContent from clipboard
|
|
327
|
+
// TODO(thure): Should `dx-grid` expose a method like this?
|
|
328
|
+
const cellElement = (event.target as HTMLElement).closest(
|
|
329
|
+
'[data-dx-grid-action="cell"]',
|
|
330
|
+
) as HTMLElement;
|
|
331
|
+
if (cellElement) {
|
|
332
|
+
const rect = cellElement.getBoundingClientRect();
|
|
333
|
+
const editRequest = new DxEditRequest({
|
|
334
|
+
cellIndex: `${cell.plane},${cell.col},${cell.row}`,
|
|
335
|
+
cellBox: {
|
|
336
|
+
insetInlineStart: rect.left,
|
|
337
|
+
insetBlockStart: rect.top,
|
|
338
|
+
inlineSize: rect.width,
|
|
339
|
+
blockSize: rect.height,
|
|
340
|
+
},
|
|
341
|
+
cellElement,
|
|
342
|
+
initialContent: clipboardText,
|
|
343
|
+
});
|
|
344
|
+
cellElement.dispatchEvent(editRequest);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
282
353
|
switch (event.key) {
|
|
283
354
|
case 'Backspace':
|
|
284
355
|
case 'Delete': {
|
|
285
|
-
|
|
356
|
+
try {
|
|
357
|
+
model.setCellData(cell, undefined);
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
} catch {
|
|
360
|
+
// Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
|
|
361
|
+
}
|
|
286
362
|
break;
|
|
287
363
|
}
|
|
288
364
|
}
|
|
289
365
|
},
|
|
290
|
-
[model],
|
|
366
|
+
[model, dxGrid, handleSave],
|
|
291
367
|
);
|
|
292
368
|
|
|
293
369
|
const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
|
|
@@ -316,57 +392,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
316
392
|
}
|
|
317
393
|
}, [model, dxGrid]);
|
|
318
394
|
|
|
319
|
-
// TODO(burdon): Factor out?
|
|
320
|
-
// TODO(burdon): Generalize to handle other value types (e.g., enums).
|
|
321
|
-
const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
|
|
322
|
-
async ({ field, props }, text) => {
|
|
323
|
-
if (model && props.referenceSchema && field.referencePath) {
|
|
324
|
-
const space = getSpace(model.view);
|
|
325
|
-
invariant(space);
|
|
326
|
-
|
|
327
|
-
let schema;
|
|
328
|
-
if (client) {
|
|
329
|
-
schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
|
|
330
|
-
}
|
|
331
|
-
if (!schema) {
|
|
332
|
-
schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (schema) {
|
|
336
|
-
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
337
|
-
const options = objects
|
|
338
|
-
.map((obj) => {
|
|
339
|
-
const value = getValue(obj, field.referencePath!);
|
|
340
|
-
if (!value || typeof value !== 'string') {
|
|
341
|
-
return undefined;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return {
|
|
345
|
-
label: value,
|
|
346
|
-
data: obj,
|
|
347
|
-
};
|
|
348
|
-
})
|
|
349
|
-
.filter(isNotFalsy);
|
|
350
|
-
|
|
351
|
-
return [
|
|
352
|
-
...options,
|
|
353
|
-
{
|
|
354
|
-
label: t('create new object label', { text }),
|
|
355
|
-
data: createOption(text),
|
|
356
|
-
},
|
|
357
|
-
];
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return [];
|
|
362
|
-
},
|
|
363
|
-
[model, client, t],
|
|
364
|
-
);
|
|
365
|
-
|
|
366
|
-
const handleSave = useCallback(() => {
|
|
367
|
-
dxGrid?.updateCells(true);
|
|
368
|
-
}, [dxGrid]);
|
|
369
|
-
|
|
370
395
|
if (!model || !modals) {
|
|
371
396
|
return <span role='none' className='attention-surface' />;
|
|
372
397
|
}
|
|
@@ -377,21 +402,22 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
377
402
|
model={model}
|
|
378
403
|
modals={modals}
|
|
379
404
|
schema={schema}
|
|
380
|
-
onEnter={handleEnter}
|
|
381
405
|
onFocus={handleFocus}
|
|
382
|
-
onQuery={handleQuery}
|
|
383
406
|
onSave={handleSave}
|
|
407
|
+
client={client}
|
|
384
408
|
/>
|
|
385
409
|
<Grid.Content
|
|
386
410
|
className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
387
411
|
frozen={frozen}
|
|
388
412
|
columns={model.columnMeta.value}
|
|
413
|
+
columnDefault={columnDefault}
|
|
414
|
+
rowDefault={rowDefault}
|
|
389
415
|
limitRows={model.getRowCount() ?? 0}
|
|
390
416
|
limitColumns={model.projection.fields.length}
|
|
391
417
|
overscroll='trap'
|
|
392
418
|
onAxisResize={handleAxisResize}
|
|
393
419
|
onClick={handleGridClick}
|
|
394
|
-
|
|
420
|
+
onKeyDownCapture={handleKeyDown}
|
|
395
421
|
onWheelCapture={handleWheel}
|
|
396
422
|
ref={setDxGrid}
|
|
397
423
|
/>
|
|
@@ -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;
|
|
@@ -5,15 +5,20 @@
|
|
|
5
5
|
import { type Schema } from 'effect';
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Filter, Obj } from '@dxos/echo';
|
|
9
|
+
import { Ref, type TypeAnnotation, getValue } from '@dxos/echo-schema';
|
|
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';
|
|
12
|
-
import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
|
|
15
|
+
import { Form, type FormProps } from '@dxos/react-ui-form';
|
|
16
|
+
import { cellQuery, 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, isNotFalsy, 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
24
|
type FormCellEditorProps = {
|
|
@@ -21,64 +26,148 @@ type FormCellEditorProps = {
|
|
|
21
26
|
model?: TableModel;
|
|
22
27
|
schema?: Schema.Schema.AnyNoContext;
|
|
23
28
|
onSave?: () => void;
|
|
29
|
+
client?: Client;
|
|
30
|
+
modals?: ModalController;
|
|
24
31
|
__gridScope: any;
|
|
25
|
-
}
|
|
32
|
+
} & Omit<FormProps<any>, 'values' | 'schema'>;
|
|
33
|
+
|
|
34
|
+
const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
|
|
26
35
|
|
|
27
|
-
export const FormCellEditor = ({
|
|
28
|
-
|
|
36
|
+
export const FormCellEditor = ({
|
|
37
|
+
fieldProjection,
|
|
38
|
+
model,
|
|
39
|
+
schema,
|
|
40
|
+
onSave,
|
|
41
|
+
client,
|
|
42
|
+
modals,
|
|
43
|
+
__gridScope,
|
|
44
|
+
...formProps
|
|
45
|
+
}: FormCellEditorProps) => {
|
|
46
|
+
const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
|
|
29
47
|
const [editing, setLocalEditing] = useState(false);
|
|
30
48
|
const anchorRef = useRef<HTMLButtonElement | null>(null);
|
|
31
49
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
) as HTMLButtonElement;
|
|
37
|
-
setLocalEditing(true);
|
|
38
|
-
} else {
|
|
39
|
-
anchorRef.current = null;
|
|
40
|
-
setLocalEditing(false);
|
|
41
|
-
}
|
|
42
|
-
}, [contextEditing]);
|
|
50
|
+
const getSchema = useCallback(
|
|
51
|
+
(typeAnnotation: TypeAnnotation) => {
|
|
52
|
+
const space = getSpace(model!.view);
|
|
53
|
+
invariant(space);
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
let schema;
|
|
56
|
+
if (client) {
|
|
57
|
+
schema = client.graph.schemaRegistry.getSchema(typeAnnotation.typename);
|
|
58
|
+
}
|
|
59
|
+
if (!schema) {
|
|
60
|
+
schema = space.db.schemaRegistry.getSchema(typeAnnotation.typename);
|
|
61
|
+
}
|
|
62
|
+
return { space, schema };
|
|
63
|
+
},
|
|
64
|
+
[client, model],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleQueryRefOptions = useCallback(
|
|
68
|
+
async (typeAnnotation: TypeAnnotation) => {
|
|
69
|
+
const { schema, space } = getSchema(typeAnnotation);
|
|
70
|
+
if (model && schema && space) {
|
|
71
|
+
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
72
|
+
return objects
|
|
73
|
+
.map((obj) => {
|
|
74
|
+
return {
|
|
75
|
+
dxn: Obj.getDXN(obj),
|
|
76
|
+
label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
|
|
77
|
+
};
|
|
78
|
+
})
|
|
79
|
+
.filter(isNotFalsy);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return [];
|
|
83
|
+
},
|
|
84
|
+
[client, model],
|
|
85
|
+
);
|
|
50
86
|
|
|
51
87
|
const originalRow = useMemo(() => {
|
|
52
88
|
if (model && contextEditing) {
|
|
53
89
|
const cell = parseCellIndex(contextEditing.index);
|
|
54
|
-
const row = model.getRowAt(cell.row);
|
|
55
|
-
invariant(row);
|
|
56
90
|
|
|
57
|
-
|
|
91
|
+
// Check if this is a draft cell and get the appropriate row data
|
|
92
|
+
if (model.isDraftCell(cell)) {
|
|
93
|
+
const draftRow = model.draftRows.value[cell.row];
|
|
94
|
+
invariant(draftRow);
|
|
95
|
+
return draftRow.data;
|
|
96
|
+
} else {
|
|
97
|
+
const row = model.getRowAt(cell.row);
|
|
98
|
+
invariant(row);
|
|
99
|
+
return row;
|
|
100
|
+
}
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
return undefined;
|
|
61
104
|
}, [model, contextEditing]);
|
|
62
105
|
|
|
63
|
-
const formValues = useMemo(() => {
|
|
64
|
-
if (originalRow) {
|
|
65
|
-
// NOTE(ZaymonFC): Important to get a snapshot to eject from the live object.
|
|
66
|
-
return getSnapshot(originalRow);
|
|
67
|
-
}
|
|
68
|
-
}, [originalRow]);
|
|
69
|
-
|
|
70
106
|
const handleSave = useCallback(
|
|
71
107
|
(values: any) => {
|
|
72
108
|
const path = fieldProjection.field.path;
|
|
73
109
|
const value = getDeep(values, [path]);
|
|
74
110
|
setDeep(originalRow, [path], value);
|
|
111
|
+
contextEditing?.cellElement?.focus();
|
|
75
112
|
setEditing(null);
|
|
76
113
|
setLocalEditing(false);
|
|
77
114
|
onSave?.();
|
|
78
115
|
},
|
|
79
|
-
[fieldProjection.field.path, originalRow],
|
|
116
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow],
|
|
80
117
|
);
|
|
81
118
|
|
|
119
|
+
const handleCreate = useCallback(
|
|
120
|
+
(object: any) => {
|
|
121
|
+
const ref = Ref.make(object);
|
|
122
|
+
const path = fieldProjection.field.path;
|
|
123
|
+
setDeep(originalRow, [path], ref);
|
|
124
|
+
contextEditing?.cellElement?.focus();
|
|
125
|
+
setEditing(null);
|
|
126
|
+
setLocalEditing(false);
|
|
127
|
+
onSave?.();
|
|
128
|
+
},
|
|
129
|
+
[fieldProjection.field.path, onSave, contextEditing, originalRow],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const handleCreateFromQuery = useCallback(
|
|
133
|
+
async (typeAnnotation: TypeAnnotation, query: string) => {
|
|
134
|
+
if (model && modals && contextEditing?.index) {
|
|
135
|
+
setLocalEditing(false);
|
|
136
|
+
modals.openCreateRef(
|
|
137
|
+
typeAnnotation.typename,
|
|
138
|
+
document.querySelector(cellQuery(contextEditing.index, gridId)),
|
|
139
|
+
{
|
|
140
|
+
[fieldProjection.field.referencePath!]: query,
|
|
141
|
+
},
|
|
142
|
+
handleCreate,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
[model, modals, client, contextEditing?.index, gridId, fieldProjection, handleSave],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (contextEditing && contextEditing.cellElement) {
|
|
151
|
+
anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
|
|
152
|
+
'.dx-grid__cell__content',
|
|
153
|
+
) as HTMLButtonElement;
|
|
154
|
+
setLocalEditing(true);
|
|
155
|
+
} else {
|
|
156
|
+
anchorRef.current = null;
|
|
157
|
+
setLocalEditing(false);
|
|
158
|
+
}
|
|
159
|
+
}, [contextEditing]);
|
|
160
|
+
|
|
161
|
+
const narrowedSchema = useMemo(() => {
|
|
162
|
+
if (!schema) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
return narrowSchema(schema, [fieldProjection.field.path]);
|
|
166
|
+
}, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
|
|
167
|
+
|
|
168
|
+
// NOTE: Important to get a snapshot to eject from the live object.
|
|
169
|
+
const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
|
|
170
|
+
|
|
82
171
|
const handleOpenChange = useCallback((nextOpen: boolean) => {
|
|
83
172
|
if (nextOpen === false) {
|
|
84
173
|
setEditing(null);
|
|
@@ -94,12 +183,24 @@ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridS
|
|
|
94
183
|
return (
|
|
95
184
|
<Popover.Root open={editing} onOpenChange={handleOpenChange}>
|
|
96
185
|
<Popover.VirtualTrigger virtualRef={anchorRef} />
|
|
97
|
-
<Popover.
|
|
98
|
-
<Popover.
|
|
99
|
-
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
186
|
+
<Popover.Portal>
|
|
187
|
+
<Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
|
|
188
|
+
<Popover.Arrow />
|
|
189
|
+
<Popover.Viewport>
|
|
190
|
+
<Form
|
|
191
|
+
autoFocus
|
|
192
|
+
values={formValues}
|
|
193
|
+
schema={narrowedSchema as any}
|
|
194
|
+
onSave={handleSave}
|
|
195
|
+
{...formProps}
|
|
196
|
+
onQueryRefOptions={handleQueryRefOptions}
|
|
197
|
+
onCreateFromQuery={handleCreateFromQuery}
|
|
198
|
+
createOptionIcon='ph--plus--regular'
|
|
199
|
+
createOptionLabel={createOptionLabel}
|
|
200
|
+
/>
|
|
201
|
+
</Popover.Viewport>
|
|
202
|
+
</Popover.Content>
|
|
203
|
+
</Popover.Portal>
|
|
103
204
|
</Popover.Root>
|
|
104
205
|
);
|
|
105
206
|
};
|