@dxos/react-ui-table 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8
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-4KOQTPFC.mjs +1702 -0
- package/dist/lib/browser/chunk-4KOQTPFC.mjs.map +7 -0
- package/dist/lib/browser/chunk-62PYOJPY.mjs +67 -0
- package/dist/lib/browser/chunk-62PYOJPY.mjs.map +7 -0
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1151 -1175
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +37 -45
- 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 +4 -3
- package/dist/lib/node-esm/chunk-7LMOZUTS.mjs +68 -0
- package/dist/lib/node-esm/chunk-7LMOZUTS.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-KOSV2J5N.mjs +1704 -0
- package/dist/lib/node-esm/chunk-KOSV2J5N.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1151 -1175
- 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 +37 -45
- 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 +4 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +6 -7
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +36 -3
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +112 -5
- 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 +21 -19
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +115 -5
- 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 +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -9
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -14
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +38 -5
- 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/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +23 -16
- 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 +6 -6
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useTableModel.d.ts +9 -9
- 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 +5 -8
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +18 -9
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +111 -30
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +4 -2
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts +2 -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 +18 -34
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +21 -23
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +32 -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 +14 -12
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
- package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts +7 -0
- package/dist/types/src/util/query-ast.d.ts.map +1 -0
- package/dist/types/src/util/schema.d.ts +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 +12 -0
- package/dist/types/src/util/sort.d.ts.map +1 -0
- 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 +68 -77
- package/src/components/Table/ColumnActionsMenu.tsx +17 -12
- package/src/components/Table/ColumnSettings.tsx +8 -9
- package/src/components/Table/DynamicTable.stories.tsx +41 -38
- package/src/components/Table/DynamicTable.tsx +32 -36
- package/src/components/Table/Relations.stories.tsx +219 -61
- package/src/components/Table/RowActionsMenu.tsx +27 -17
- package/src/components/Table/Table.stories.tsx +217 -141
- package/src/components/Table/Table.tsx +46 -391
- package/src/components/Table/TableContent.tsx +426 -0
- package/src/components/Table/TableToolbar.tsx +111 -0
- package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -6
- package/src/components/TableCellEditor/FormCellEditor.tsx +136 -41
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +39 -73
- package/src/components/TableCellEditor/TableCellEditor.tsx +81 -223
- package/src/components/TableCellEditor/index.ts +1 -0
- package/src/components/index.ts +0 -1
- package/src/components/testing.ts +42 -21
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useAddRow.ts +14 -14
- package/src/hooks/useProjectionModel.ts +38 -0
- package/src/hooks/useTableModel.ts +36 -34
- package/src/index.ts +0 -4
- package/src/model/modal-controller.ts +23 -40
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +86 -56
- package/src/model/table-model.ts +464 -182
- package/src/model/table-presentation.test.ts +64 -59
- package/src/model/table-presentation.ts +107 -83
- package/src/playwright/TableManager.ts +13 -6
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +92 -46
- package/src/testing/useTestTableModel.ts +51 -42
- package/src/translations.ts +20 -22
- package/src/types/Table.ts +80 -0
- package/src/types/index.ts +1 -1
- package/src/util/dynamic-table.test.ts +63 -0
- package/src/util/dynamic-table.ts +53 -39
- package/src/util/index.ts +2 -1
- package/src/util/query-ast.ts +22 -0
- package/src/util/schema.ts +4 -4
- package/src/util/sort.ts +90 -0
- package/src/util/table-buttons.ts +3 -3
- package/src/util/table-controls.ts +2 -2
- package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
- package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
- package/dist/lib/browser/chunk-ZAOSIG3L.mjs +0 -1814
- package/dist/lib/browser/chunk-ZAOSIG3L.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-ZOFFI3JJ.mjs +0 -1815
- package/dist/lib/node-esm/chunk-ZOFFI3JJ.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/TableCellEditor/extension.d.ts +0 -13
- package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
- 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/dist/types/src/model/table-sorting.d.ts +0 -53
- package/dist/types/src/model/table-sorting.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 -12
- package/dist/types/src/util/table.d.ts.map +0 -1
- package/src/components/Table/CreateRefPanel.tsx +0 -86
- package/src/components/TableCellEditor/extension.ts +0 -61
- package/src/components/TableToolbar/TableToolbar.tsx +0 -102
- package/src/components/TableToolbar/index.ts +0 -5
- package/src/model/table-sorting.ts +0 -216
- package/src/types/schema.ts +0 -16
- package/src/util/table.ts +0 -53
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Completion } from '@codemirror/autocomplete';
|
|
6
|
-
import {
|
|
7
|
-
import React, { useCallback,
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { debounce } from '@dxos/async';
|
|
10
|
+
import { type Type } from '@dxos/echo';
|
|
11
|
+
import { Format, TypeEnum } from '@dxos/echo/Format';
|
|
10
12
|
import { invariant } from '@dxos/invariant';
|
|
11
13
|
import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
|
|
12
|
-
import { useThemeContext } from '@dxos/react-ui';
|
|
13
|
-
import { createMarkdownExtensions } from '@dxos/react-ui-editor';
|
|
14
14
|
import {
|
|
15
15
|
type EditorBlurHandler,
|
|
16
16
|
type EditorKeyEvent,
|
|
@@ -18,51 +18,41 @@ import {
|
|
|
18
18
|
GridCellEditor,
|
|
19
19
|
type GridCellEditorProps,
|
|
20
20
|
type GridScopedProps,
|
|
21
|
-
cellQuery,
|
|
22
21
|
editorKeys,
|
|
23
22
|
parseCellIndex,
|
|
24
23
|
useGridContext,
|
|
25
24
|
} from '@dxos/react-ui-grid';
|
|
26
|
-
import { createLinks, tagPicker } from '@dxos/react-ui-tag-picker';
|
|
27
25
|
import { type FieldProjection } from '@dxos/schema';
|
|
28
26
|
|
|
29
27
|
import { type ModalController, type TableModel } from '../../model';
|
|
30
|
-
|
|
31
28
|
import { CellValidationMessage } from './CellValidationMessage';
|
|
32
|
-
import {
|
|
33
|
-
import { FormCellEditor } from './FormCellEditor';
|
|
34
|
-
|
|
35
|
-
const newValue = Symbol.for('newValue');
|
|
29
|
+
import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
|
|
36
30
|
|
|
37
31
|
/**
|
|
38
32
|
* Option to create new object/value.
|
|
39
33
|
*/
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export type QueryResult = Pick<Completion, 'label'> & { data: any };
|
|
34
|
+
export type QueryResult = Pick<Completion, 'label'> & {
|
|
35
|
+
data: any;
|
|
36
|
+
};
|
|
45
37
|
|
|
46
|
-
export type TableCellEditorProps = {
|
|
38
|
+
export type TableCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
|
|
39
|
+
schema?: T;
|
|
47
40
|
model?: TableModel;
|
|
48
41
|
modals?: ModalController;
|
|
49
|
-
schema?: Schema.AnyNoContext;
|
|
50
|
-
onEnter?: (cell: DxGridPosition) => void;
|
|
51
42
|
onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
|
|
43
|
+
onCreate?: OnCreateHandler;
|
|
52
44
|
onSave?: () => void;
|
|
53
|
-
onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
|
|
54
45
|
};
|
|
55
46
|
|
|
56
|
-
export const TableValueEditor = ({
|
|
47
|
+
export const TableValueEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
|
|
48
|
+
__gridScope,
|
|
49
|
+
schema,
|
|
57
50
|
model,
|
|
58
51
|
modals,
|
|
59
|
-
schema,
|
|
60
|
-
onEnter,
|
|
61
52
|
onFocus,
|
|
62
53
|
onSave,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}: GridScopedProps<TableCellEditorProps>) => {
|
|
54
|
+
onCreate,
|
|
55
|
+
}: GridScopedProps<TableCellEditorProps<T>>) => {
|
|
66
56
|
const { editing } = useGridContext('TableValueEditor', __gridScope);
|
|
67
57
|
|
|
68
58
|
const fieldProjection = useMemo<FieldProjection | undefined>(() => {
|
|
@@ -71,7 +61,7 @@ export const TableValueEditor = ({
|
|
|
71
61
|
}
|
|
72
62
|
|
|
73
63
|
const { col } = parseCellIndex(editing.index);
|
|
74
|
-
const field = model.projection.
|
|
64
|
+
const field = model.projection.getFields()[col];
|
|
75
65
|
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
76
66
|
invariant(fieldProjection);
|
|
77
67
|
return fieldProjection;
|
|
@@ -79,48 +69,42 @@ export const TableValueEditor = ({
|
|
|
79
69
|
|
|
80
70
|
if (
|
|
81
71
|
fieldProjection?.props.type === TypeEnum.Array ||
|
|
82
|
-
fieldProjection?.props.format ===
|
|
83
|
-
|
|
72
|
+
fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
|
|
73
|
+
fieldProjection?.props.format === Format.TypeFormat.Ref
|
|
74
|
+
// TODO(thure): Support `Format.TypeFormat.MultiSelect`
|
|
84
75
|
) {
|
|
85
76
|
return (
|
|
86
|
-
<FormCellEditor
|
|
87
|
-
fieldProjection={fieldProjection}
|
|
88
|
-
model={model}
|
|
89
|
-
schema={schema}
|
|
77
|
+
<FormCellEditor<T>
|
|
90
78
|
__gridScope={__gridScope}
|
|
79
|
+
schema={schema}
|
|
80
|
+
model={model}
|
|
81
|
+
fieldProjection={fieldProjection}
|
|
82
|
+
modals={modals}
|
|
91
83
|
onSave={onSave}
|
|
84
|
+
onCreate={onCreate}
|
|
92
85
|
/>
|
|
93
86
|
);
|
|
94
87
|
}
|
|
95
88
|
|
|
96
|
-
// For all other types, use the existing cell editor
|
|
97
|
-
return
|
|
98
|
-
<TableCellEditor
|
|
99
|
-
model={model}
|
|
100
|
-
modals={modals}
|
|
101
|
-
onEnter={onEnter}
|
|
102
|
-
onFocus={onFocus}
|
|
103
|
-
onQuery={onQuery}
|
|
104
|
-
onSave={onSave}
|
|
105
|
-
__gridScope={__gridScope}
|
|
106
|
-
/>
|
|
107
|
-
);
|
|
89
|
+
// For all other types, use the existing cell editor.
|
|
90
|
+
return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
|
|
108
91
|
};
|
|
109
92
|
|
|
110
|
-
const
|
|
93
|
+
const cellEditorSlots: GridCellEditorProps['slots'] = {
|
|
94
|
+
content: {
|
|
95
|
+
className: '!py-(--dx-grid-cell-editor-padding-block)',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
111
98
|
|
|
112
99
|
export const TableCellEditor = ({
|
|
100
|
+
__gridScope,
|
|
113
101
|
model,
|
|
114
102
|
modals,
|
|
115
|
-
onEnter,
|
|
116
103
|
onFocus,
|
|
117
|
-
onQuery,
|
|
118
104
|
onSave,
|
|
119
|
-
__gridScope,
|
|
120
105
|
}: GridScopedProps<TableCellEditorProps>) => {
|
|
121
|
-
const {
|
|
106
|
+
const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
|
|
122
107
|
const suppressNextBlur = useRef(false);
|
|
123
|
-
const { themeMode } = useThemeContext();
|
|
124
108
|
const [validationError, setValidationError] = useState<string | null>(null);
|
|
125
109
|
const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
|
|
126
110
|
|
|
@@ -130,44 +114,13 @@ export const TableCellEditor = ({
|
|
|
130
114
|
}
|
|
131
115
|
|
|
132
116
|
const { col } = parseCellIndex(editing.index);
|
|
133
|
-
const field = model.projection.
|
|
117
|
+
const field = model.projection.getFields()[col];
|
|
134
118
|
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
135
119
|
invariant(fieldProjection);
|
|
136
120
|
return fieldProjection;
|
|
137
121
|
}, [model, editing]);
|
|
138
122
|
|
|
139
|
-
|
|
140
|
-
// Check for existing validation errors when editing starts (for draft rows).
|
|
141
|
-
if (!model || !editing || !fieldProjection) {
|
|
142
|
-
setValidationError(null);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const cell = parseCellIndex(editing.index);
|
|
147
|
-
const { row, col } = cell;
|
|
148
|
-
|
|
149
|
-
if (model.isDraftCell(cell)) {
|
|
150
|
-
const field = model.projection.fields[col];
|
|
151
|
-
const hasValidationError = model.hasDraftRowValidationError(row, field.path);
|
|
152
|
-
|
|
153
|
-
if (hasValidationError) {
|
|
154
|
-
const draftRows = model.draftRows.value;
|
|
155
|
-
if (row >= 0 && row < draftRows.length) {
|
|
156
|
-
const draftRow = draftRows[row];
|
|
157
|
-
const validationError = draftRow.validationErrors?.find((error) => error.path === field.path);
|
|
158
|
-
if (validationError) {
|
|
159
|
-
setValidationError(validationError.message);
|
|
160
|
-
setValidationVariant('warning');
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
setValidationError(null);
|
|
165
|
-
}
|
|
166
|
-
} else {
|
|
167
|
-
setValidationError(null);
|
|
168
|
-
}
|
|
169
|
-
}, [model, editing, fieldProjection]);
|
|
170
|
-
|
|
123
|
+
// TOOD(burdon): Attach to event handler?
|
|
171
124
|
const handleEnter = useCallback(
|
|
172
125
|
async (value: any) => {
|
|
173
126
|
if (!model || !editing) {
|
|
@@ -180,7 +133,6 @@ export const TableCellEditor = ({
|
|
|
180
133
|
if (validationResult.valid) {
|
|
181
134
|
setValidationError(null);
|
|
182
135
|
model.setCellData(cell, value);
|
|
183
|
-
onEnter?.(cell);
|
|
184
136
|
onFocus?.();
|
|
185
137
|
setEditing(null);
|
|
186
138
|
onSave?.();
|
|
@@ -189,19 +141,26 @@ export const TableCellEditor = ({
|
|
|
189
141
|
setValidationVariant('error');
|
|
190
142
|
}
|
|
191
143
|
},
|
|
192
|
-
[model, editing,
|
|
144
|
+
[model, editing, onFocus, setEditing],
|
|
193
145
|
);
|
|
194
146
|
|
|
195
147
|
const handleBlur = useCallback<EditorBlurHandler>(
|
|
196
148
|
async (value) => {
|
|
197
|
-
if (!model || !editing) {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
149
|
if (suppressNextBlur.current) {
|
|
201
150
|
suppressNextBlur.current = false;
|
|
151
|
+
} else if (model && editing) {
|
|
152
|
+
// Save silently if validation passes
|
|
153
|
+
const cell = parseCellIndex(editing.index);
|
|
154
|
+
if (value !== undefined) {
|
|
155
|
+
const result = await model.validateCellData(cell, value);
|
|
156
|
+
if (result.valid) {
|
|
157
|
+
setValidationError(null);
|
|
158
|
+
model.setCellData(cell, value);
|
|
159
|
+
setEditing(null);
|
|
160
|
+
onSave?.();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
202
163
|
}
|
|
203
|
-
|
|
204
|
-
// Don't save on blur - let handleClose handle validation and saving
|
|
205
164
|
},
|
|
206
165
|
[model, editing],
|
|
207
166
|
);
|
|
@@ -213,17 +172,16 @@ export const TableCellEditor = ({
|
|
|
213
172
|
}
|
|
214
173
|
|
|
215
174
|
const cell = parseCellIndex(editing.index);
|
|
216
|
-
|
|
217
175
|
if (value !== undefined) {
|
|
218
176
|
// Pre-commit validation check.
|
|
219
177
|
const result = await model.validateCellData(cell, value);
|
|
220
178
|
|
|
221
179
|
if (result.valid) {
|
|
180
|
+
suppressNextBlur.current = true;
|
|
222
181
|
setValidationError(null);
|
|
223
182
|
model.setCellData(cell, value);
|
|
224
183
|
setEditing(null);
|
|
225
184
|
onSave?.();
|
|
226
|
-
onEnter?.(cell);
|
|
227
185
|
if (event && onFocus) {
|
|
228
186
|
onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
|
|
229
187
|
}
|
|
@@ -232,6 +190,7 @@ export const TableCellEditor = ({
|
|
|
232
190
|
setValidationVariant('error');
|
|
233
191
|
}
|
|
234
192
|
} else {
|
|
193
|
+
suppressNextBlur.current = true;
|
|
235
194
|
setValidationError(null);
|
|
236
195
|
setEditing(null);
|
|
237
196
|
onSave?.();
|
|
@@ -240,154 +199,48 @@ export const TableCellEditor = ({
|
|
|
240
199
|
}
|
|
241
200
|
}
|
|
242
201
|
},
|
|
243
|
-
[model, editing, onFocus,
|
|
202
|
+
[model, editing, onFocus, fieldProjection, setEditing, onSave],
|
|
244
203
|
);
|
|
245
204
|
|
|
246
|
-
const
|
|
205
|
+
const extensions = useMemo(() => {
|
|
247
206
|
if (!fieldProjection) {
|
|
248
207
|
return [];
|
|
249
208
|
}
|
|
250
209
|
|
|
251
|
-
const
|
|
210
|
+
const extensions = [
|
|
252
211
|
editorKeys({
|
|
253
212
|
onClose: handleClose,
|
|
254
213
|
...(editing?.initialContent && { onNav: handleClose }),
|
|
255
214
|
}),
|
|
256
215
|
];
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
extension.push(
|
|
270
|
-
tagPicker({
|
|
271
|
-
mode,
|
|
272
|
-
keymap: false,
|
|
273
|
-
onSearch: (text, selectedIds) => {
|
|
274
|
-
return options
|
|
275
|
-
.filter(
|
|
276
|
-
(option) =>
|
|
277
|
-
selectedIds.indexOf(option.id) === -1 &&
|
|
278
|
-
(text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
|
|
279
|
-
)
|
|
280
|
-
.map((option) => ({
|
|
281
|
-
id: option.id,
|
|
282
|
-
label: option.title,
|
|
283
|
-
hue: option.color as any,
|
|
284
|
-
}));
|
|
285
|
-
},
|
|
286
|
-
onUpdate: (ids) => {
|
|
287
|
-
if (model && editing) {
|
|
288
|
-
if (ids.length === 0) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
if (mode === 'single-select') {
|
|
292
|
-
void handleEnter(ids[0]);
|
|
216
|
+
// Add validation extension to handle content changes.
|
|
217
|
+
if (model && editing) {
|
|
218
|
+
extensions.push(
|
|
219
|
+
EditorView.updateListener.of(
|
|
220
|
+
debounce((update) => {
|
|
221
|
+
const content = update.state.doc.toString();
|
|
222
|
+
const cell = parseCellIndex(editing.index);
|
|
223
|
+
|
|
224
|
+
// Perform validation on content change.
|
|
225
|
+
void model.validateCellData(cell, content).then((result) => {
|
|
226
|
+
if (result.valid) {
|
|
227
|
+
setValidationError(null);
|
|
293
228
|
} else {
|
|
294
|
-
|
|
229
|
+
setValidationError(result.error);
|
|
230
|
+
setValidationVariant('error');
|
|
295
231
|
}
|
|
296
|
-
}
|
|
297
|
-
},
|
|
298
|
-
|
|
232
|
+
});
|
|
233
|
+
}, 10),
|
|
234
|
+
),
|
|
299
235
|
);
|
|
300
236
|
}
|
|
301
237
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
case FormatEnum.Ref: {
|
|
305
|
-
extension.push([
|
|
306
|
-
completion({
|
|
307
|
-
onQuery: (text) => onQuery(fieldProjection, text),
|
|
308
|
-
onMatch: (data) => {
|
|
309
|
-
if (model && editing && modals) {
|
|
310
|
-
if (isCreateOption(data)) {
|
|
311
|
-
const { field, props } = fieldProjection;
|
|
312
|
-
if (props.referenceSchema) {
|
|
313
|
-
suppressNextBlur.current = true;
|
|
314
|
-
modals.openCreateRef(
|
|
315
|
-
props.referenceSchema,
|
|
316
|
-
document.querySelector(cellQuery(editing.index, gridId)),
|
|
317
|
-
{
|
|
318
|
-
[field.referencePath!]: data.text,
|
|
319
|
-
},
|
|
320
|
-
(data) => {
|
|
321
|
-
void handleEnter(data);
|
|
322
|
-
},
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
|
-
} else {
|
|
326
|
-
void handleEnter(data);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
}),
|
|
331
|
-
]);
|
|
332
|
-
break;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return extension;
|
|
338
|
-
}, [model, modals, editing, fieldProjection, handleClose, themeMode]);
|
|
238
|
+
return extensions;
|
|
239
|
+
}, [model, modals, editing, fieldProjection, handleClose]);
|
|
339
240
|
|
|
340
241
|
const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
|
|
341
242
|
if (model && editing) {
|
|
342
243
|
const cell = parseCellIndex(editing.index);
|
|
343
|
-
const { col } = cell;
|
|
344
|
-
const field = model.projection.fields[col];
|
|
345
|
-
const fieldProjection = model.projection.getFieldProjection(field.id);
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
fieldProjection?.props.format === FormatEnum.SingleSelect ||
|
|
349
|
-
fieldProjection?.props.format === FormatEnum.MultiSelect
|
|
350
|
-
) {
|
|
351
|
-
const value = model.getCellData(cell);
|
|
352
|
-
|
|
353
|
-
if (value !== undefined) {
|
|
354
|
-
const options = fieldProjection.props.options || [];
|
|
355
|
-
|
|
356
|
-
if (fieldProjection.props.format === FormatEnum.MultiSelect) {
|
|
357
|
-
const tagItems = value
|
|
358
|
-
.split(',')
|
|
359
|
-
.map((id: string) => {
|
|
360
|
-
const option = options.find((o) => o.id === id);
|
|
361
|
-
if (option) {
|
|
362
|
-
return {
|
|
363
|
-
id,
|
|
364
|
-
label: option.title,
|
|
365
|
-
hue: option.color as any,
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
return undefined;
|
|
369
|
-
})
|
|
370
|
-
.filter((item: any): item is { id: any; label: string; hue: any } => item !== undefined);
|
|
371
|
-
|
|
372
|
-
return createLinks(tagItems);
|
|
373
|
-
} else {
|
|
374
|
-
const option = options.find((o) => o.id === value);
|
|
375
|
-
|
|
376
|
-
if (option) {
|
|
377
|
-
const tagItem = {
|
|
378
|
-
id: value,
|
|
379
|
-
label: option.title,
|
|
380
|
-
hue: option.color as any,
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
return createLinks([tagItem]);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return '';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
244
|
const value = model.getCellData(cell);
|
|
392
245
|
return value !== undefined ? String(value) : '';
|
|
393
246
|
}
|
|
@@ -396,7 +249,12 @@ export const TableCellEditor = ({
|
|
|
396
249
|
return (
|
|
397
250
|
<>
|
|
398
251
|
<CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
|
|
399
|
-
<GridCellEditor
|
|
252
|
+
<GridCellEditor
|
|
253
|
+
extensions={extensions}
|
|
254
|
+
slots={cellEditorSlots}
|
|
255
|
+
getCellContent={getCellContent}
|
|
256
|
+
onBlur={handleBlur}
|
|
257
|
+
/>
|
|
400
258
|
</>
|
|
401
259
|
);
|
|
402
260
|
};
|
package/src/components/index.ts
CHANGED
|
@@ -2,33 +2,54 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
6
7
|
import { useEffect } from 'react';
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { DXN, Format, Type } from '@dxos/echo';
|
|
10
|
+
import { TypeEnum } from '@dxos/echo/Format';
|
|
11
|
+
import { SchemaEx } from '@dxos/effect';
|
|
12
|
+
import { random } from '@dxos/random';
|
|
11
13
|
import { type ProjectionModel } from '@dxos/schema';
|
|
12
14
|
|
|
13
|
-
export const TestSchema =
|
|
14
|
-
id: ObjectId,
|
|
15
|
+
export const TestSchema = Schema.Struct({
|
|
15
16
|
name: Schema.optional(Schema.String),
|
|
16
17
|
age: Schema.optional(Schema.Number),
|
|
17
18
|
active: Schema.optional(Schema.Boolean),
|
|
18
19
|
netWorth: Schema.optional(Schema.Number),
|
|
19
|
-
});
|
|
20
|
+
}).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
|
|
20
21
|
|
|
21
|
-
export
|
|
22
|
-
|
|
22
|
+
export type TestItem = {
|
|
23
|
+
name: string;
|
|
24
|
+
age: number;
|
|
25
|
+
active: boolean;
|
|
26
|
+
netWorth: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates an Atom containing test items data.
|
|
31
|
+
*/
|
|
32
|
+
export const createItemsAtom = (n: number) => {
|
|
33
|
+
return Atom.make<{ data: TestItem[] }>({
|
|
23
34
|
data: Array.from({ length: n }, () => ({
|
|
24
|
-
name:
|
|
25
|
-
age:
|
|
26
|
-
active:
|
|
27
|
-
netWorth:
|
|
35
|
+
name: random.person.fullName(),
|
|
36
|
+
age: random.number.int({ min: 20, max: 70 }),
|
|
37
|
+
active: random.datatype.boolean(),
|
|
38
|
+
netWorth: random.number.int(),
|
|
28
39
|
})),
|
|
29
|
-
});
|
|
40
|
+
}).pipe(Atom.keepAlive);
|
|
41
|
+
};
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Creates plain test items (non-reactive).
|
|
45
|
+
*/
|
|
46
|
+
export const createItems = (n: number): TestItem[] => {
|
|
47
|
+
return Array.from({ length: n }, () => ({
|
|
48
|
+
name: random.person.fullName(),
|
|
49
|
+
age: random.number.int({ min: 20, max: 70 }),
|
|
50
|
+
active: random.datatype.boolean(),
|
|
51
|
+
netWorth: random.number.int(),
|
|
52
|
+
}));
|
|
32
53
|
};
|
|
33
54
|
|
|
34
55
|
export type SimulatorProps = {
|
|
@@ -59,7 +80,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
|
|
|
59
80
|
|
|
60
81
|
const i = setInterval(() => {
|
|
61
82
|
const rowIdx = Math.floor(Math.random() * items.length);
|
|
62
|
-
const fields = projection.
|
|
83
|
+
const fields = projection.getFields() ?? [];
|
|
63
84
|
const columnIdx = Math.floor(Math.random() * fields.length);
|
|
64
85
|
const field = fields[columnIdx];
|
|
65
86
|
const item = items[rowIdx];
|
|
@@ -73,22 +94,22 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
|
|
|
73
94
|
// TODO(ZaymonFC): Restore this once I know how to derive the type from the schema.
|
|
74
95
|
switch (type) {
|
|
75
96
|
case TypeEnum.String: {
|
|
76
|
-
setValue(item, path, `Updated ${Date.now()}`);
|
|
97
|
+
SchemaEx.setValue(item, path, `Updated ${Date.now()}`);
|
|
77
98
|
break;
|
|
78
99
|
}
|
|
79
100
|
case TypeEnum.Number: {
|
|
80
|
-
setValue(item, path, Math.floor(Math.random() * 100));
|
|
101
|
+
SchemaEx.setValue(item, path, Math.floor(Math.random() * 100));
|
|
81
102
|
break;
|
|
82
103
|
}
|
|
83
104
|
case TypeEnum.Boolean: {
|
|
84
|
-
setValue(item, path, !item[path]);
|
|
105
|
+
SchemaEx.setValue(item, path, !item[path]);
|
|
85
106
|
break;
|
|
86
107
|
}
|
|
87
108
|
}
|
|
88
109
|
|
|
89
110
|
if (format) {
|
|
90
111
|
switch (format) {
|
|
91
|
-
case
|
|
112
|
+
case Format.TypeFormat.Currency: {
|
|
92
113
|
item[path] = Math.floor(Math.random() * 1000);
|
|
93
114
|
break;
|
|
94
115
|
}
|
|
@@ -98,5 +119,5 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
|
|
|
98
119
|
}, updateInterval);
|
|
99
120
|
|
|
100
121
|
return () => clearInterval(i);
|
|
101
|
-
}, [items, projection.
|
|
122
|
+
}, [items, projection.getFields(), updateInterval]);
|
|
102
123
|
};
|
package/src/hooks/index.ts
CHANGED
package/src/hooks/useAddRow.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Schema } from 'effect';
|
|
6
5
|
import { useCallback } from 'react';
|
|
7
6
|
|
|
8
|
-
import { type
|
|
9
|
-
import { Obj } from '@dxos/echo';
|
|
7
|
+
import { type Database, Obj, type Type } from '@dxos/echo';
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
import { type InsertRowResult } from '../model';
|
|
10
|
+
|
|
11
|
+
export type UseAddRowProps = {
|
|
12
|
+
db?: Database.Database;
|
|
13
|
+
schema?: Type.AnyObj;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -18,19 +18,19 @@ export type UseAddRowParams = {
|
|
|
18
18
|
* Returns a callback that attempts to create an object and returns boolean success/failure.
|
|
19
19
|
* Can accept optional data to create objects with specific content (used for committing draft rows).
|
|
20
20
|
*/
|
|
21
|
-
export const useAddRow = ({
|
|
21
|
+
export const useAddRow = ({ db, schema }: UseAddRowProps) => {
|
|
22
22
|
return useCallback(
|
|
23
|
-
(data?: any) => {
|
|
24
|
-
if (
|
|
23
|
+
(data?: any): InsertRowResult => {
|
|
24
|
+
if (db && schema) {
|
|
25
25
|
try {
|
|
26
|
-
|
|
27
|
-
return
|
|
26
|
+
db.add(Obj.make(schema, data ?? {}));
|
|
27
|
+
return 'final';
|
|
28
28
|
} catch (error) {
|
|
29
|
-
return
|
|
29
|
+
return 'draft';
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
return
|
|
32
|
+
return 'final';
|
|
33
33
|
},
|
|
34
|
-
[
|
|
34
|
+
[db, schema],
|
|
35
35
|
);
|
|
36
36
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Registry } from '@effect-atom/atom-react';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Type } from '@dxos/echo';
|
|
9
|
+
import { useAsyncEffect } from '@dxos/react-ui';
|
|
10
|
+
import { ProjectionModel, createEchoChangeCallback } from '@dxos/schema';
|
|
11
|
+
|
|
12
|
+
import { type Table } from '../types';
|
|
13
|
+
|
|
14
|
+
export const useProjectionModel = <S extends Type.AnyEntity>(
|
|
15
|
+
schema: S | undefined,
|
|
16
|
+
table: Table.Table | undefined,
|
|
17
|
+
registry: Registry.Registry,
|
|
18
|
+
) => {
|
|
19
|
+
const [projection, setProjection] = useState<ProjectionModel | undefined>();
|
|
20
|
+
|
|
21
|
+
useAsyncEffect(async () => {
|
|
22
|
+
if (schema && table) {
|
|
23
|
+
const view = await table.view.load();
|
|
24
|
+
// Use the live jsonSchema reference for reactivity.
|
|
25
|
+
const jsonSchema = schema.jsonSchema;
|
|
26
|
+
|
|
27
|
+
// Always use createEchoChangeCallback since the view is ECHO-backed.
|
|
28
|
+
// Pass the type entity only when stored, to allow schema mutations.
|
|
29
|
+
const change = createEchoChangeCallback(view, Type.getDatabase(schema) != null ? schema : undefined);
|
|
30
|
+
|
|
31
|
+
const projection = new ProjectionModel({ registry, view, baseSchema: jsonSchema, change });
|
|
32
|
+
projection.normalizeView();
|
|
33
|
+
setProjection(projection);
|
|
34
|
+
}
|
|
35
|
+
}, [schema, table, registry]);
|
|
36
|
+
|
|
37
|
+
return projection;
|
|
38
|
+
};
|