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