@dxos/react-ui-table 0.8.4-main.d05673bc65 → 0.8.4-main.d9fc60f731
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/LICENSE +102 -5
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/browser/{chunk-PTHMYAAM.mjs → chunk-X7A7G6RR.mjs} +28 -83
- package/dist/lib/browser/chunk-X7A7G6RR.mjs.map +7 -0
- package/dist/lib/browser/{chunk-BF54SSNL.mjs → chunk-YURROGGF.mjs} +12 -15
- package/dist/lib/browser/chunk-YURROGGF.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +230 -280
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +10 -9
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/translations.mjs +36 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/browser/types/index.mjs +2 -1
- package/dist/lib/node-esm/{chunk-SIRDIPHZ.mjs → chunk-DLAGRBKD.mjs} +12 -15
- package/dist/lib/node-esm/chunk-DLAGRBKD.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-GI72MQSG.mjs → chunk-Q4HSBRJJ.mjs} +28 -83
- package/dist/lib/node-esm/chunk-Q4HSBRJJ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +230 -280
- 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 +10 -9
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/translations.mjs +37 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/lib/node-esm/types/index.mjs +2 -1
- 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 +3 -4
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +21 -22
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +83 -82
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +20 -21
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +85 -84
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableContent.d.ts +18 -0
- package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
- package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +22 -24
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.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 +1 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +4 -5
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +19 -21
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +11 -5
- package/dist/types/src/types/Table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +6 -6
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts.map +1 -1
- package/dist/types/src/util/signals.d.ts.map +1 -1
- package/dist/types/src/util/sort.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts +20 -20
- package/dist/types/src/util/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts +4 -4
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/src/util/tag.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +55 -64
- package/src/components/Table/ColumnActionsMenu.tsx +7 -6
- package/src/components/Table/ColumnSettings.tsx +2 -2
- package/src/components/Table/DynamicTable.stories.tsx +19 -19
- package/src/components/Table/DynamicTable.tsx +13 -22
- package/src/components/Table/Relations.stories.tsx +76 -34
- package/src/components/Table/RowActionsMenu.tsx +3 -2
- package/src/components/Table/Table.stories.tsx +46 -51
- package/src/components/Table/Table.tsx +37 -426
- package/src/components/Table/TableContent.tsx +426 -0
- package/src/components/{TableToolbar → Table}/TableToolbar.tsx +34 -41
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -2
- package/src/components/TableCellEditor/FormCellEditor.tsx +7 -6
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +14 -12
- package/src/components/TableCellEditor/TableCellEditor.tsx +10 -8
- package/src/components/index.ts +0 -1
- package/src/components/testing.ts +11 -16
- package/src/hooks/useProjectionModel.ts +5 -6
- package/src/hooks/useTableModel.ts +1 -1
- package/src/index.ts +0 -4
- package/src/model/table-model.test.ts +4 -10
- package/src/model/table-model.ts +15 -15
- package/src/model/table-presentation.test.ts +4 -10
- package/src/model/table-presentation.ts +5 -7
- package/src/playwright/smoke.spec.ts +3 -3
- package/src/testing/useTestTableModel.ts +7 -8
- package/src/translations.ts +19 -21
- package/src/types/Table.ts +12 -13
- package/src/util/dynamic-table.test.ts +13 -20
- package/src/util/dynamic-table.ts +21 -20
- package/dist/lib/browser/chunk-BF54SSNL.mjs.map +0 -7
- package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +0 -7
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
- package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
- package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
- package/src/components/TableToolbar/index.ts +0 -5
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import * as String from 'effect/String';
|
|
7
|
+
import React, { type MouseEvent, type WheelEvent, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { type Type } from '@dxos/echo';
|
|
10
|
+
import { log } from '@dxos/log';
|
|
11
|
+
import { composable, composableProps } from '@dxos/react-ui';
|
|
12
|
+
import { useAttention } from '@dxos/react-ui-attention';
|
|
13
|
+
import {
|
|
14
|
+
type DxGridAxisMeta,
|
|
15
|
+
type DxGridElement,
|
|
16
|
+
type DxGridPlane,
|
|
17
|
+
type DxGridPlaneRange,
|
|
18
|
+
Grid,
|
|
19
|
+
type GridContentProps,
|
|
20
|
+
closestCell,
|
|
21
|
+
gridSeparatorBlockEnd,
|
|
22
|
+
gridSeparatorInlineEnd,
|
|
23
|
+
} from '@dxos/react-ui-grid';
|
|
24
|
+
import { DxEditRequest } from '@dxos/react-ui-grid';
|
|
25
|
+
import { mx } from '@dxos/ui-theme';
|
|
26
|
+
|
|
27
|
+
import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
|
|
28
|
+
import { tableButtons, tableControls } from '../../util';
|
|
29
|
+
import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
|
|
30
|
+
import { ColumnActionsMenu } from './ColumnActionsMenu';
|
|
31
|
+
import { ColumnSettings } from './ColumnSettings';
|
|
32
|
+
import { RowActionsMenu } from './RowActionsMenu';
|
|
33
|
+
import { TableController, useTableContext } from './Table';
|
|
34
|
+
|
|
35
|
+
const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
|
|
36
|
+
const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
|
|
37
|
+
const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
|
|
38
|
+
const emptyRowsAtom = Atom.make<unknown[]>([]);
|
|
39
|
+
const emptyCellUpdateAtom = Atom.make<number>(0);
|
|
40
|
+
|
|
41
|
+
//
|
|
42
|
+
// Content
|
|
43
|
+
//
|
|
44
|
+
|
|
45
|
+
const TABLE_CONTENT_NAME = 'Table.Content';
|
|
46
|
+
|
|
47
|
+
export type TableContentProps = {
|
|
48
|
+
schema?: Type.AnyEntity;
|
|
49
|
+
model?: TableModel;
|
|
50
|
+
presentation?: TablePresentation;
|
|
51
|
+
// TODO(burdon): Factor out attention.
|
|
52
|
+
ignoreAttention?: boolean;
|
|
53
|
+
attendableId?: string;
|
|
54
|
+
onCreate?: OnCreateHandler;
|
|
55
|
+
onRowClick?: (row: any) => void;
|
|
56
|
+
testId?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const TableContent = composable<HTMLDivElement, TableContentProps>(
|
|
60
|
+
(
|
|
61
|
+
{ schema, model, presentation, ignoreAttention, attendableId, onCreate, onRowClick, testId, ...props },
|
|
62
|
+
forwardedRef,
|
|
63
|
+
) => {
|
|
64
|
+
const registry = useContext(RegistryContext);
|
|
65
|
+
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
66
|
+
const { hasAttention } = useAttention(attendableId ?? model?.id ?? 'table');
|
|
67
|
+
const modals = useMemo(() => new ModalController(registry), [registry]);
|
|
68
|
+
const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
|
|
69
|
+
// Subscribe to rows atom to trigger re-render when rows change.
|
|
70
|
+
const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
|
|
71
|
+
// Subscribe to cell update counter to trigger re-render when individual cells change.
|
|
72
|
+
const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
|
|
73
|
+
// Derive column count from columnMeta (reactive) with fallback.
|
|
74
|
+
const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
|
|
75
|
+
|
|
76
|
+
const draftRowCount = model?.getDraftRowCount() ?? 0;
|
|
77
|
+
|
|
78
|
+
const frozen = useMemo(() => {
|
|
79
|
+
const noActionColumn =
|
|
80
|
+
model?.features.dataEditable === false &&
|
|
81
|
+
model?.features.schemaEditable === false &&
|
|
82
|
+
model.rowActions.length === 0;
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
frozenRowsStart: 1,
|
|
86
|
+
frozenRowsEnd: Math.max(1, draftRowCount),
|
|
87
|
+
frozenColsStart: model?.features.selection.enabled ? 1 : 0,
|
|
88
|
+
frozenColsEnd: noActionColumn ? 0 : 1,
|
|
89
|
+
};
|
|
90
|
+
}, [model, draftRowCount]);
|
|
91
|
+
|
|
92
|
+
const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
|
|
93
|
+
(range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
|
|
94
|
+
[presentation],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!dxGrid || !presentation) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dxGrid.getCells = getCells;
|
|
103
|
+
}, [dxGrid, presentation, getCells]);
|
|
104
|
+
|
|
105
|
+
// Trigger grid update when rows or cell values change.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!dxGrid) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dxGrid.updateCells(true);
|
|
112
|
+
dxGrid.requestUpdate();
|
|
113
|
+
}, [dxGrid, rows, cellUpdateCounter]);
|
|
114
|
+
|
|
115
|
+
const handleInsertRowResult = useCallback(
|
|
116
|
+
(insertResult?: InsertRowResult) => {
|
|
117
|
+
if (insertResult === 'draft') {
|
|
118
|
+
requestAnimationFrame(() => {
|
|
119
|
+
dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
|
|
120
|
+
dxGrid?.refocus();
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
requestAnimationFrame(() => {
|
|
124
|
+
dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
|
|
125
|
+
dxGrid?.refocus();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
[model, dxGrid],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const handleSave = useCallback(() => {
|
|
133
|
+
dxGrid?.updateCells(true);
|
|
134
|
+
dxGrid?.requestUpdate();
|
|
135
|
+
}, [dxGrid]);
|
|
136
|
+
|
|
137
|
+
const handleSaveDraftRow = useCallback(
|
|
138
|
+
(rowIndex = 0) => {
|
|
139
|
+
if (model && dxGrid) {
|
|
140
|
+
const didCommitSuccessfully = model.commitDraftRow(rowIndex);
|
|
141
|
+
if (didCommitSuccessfully) {
|
|
142
|
+
requestAnimationFrame(() => {
|
|
143
|
+
dxGrid.scrollToEndRow();
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[model, dxGrid],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const handleGridClick = useCallback(
|
|
152
|
+
(event: MouseEvent) => {
|
|
153
|
+
const cell = closestCell(event.target as HTMLElement);
|
|
154
|
+
if (cell) {
|
|
155
|
+
const { row: rowIndex, plane } = cell;
|
|
156
|
+
if (onRowClick) {
|
|
157
|
+
if (plane === 'grid') {
|
|
158
|
+
const row = model?.getRowAt(rowIndex);
|
|
159
|
+
row && onRowClick(row);
|
|
160
|
+
} else {
|
|
161
|
+
onRowClick(cell);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
|
|
166
|
+
model.selection.toggleSelectionForRowIndex(rowIndex);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!modals) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const buttonSelector = Object.values(tableButtons)
|
|
175
|
+
.map((button) => `button[${button.attr}]`)
|
|
176
|
+
.join(',');
|
|
177
|
+
|
|
178
|
+
const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
|
|
179
|
+
if (tableButtonElement) {
|
|
180
|
+
const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
|
|
181
|
+
if (!button) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
modals.setTrigger(tableButtonElement);
|
|
186
|
+
const data = button.getData(tableButtonElement);
|
|
187
|
+
switch (data.type) {
|
|
188
|
+
case 'rowMenu': {
|
|
189
|
+
modals.showRowMenu(data.rowIndex);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'columnSettings': {
|
|
193
|
+
modals.showColumnMenu(data.fieldId);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'newColumn': {
|
|
197
|
+
modals.showColumnCreator();
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case 'sort': {
|
|
201
|
+
model?.toggleSort(data.fieldId);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'saveDraftRow': {
|
|
205
|
+
handleSaveDraftRow();
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const controlSelector = Object.values(tableControls)
|
|
213
|
+
.map((control) => `input[${control.attr}]`)
|
|
214
|
+
.join(',');
|
|
215
|
+
|
|
216
|
+
const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
|
|
217
|
+
if (controlElement) {
|
|
218
|
+
const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
|
|
219
|
+
if (!control) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const data = control.getData(controlElement);
|
|
224
|
+
switch (data.type) {
|
|
225
|
+
case 'checkbox': {
|
|
226
|
+
if (data.header) {
|
|
227
|
+
model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
|
|
228
|
+
} else {
|
|
229
|
+
model?.selection.toggleSelectionForRowIndex(data.rowIndex);
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case 'switch': {
|
|
234
|
+
if (model) {
|
|
235
|
+
model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
[model, modals, dxGrid],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
|
|
246
|
+
(increment, delta, cell) => {
|
|
247
|
+
if (dxGrid && model) {
|
|
248
|
+
if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
|
|
249
|
+
handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
|
|
250
|
+
} else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
|
|
251
|
+
handleSaveDraftRow(cell.row);
|
|
252
|
+
handleInsertRowResult(model.insertRow());
|
|
253
|
+
} else {
|
|
254
|
+
dxGrid.refocus(increment, delta);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
[dxGrid, model],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
|
|
262
|
+
(event) => {
|
|
263
|
+
if (!model?.features.dataEditable) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const cell = closestCell(event.target);
|
|
268
|
+
if (!model || !cell) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Handle Meta+C (Copy) and Meta+V (Paste) commands
|
|
273
|
+
if (event.metaKey || event.ctrlKey) {
|
|
274
|
+
switch (event.key) {
|
|
275
|
+
case 'c': {
|
|
276
|
+
// Copy focused cell's text content to clipboard
|
|
277
|
+
try {
|
|
278
|
+
const cellData = model.getCellData(cell);
|
|
279
|
+
const textContent = cellData?.toString() ?? '';
|
|
280
|
+
void navigator.clipboard.writeText(textContent);
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
log.catch(error);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case 'v': {
|
|
288
|
+
// Paste clipboard content to focused cell
|
|
289
|
+
event.preventDefault();
|
|
290
|
+
void navigator.clipboard.readText().then((clipboardText) => {
|
|
291
|
+
try {
|
|
292
|
+
// Attempt to set the cell's content to clipboard content
|
|
293
|
+
model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
|
|
294
|
+
handleSave();
|
|
295
|
+
} catch {
|
|
296
|
+
// If validation fails, emit a DxEditRequest event with initialContent from clipboard
|
|
297
|
+
// TODO(thure): Should `dx-grid` expose a method like this?
|
|
298
|
+
const cellElement = (event.target as HTMLElement).closest(
|
|
299
|
+
'[data-dx-grid-action="cell"]',
|
|
300
|
+
) as HTMLElement;
|
|
301
|
+
if (cellElement) {
|
|
302
|
+
const rect = cellElement.getBoundingClientRect();
|
|
303
|
+
const editRequest = new DxEditRequest({
|
|
304
|
+
cellIndex: `${cell.plane},${cell.col},${cell.row}`,
|
|
305
|
+
cellBox: {
|
|
306
|
+
insetInlineStart: rect.left,
|
|
307
|
+
insetBlockStart: rect.top,
|
|
308
|
+
inlineSize: rect.width,
|
|
309
|
+
blockSize: rect.height,
|
|
310
|
+
},
|
|
311
|
+
cellElement,
|
|
312
|
+
initialContent: clipboardText,
|
|
313
|
+
});
|
|
314
|
+
cellElement.dispatchEvent(editRequest);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
switch (event.key) {
|
|
324
|
+
case 'Backspace':
|
|
325
|
+
case 'Delete': {
|
|
326
|
+
try {
|
|
327
|
+
model.setCellData(cell, undefined);
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
handleSave();
|
|
330
|
+
} catch {
|
|
331
|
+
// Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
[model, dxGrid, handleSave],
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
|
|
341
|
+
(event) => {
|
|
342
|
+
if (event.axis === 'col') {
|
|
343
|
+
const columnIndex = parseInt(event.index, 10);
|
|
344
|
+
model?.setColumnWidth(columnIndex, event.size);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
[model],
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const handleWheel = useCallback(
|
|
351
|
+
(event: WheelEvent) => {
|
|
352
|
+
if (!ignoreAttention && !hasAttention) {
|
|
353
|
+
event.stopPropagation();
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
[hasAttention, ignoreAttention],
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const handleNewColumn = useCallback(() => {
|
|
360
|
+
const columns = model?.getColumnCount();
|
|
361
|
+
if (dxGrid && columns) {
|
|
362
|
+
dxGrid.scrollToColumn(columns - 1);
|
|
363
|
+
}
|
|
364
|
+
}, [model, dxGrid]);
|
|
365
|
+
|
|
366
|
+
// Publish controller to Root.
|
|
367
|
+
const { controllerRef } = useTableContext(TABLE_CONTENT_NAME);
|
|
368
|
+
controllerRef.current = useMemo<TableController>(() => {
|
|
369
|
+
if (!dxGrid || !presentation) {
|
|
370
|
+
return {};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
update: (cell) => {
|
|
375
|
+
if (cell) {
|
|
376
|
+
dxGrid.updateIfWithinBounds(cell, true);
|
|
377
|
+
} else {
|
|
378
|
+
dxGrid.updateCells(true);
|
|
379
|
+
dxGrid.requestUpdate();
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
handleInsertRowResult,
|
|
383
|
+
};
|
|
384
|
+
}, [dxGrid, model, presentation]);
|
|
385
|
+
|
|
386
|
+
if (!model || !modals) {
|
|
387
|
+
return <span role='none' className='dx-attention-surface' />;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<div {...composableProps(props, { classNames: 'dx-container relative' })} ref={forwardedRef}>
|
|
392
|
+
<Grid.Root id={model.id ?? 'table-grid'}>
|
|
393
|
+
<TableValueEditor
|
|
394
|
+
model={model}
|
|
395
|
+
modals={modals}
|
|
396
|
+
schema={schema}
|
|
397
|
+
onFocus={handleFocus}
|
|
398
|
+
onSave={handleSave}
|
|
399
|
+
onCreate={onCreate}
|
|
400
|
+
/>
|
|
401
|
+
<Grid.Content
|
|
402
|
+
className={mx('[--dx-grid-base:var(--base-surface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
|
|
403
|
+
frozen={frozen}
|
|
404
|
+
columns={columnMeta}
|
|
405
|
+
columnDefault={columnDefault}
|
|
406
|
+
rowDefault={rowDefault}
|
|
407
|
+
limitRows={rows.length}
|
|
408
|
+
limitColumns={columnCount}
|
|
409
|
+
overscroll='trap'
|
|
410
|
+
onAxisResize={handleAxisResize}
|
|
411
|
+
onClick={handleGridClick}
|
|
412
|
+
onKeyDownCapture={handleKeyDown}
|
|
413
|
+
onWheelCapture={handleWheel}
|
|
414
|
+
{...(testId && { 'data-testid': testId })}
|
|
415
|
+
ref={setDxGrid}
|
|
416
|
+
/>
|
|
417
|
+
<RowActionsMenu model={model} modals={modals} />
|
|
418
|
+
<ColumnActionsMenu model={model} modals={modals} />
|
|
419
|
+
<ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
|
|
420
|
+
</Grid.Root>
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
TableContent.displayName = TABLE_CONTENT_NAME;
|
|
@@ -5,19 +5,20 @@
|
|
|
5
5
|
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
6
6
|
import React, { useContext, useEffect, useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { composable, composableProps } from '@dxos/react-ui';
|
|
9
9
|
import {
|
|
10
10
|
type ActionGraphEdges,
|
|
11
11
|
type ActionGraphNodes,
|
|
12
12
|
type ActionGraphProps,
|
|
13
13
|
Menu,
|
|
14
14
|
type MenuAction,
|
|
15
|
+
MenuRootProps,
|
|
15
16
|
createGapSeparator,
|
|
16
17
|
createMenuAction,
|
|
17
18
|
useMenuActions,
|
|
18
19
|
} from '@dxos/react-ui-menu';
|
|
19
20
|
|
|
20
|
-
import { translationKey } from '
|
|
21
|
+
import { translationKey } from '#translations';
|
|
21
22
|
|
|
22
23
|
export type TableToolbarActionProperties = { type: TableToolbarActionType };
|
|
23
24
|
|
|
@@ -27,17 +28,6 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
|
|
|
27
28
|
|
|
28
29
|
type TableToolbarState = Partial<{ viewDirty: boolean }>;
|
|
29
30
|
|
|
30
|
-
// TODO(burdon): Radix style layout.
|
|
31
|
-
|
|
32
|
-
export type TableToolbarProps = ThemedClassName<
|
|
33
|
-
TableToolbarState & {
|
|
34
|
-
onAdd?: () => void;
|
|
35
|
-
onSave?: () => void;
|
|
36
|
-
attendableId?: string;
|
|
37
|
-
customActions?: Atom.Atom<ActionGraphProps>;
|
|
38
|
-
}
|
|
39
|
-
>;
|
|
40
|
-
|
|
41
31
|
const createTableToolbarActions = ({
|
|
42
32
|
stateAtom,
|
|
43
33
|
onAdd,
|
|
@@ -55,7 +45,7 @@ const createTableToolbarActions = ({
|
|
|
55
45
|
const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
|
|
56
46
|
type: 'add-row' as const,
|
|
57
47
|
icon: 'ph--plus--regular',
|
|
58
|
-
label: ['add
|
|
48
|
+
label: ['add-row.label', { ns: translationKey }],
|
|
59
49
|
testId: 'table.toolbar.add-row',
|
|
60
50
|
});
|
|
61
51
|
nodes.push(add);
|
|
@@ -65,7 +55,7 @@ const createTableToolbarActions = ({
|
|
|
65
55
|
const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
|
|
66
56
|
type: 'save-view' as const,
|
|
67
57
|
icon: 'ph--floppy-disk--regular',
|
|
68
|
-
label: ['save
|
|
58
|
+
label: ['save-view.label', { ns: translationKey }],
|
|
69
59
|
testId: 'table.toolbar.save-view',
|
|
70
60
|
iconOnly: false,
|
|
71
61
|
hidden: !state.viewDirty,
|
|
@@ -87,32 +77,35 @@ const createTableToolbarActions = ({
|
|
|
87
77
|
};
|
|
88
78
|
});
|
|
89
79
|
|
|
90
|
-
export
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
80
|
+
export type TableToolbarProps = Pick<MenuRootProps, 'attendableId'> &
|
|
81
|
+
TableToolbarState & {
|
|
82
|
+
onAdd?: () => void;
|
|
83
|
+
onSave?: () => void;
|
|
84
|
+
customActions?: Atom.Atom<ActionGraphProps>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const TableToolbar = composable<HTMLDivElement, TableToolbarProps>(
|
|
88
|
+
({ attendableId, viewDirty, onAdd, onSave, customActions, ...props }, forwardedRef) => {
|
|
89
|
+
const registry = useContext(RegistryContext);
|
|
90
|
+
const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
|
|
91
|
+
|
|
92
|
+
// Update state.viewDirty when the prop changes.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
registry.set(stateAtom, { viewDirty });
|
|
95
|
+
}, [registry, stateAtom, viewDirty]);
|
|
101
96
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
const actionsCreator = useMemo(
|
|
98
|
+
() => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
|
|
99
|
+
[stateAtom, onAdd, onSave, customActions],
|
|
100
|
+
);
|
|
101
|
+
const menuActions = useMenuActions(actionsCreator);
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
return (
|
|
104
|
+
<Menu.Root {...menuActions} attendableId={attendableId}>
|
|
105
|
+
<Menu.Toolbar {...composableProps(props)} ref={forwardedRef} />
|
|
106
|
+
</Menu.Root>
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
<Menu.Root {...menu} attendableId={attendableId}>
|
|
115
|
-
<Menu.Toolbar {...props} classNames={classNames} />
|
|
116
|
-
</Menu.Root>
|
|
117
|
-
);
|
|
118
|
-
};
|
|
111
|
+
TableToolbar.displayName = 'TableToolbar';
|
|
@@ -23,11 +23,10 @@ export const CellValidationMessage = ({
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const bgClass = variant === 'error' ? 'bg-error-surface' : 'bg-warning-surface';
|
|
26
|
-
const textClass = variant === 'error' ? 'text-error-
|
|
26
|
+
const textClass = variant === 'error' ? 'text-error-fg' : 'text-warning-fg';
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<div
|
|
30
|
-
role='none'
|
|
31
30
|
className={`absolute ${bgClass} ${textClass} rounded-h-sm text-xs p-1`}
|
|
32
31
|
style={{
|
|
33
32
|
...{ '--dx-grid-cell-width': `${box?.inlineSize ?? 200}px` },
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type * as Schema from 'effect/Schema';
|
|
6
6
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import { Entity,
|
|
8
|
+
import { Entity, Type } from '@dxos/echo';
|
|
9
9
|
import { Ref, getValue } from '@dxos/echo/internal';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
11
11
|
import { type Label, Popover } from '@dxos/react-ui';
|
|
@@ -14,11 +14,12 @@ import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
|
|
|
14
14
|
import { type FieldProjection } from '@dxos/schema';
|
|
15
15
|
import { getDeep, isTruthy, setDeep } from '@dxos/util';
|
|
16
16
|
|
|
17
|
+
import { translationKey } from '#translations';
|
|
18
|
+
|
|
17
19
|
import { type ModalController, type TableModel, type TableRow } from '../../model';
|
|
18
|
-
import { translationKey } from '../../translations';
|
|
19
20
|
import { narrowSchema } from '../../util';
|
|
20
21
|
|
|
21
|
-
const createOptionLabel: Label = ['create
|
|
22
|
+
const createOptionLabel: Label = ['create-new-object.label', { ns: translationKey }];
|
|
22
23
|
|
|
23
24
|
export type OnCreateHandler = (schema: Type.AnyEntity, values: any) => Parameters<typeof Ref.make>[0];
|
|
24
25
|
|
|
@@ -62,7 +63,7 @@ export const FormCellEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
|
|
|
62
63
|
return undefined;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
return narrowSchema(schema, [fieldProjection.field.path]);
|
|
66
|
+
return narrowSchema(Type.getSchema(schema), [fieldProjection.field.path]);
|
|
66
67
|
}, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
|
|
67
68
|
|
|
68
69
|
const originalRow = useMemo<TableRow | undefined>(() => {
|
|
@@ -150,7 +151,7 @@ export const FormCellEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
|
|
|
150
151
|
results
|
|
151
152
|
.map((obj) => {
|
|
152
153
|
return {
|
|
153
|
-
id: Entity.
|
|
154
|
+
id: Entity.getURI(obj),
|
|
154
155
|
label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
|
|
155
156
|
};
|
|
156
157
|
})
|
|
@@ -166,7 +167,7 @@ export const FormCellEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
|
|
|
166
167
|
<Popover.Root open={editing} onOpenChange={handleOpenChange}>
|
|
167
168
|
<Popover.VirtualTrigger virtualRef={anchorRef} />
|
|
168
169
|
<Popover.Portal>
|
|
169
|
-
<Popover.Content tabIndex={-1} classNames='dx-card-popover-width dx-density-
|
|
170
|
+
<Popover.Content tabIndex={-1} classNames='dx-card-popover-width dx-density-md'>
|
|
170
171
|
<Popover.Arrow />
|
|
171
172
|
<Popover.Viewport>
|
|
172
173
|
<Form.Root
|
|
@@ -5,27 +5,26 @@
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
|
-
import { Obj } from '@dxos/echo';
|
|
9
|
-
import {
|
|
10
|
-
import { faker } from '@dxos/random';
|
|
8
|
+
import { Obj, Type, View } from '@dxos/echo';
|
|
9
|
+
import { random } from '@dxos/random';
|
|
11
10
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
12
|
-
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
13
11
|
import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
|
|
12
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
14
13
|
import { ViewModel } from '@dxos/schema';
|
|
15
14
|
import { Task } from '@dxos/types';
|
|
16
15
|
|
|
16
|
+
import { translations } from '#translations';
|
|
17
|
+
|
|
17
18
|
import { useTestTableModel } from '../../testing';
|
|
18
|
-
import { translations } from '../../translations';
|
|
19
19
|
import { Table } from '../../types';
|
|
20
|
-
|
|
21
20
|
import { TableCellEditor } from './TableCellEditor';
|
|
22
21
|
|
|
23
|
-
type
|
|
22
|
+
type DefaultStoryProps = {
|
|
24
23
|
editing: GridEditing;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
// TODO(burdon): Broken layout.
|
|
28
|
-
const DefaultStory = ({ editing }:
|
|
27
|
+
const DefaultStory = ({ editing }: DefaultStoryProps) => {
|
|
29
28
|
const { model, table } = useTestTableModel();
|
|
30
29
|
|
|
31
30
|
if (!model || !table) {
|
|
@@ -53,15 +52,18 @@ const meta = {
|
|
|
53
52
|
createIdentity: true,
|
|
54
53
|
createSpace: true,
|
|
55
54
|
onCreateSpace: async ({ space }) => {
|
|
56
|
-
const { view, jsonSchema } = await ViewModel.makeFromDatabase({
|
|
55
|
+
const { view, jsonSchema } = await ViewModel.makeFromDatabase({
|
|
56
|
+
db: space.db,
|
|
57
|
+
typename: Type.getTypename(Task.Task),
|
|
58
|
+
});
|
|
57
59
|
const table = Table.make({ view, jsonSchema });
|
|
58
60
|
space.db.add(table);
|
|
59
61
|
Array.from({ length: 10 }).forEach(() => {
|
|
60
62
|
space.db.add(
|
|
61
63
|
Obj.make(Task.Task, {
|
|
62
|
-
title:
|
|
63
|
-
status:
|
|
64
|
-
description:
|
|
64
|
+
title: random.person.fullName(),
|
|
65
|
+
status: random.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
|
|
66
|
+
description: random.lorem.sentence(),
|
|
65
67
|
}),
|
|
66
68
|
);
|
|
67
69
|
});
|