@dxos/react-ui-table 0.8.3-main.7f5a14c → 0.8.3
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-U6LL2AVU.mjs → chunk-TL7CKQQV.mjs} +4 -3
- package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +252 -125
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node/{chunk-CXDGXPM6.cjs → chunk-O2VPBNS5.cjs} +6 -5
- package/dist/lib/node/chunk-O2VPBNS5.cjs.map +7 -0
- package/dist/lib/node/index.cjs +252 -127
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types/index.cjs +3 -3
- package/dist/lib/node/types/index.cjs.map +1 -1
- package/dist/lib/node-esm/{chunk-EFSM52KQ.mjs → chunk-GBT6GD4G.mjs} +4 -3
- package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +252 -125
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +2 -0
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +7 -0
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -0
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +2 -2
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +7 -0
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/types/table.d.ts +5 -4
- package/dist/types/src/types/table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/package.json +34 -34
- package/src/components/Table/DynamicTable.stories.tsx +3 -2
- package/src/components/Table/Table.stories.tsx +15 -7
- package/src/components/Table/Table.tsx +13 -3
- package/src/components/Table/relations.stories.tsx +15 -10
- package/src/components/TableCellEditor/CellValidationMessage.tsx +38 -0
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -1
- package/src/components/TableCellEditor/TableCellEditor.tsx +54 -22
- package/src/components/testing.ts +5 -4
- package/src/hooks/useTableModel.ts +8 -1
- package/src/model/table-model.test.ts +3 -2
- package/src/model/table-model.ts +88 -39
- package/src/model/table-presentation.test.ts +3 -2
- package/src/model/table-presentation.ts +1 -1
- package/src/types/table.ts +3 -2
- package/src/util/dynamic-table.ts +2 -2
- package/src/util/table.ts +11 -10
- package/dist/lib/browser/chunk-U6LL2AVU.mjs.map +0 -7
- package/dist/lib/node/chunk-CXDGXPM6.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-EFSM52KQ.mjs.map +0 -7
|
@@ -8,7 +8,7 @@ import { type StoryObj, type Meta } from '@storybook/react';
|
|
|
8
8
|
import { Schema } from 'effect';
|
|
9
9
|
import React, { useCallback, useMemo, useRef } from 'react';
|
|
10
10
|
|
|
11
|
-
import { Type } from '@dxos/echo';
|
|
11
|
+
import { Obj, Type } from '@dxos/echo';
|
|
12
12
|
import { FormatEnum, isMutable, toJsonSchema, EchoObject, GeneratorAnnotation } from '@dxos/echo-schema';
|
|
13
13
|
import { invariant } from '@dxos/invariant';
|
|
14
14
|
import { useGlobalFilteredObjects } from '@dxos/plugin-search';
|
|
@@ -125,6 +125,7 @@ const useTestTableModel = () => {
|
|
|
125
125
|
model,
|
|
126
126
|
presentation,
|
|
127
127
|
space,
|
|
128
|
+
client,
|
|
128
129
|
handleInsertRow,
|
|
129
130
|
handleSaveView,
|
|
130
131
|
handleDeleteRows,
|
|
@@ -166,7 +167,7 @@ const StoryViewEditor = () => {
|
|
|
166
167
|
//
|
|
167
168
|
|
|
168
169
|
const DefaultStory = () => {
|
|
169
|
-
const { schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView } = useTestTableModel();
|
|
170
|
+
const { schema, table, tableRef, model, presentation, client, handleInsertRow, handleSaveView } = useTestTableModel();
|
|
170
171
|
|
|
171
172
|
if (!schema || !table) {
|
|
172
173
|
return <div />;
|
|
@@ -177,7 +178,14 @@ const DefaultStory = () => {
|
|
|
177
178
|
<div className='grid grid-rows-[min-content_1fr] min-bs-0 overflow-hidden'>
|
|
178
179
|
<TableToolbar classNames='border-be border-subduedSeparator' onAdd={handleInsertRow} onSave={handleSaveView} />
|
|
179
180
|
<Table.Root>
|
|
180
|
-
<Table.Main
|
|
181
|
+
<Table.Main
|
|
182
|
+
ref={tableRef}
|
|
183
|
+
model={model}
|
|
184
|
+
presentation={presentation}
|
|
185
|
+
schema={schema}
|
|
186
|
+
client={client}
|
|
187
|
+
ignoreAttention
|
|
188
|
+
/>
|
|
181
189
|
</Table.Root>
|
|
182
190
|
</div>
|
|
183
191
|
<div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
|
|
@@ -208,7 +216,7 @@ const meta: Meta<StoryProps> = {
|
|
|
208
216
|
createIdentity: true,
|
|
209
217
|
createSpace: true,
|
|
210
218
|
onSpaceCreated: async ({ client, space }) => {
|
|
211
|
-
const table = space.db.add(
|
|
219
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
212
220
|
const schema = await initializeTable({ client, space, table, initialRow: false });
|
|
213
221
|
Array.from({ length: 10 }).map(() => {
|
|
214
222
|
return space.db.add(
|
|
@@ -237,7 +245,7 @@ export const StaticSchema: StoryObj = {
|
|
|
237
245
|
createIdentity: true,
|
|
238
246
|
createSpace: true,
|
|
239
247
|
onSpaceCreated: async ({ client, space }) => {
|
|
240
|
-
const table = space.db.add(
|
|
248
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
241
249
|
await initializeTable({ client, space, table, typename: Testing.Organization.typename });
|
|
242
250
|
|
|
243
251
|
const factory = createObjectFactory(space.db, faker as any);
|
|
@@ -279,7 +287,7 @@ export const ArrayOfObjects: StoryObj = {
|
|
|
279
287
|
createIdentity: true,
|
|
280
288
|
createSpace: true,
|
|
281
289
|
onSpaceCreated: async ({ client, space }) => {
|
|
282
|
-
const table = space.db.add(
|
|
290
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
283
291
|
await initializeTable({ client, space, table, typename: ContactWithArrayOfEmails.typename });
|
|
284
292
|
|
|
285
293
|
const factory = createObjectFactory(space.db, faker as any);
|
|
@@ -332,7 +340,7 @@ export const Tags: Meta<StoryProps> = {
|
|
|
332
340
|
const [storedSchema] = await space.db.schemaRegistry.register([schema]);
|
|
333
341
|
|
|
334
342
|
// Initialize table.
|
|
335
|
-
const table = space.db.add(
|
|
343
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
336
344
|
await initializeTable({ client, space, table, initialRow: false, typename });
|
|
337
345
|
|
|
338
346
|
// Populate.
|
|
@@ -15,6 +15,7 @@ import React, {
|
|
|
15
15
|
useEffect,
|
|
16
16
|
} from 'react';
|
|
17
17
|
|
|
18
|
+
import { type Client } from '@dxos/client';
|
|
18
19
|
import { getValue } from '@dxos/echo-schema';
|
|
19
20
|
import { invariant } from '@dxos/invariant';
|
|
20
21
|
import { Filter } from '@dxos/react-client/echo';
|
|
@@ -77,13 +78,14 @@ export type TableMainProps = {
|
|
|
77
78
|
model?: TableModel;
|
|
78
79
|
presentation?: TablePresentation;
|
|
79
80
|
schema?: Schema.AnyNoContext;
|
|
81
|
+
client?: Client;
|
|
80
82
|
// TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
|
|
81
83
|
ignoreAttention?: boolean;
|
|
82
84
|
onRowClick?: (row: any) => void;
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
const TableMain = forwardRef<TableController, TableMainProps>(
|
|
86
|
-
({ model, presentation, ignoreAttention, schema, onRowClick }, forwardedRef) => {
|
|
88
|
+
({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
|
|
87
89
|
const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
|
|
88
90
|
const { hasAttention } = useAttention(model?.id ?? 'table');
|
|
89
91
|
const { t } = useTranslation(translationKey);
|
|
@@ -307,7 +309,15 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
307
309
|
if (model && props.referenceSchema && field.referencePath) {
|
|
308
310
|
const space = model.space;
|
|
309
311
|
invariant(space);
|
|
310
|
-
|
|
312
|
+
|
|
313
|
+
let schema;
|
|
314
|
+
if (client) {
|
|
315
|
+
schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
|
|
316
|
+
}
|
|
317
|
+
if (!schema) {
|
|
318
|
+
schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
|
|
319
|
+
}
|
|
320
|
+
|
|
311
321
|
if (schema) {
|
|
312
322
|
const { objects } = await space.db.query(Filter.type(schema)).run();
|
|
313
323
|
const options = objects
|
|
@@ -336,7 +346,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
|
|
|
336
346
|
|
|
337
347
|
return [];
|
|
338
348
|
},
|
|
339
|
-
[model, t],
|
|
349
|
+
[model, client, t],
|
|
340
350
|
);
|
|
341
351
|
|
|
342
352
|
if (!model || !modals) {
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
7
|
import { type StoryObj, type Meta } from '@storybook/react';
|
|
8
|
-
import { type Schema } from 'effect';
|
|
9
8
|
import { SchemaAST } from 'effect';
|
|
10
9
|
import React, { useEffect, useMemo } from 'react';
|
|
11
10
|
|
|
12
|
-
import {
|
|
11
|
+
import { Obj, type Type } from '@dxos/echo';
|
|
12
|
+
import { getSchemaTypename, toJsonSchema } from '@dxos/echo-schema';
|
|
13
13
|
import { getAnnotation } from '@dxos/effect';
|
|
14
14
|
import { invariant } from '@dxos/invariant';
|
|
15
15
|
import { faker } from '@dxos/random';
|
|
16
|
-
import {
|
|
16
|
+
import { useClient } from '@dxos/react-client';
|
|
17
|
+
import { makeRef } from '@dxos/react-client/echo';
|
|
17
18
|
import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
|
|
18
19
|
import { DataType, createView, ViewProjection, ViewType } from '@dxos/schema';
|
|
19
20
|
import { createAsyncGenerator, type ValueGenerator } from '@dxos/schema/testing';
|
|
@@ -32,26 +33,29 @@ const generator: ValueGenerator = faker as any;
|
|
|
32
33
|
// TODO(burdon): Mutable and immutable views.
|
|
33
34
|
// TODO(burdon): Reconcile schemas types and utils (see API PR).
|
|
34
35
|
// TODO(burdon): Base type for T (with id); see ECHO API PR?
|
|
35
|
-
const useTestModel = <
|
|
36
|
+
const useTestModel = <S extends Type.Obj.Any>(schema: S, count: number) => {
|
|
36
37
|
const { space } = useClientProvider();
|
|
37
38
|
|
|
38
39
|
const jsonSchema = useMemo(() => toJsonSchema(schema), [schema]);
|
|
39
40
|
const table = useMemo(() => {
|
|
41
|
+
if (!space) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
40
44
|
const typename = getSchemaTypename(schema);
|
|
41
45
|
invariant(typename);
|
|
42
46
|
const name = getAnnotation<string>(SchemaAST.TitleAnnotationId)(schema.ast) ?? typename;
|
|
43
47
|
const view = createView({ name, typename, jsonSchema });
|
|
44
|
-
return
|
|
45
|
-
}, [schema]);
|
|
48
|
+
return space.db.add(Obj.make(TableType, { view: makeRef(view) }));
|
|
49
|
+
}, [schema, space, jsonSchema]);
|
|
46
50
|
|
|
47
51
|
const projection = useMemo(() => {
|
|
48
|
-
if (!table
|
|
52
|
+
if (!table?.view?.target) {
|
|
49
53
|
return undefined;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
|
|
53
57
|
return new ViewProjection(jsonSchema, table.view.target);
|
|
54
|
-
}, [
|
|
58
|
+
}, [jsonSchema, table]);
|
|
55
59
|
|
|
56
60
|
const features = useMemo<TableFeatures>(
|
|
57
61
|
() => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
|
|
@@ -82,16 +86,17 @@ const useTestModel = <T extends BaseObject & HasId>(schema: Schema.Schema<T>, co
|
|
|
82
86
|
};
|
|
83
87
|
|
|
84
88
|
const DefaultStory = () => {
|
|
89
|
+
const client = useClient();
|
|
85
90
|
const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
|
|
86
91
|
const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
|
|
87
92
|
|
|
88
93
|
return (
|
|
89
94
|
<div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
|
|
90
95
|
<Table.Root>
|
|
91
|
-
<Table.Main model={orgModel} presentation={orgPresentation} ignoreAttention />
|
|
96
|
+
<Table.Main model={orgModel} presentation={orgPresentation} client={client} ignoreAttention />
|
|
92
97
|
</Table.Root>
|
|
93
98
|
<Table.Root>
|
|
94
|
-
<Table.Main model={contactModel} presentation={contactPresentation} ignoreAttention />
|
|
99
|
+
<Table.Main model={contactModel} presentation={contactPresentation} client={client} ignoreAttention />
|
|
95
100
|
</Table.Root>
|
|
96
101
|
</div>
|
|
97
102
|
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { useGridContext, type GridScopedProps } from '@dxos/react-ui-grid';
|
|
8
|
+
|
|
9
|
+
export type CellValidationMessageProps = {
|
|
10
|
+
validationError: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const CellValidationMessage = ({
|
|
14
|
+
validationError,
|
|
15
|
+
__gridScope,
|
|
16
|
+
}: GridScopedProps<CellValidationMessageProps>) => {
|
|
17
|
+
const { editing, editBox: box } = useGridContext('GridSheetCellEditor', __gridScope);
|
|
18
|
+
|
|
19
|
+
if (!editing || !validationError) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
role='none'
|
|
26
|
+
className='absolute bg-errorSurface text-errorSurfaceText rounded-bs-sm text-xs p-1'
|
|
27
|
+
style={{
|
|
28
|
+
...{ '--dx-gridCellWidth': `${box?.inlineSize ?? 200}px` },
|
|
29
|
+
zIndex: 10,
|
|
30
|
+
insetBlockEnd: `calc(100% - ${box.insetBlockStart}px)`,
|
|
31
|
+
insetInlineStart: box.insetInlineStart,
|
|
32
|
+
inlineSize: box.inlineSize,
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{validationError}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
@@ -7,6 +7,7 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
+
import { Obj } from '@dxos/echo';
|
|
10
11
|
import { type EchoSchema, isMutable } from '@dxos/echo-schema';
|
|
11
12
|
import { invariant } from '@dxos/invariant';
|
|
12
13
|
import { faker } from '@dxos/random';
|
|
@@ -96,7 +97,7 @@ const meta: Meta<StoryProps> = {
|
|
|
96
97
|
createIdentity: true,
|
|
97
98
|
createSpace: true,
|
|
98
99
|
onSpaceCreated: async ({ client, space }) => {
|
|
99
|
-
const table = space.db.add(
|
|
100
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
100
101
|
const schema = await initializeTable({ client, space, table });
|
|
101
102
|
Array.from({ length: 10 }).forEach(() => {
|
|
102
103
|
space.db.add(
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Completion } from '@codemirror/autocomplete';
|
|
6
6
|
import { type Schema } from 'effect/Schema';
|
|
7
|
-
import React, { useCallback, useMemo, useRef } from 'react';
|
|
7
|
+
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
|
|
10
10
|
import { invariant } from '@dxos/invariant';
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
|
|
28
28
|
import { type FieldProjection } from '@dxos/schema';
|
|
29
29
|
|
|
30
|
+
import { CellValidationMessage } from './CellValidationMessage';
|
|
30
31
|
import { FormCellEditor } from './FormCellEditor';
|
|
31
32
|
import { completion } from './extension';
|
|
32
33
|
import { type TableModel, type ModalController } from '../../model';
|
|
@@ -102,6 +103,7 @@ export const TableCellEditor = ({
|
|
|
102
103
|
const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
|
|
103
104
|
const suppressNextBlur = useRef(false);
|
|
104
105
|
const { themeMode } = useThemeContext();
|
|
106
|
+
const [_validationError, setValidationError] = useState<string | null>(null);
|
|
105
107
|
|
|
106
108
|
const fieldProjection = useMemo<FieldProjection | undefined>(() => {
|
|
107
109
|
if (!model || !editing) {
|
|
@@ -116,51 +118,76 @@ export const TableCellEditor = ({
|
|
|
116
118
|
}, [model, editing]);
|
|
117
119
|
|
|
118
120
|
const handleEnter = useCallback(
|
|
119
|
-
(value: any) => {
|
|
121
|
+
async (value: any) => {
|
|
120
122
|
if (!model || !editing) {
|
|
121
123
|
return;
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
const cell = parseCellIndex(editing.index);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
|
|
128
|
+
// Validate the value
|
|
129
|
+
const result = await model.validateCellData(cell, value);
|
|
130
|
+
|
|
131
|
+
if (result.valid) {
|
|
132
|
+
setValidationError(null);
|
|
133
|
+
model.setCellData(cell, value);
|
|
134
|
+
onEnter?.(cell);
|
|
135
|
+
onFocus?.();
|
|
136
|
+
setEditing(null);
|
|
137
|
+
} else {
|
|
138
|
+
setValidationError(result.error);
|
|
139
|
+
// Editor stays open on validation failure
|
|
140
|
+
}
|
|
129
141
|
},
|
|
130
142
|
[model, editing, onEnter, onFocus, setEditing],
|
|
131
143
|
);
|
|
132
144
|
|
|
133
145
|
const handleBlur = useCallback<EditorBlurHandler>(
|
|
134
|
-
(value) => {
|
|
146
|
+
async (value) => {
|
|
135
147
|
if (!model || !editing) {
|
|
136
148
|
return;
|
|
137
149
|
}
|
|
138
150
|
if (suppressNextBlur.current) {
|
|
139
151
|
suppressNextBlur.current = false;
|
|
140
|
-
return;
|
|
141
152
|
}
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
if (value !== undefined) {
|
|
145
|
-
model.setCellData(cell, value);
|
|
146
|
-
}
|
|
154
|
+
// Don't save on blur - let handleClose handle validation and saving
|
|
147
155
|
},
|
|
148
156
|
[model, editing],
|
|
149
157
|
);
|
|
150
158
|
|
|
151
159
|
const handleClose = useCallback<EditorKeyOrBlurHandler>(
|
|
152
|
-
(value, event) => {
|
|
160
|
+
async (value, event) => {
|
|
153
161
|
if (!model || !editing || !fieldProjection) {
|
|
154
162
|
return;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
165
|
const cell = parseCellIndex(editing.index);
|
|
158
|
-
|
|
159
|
-
if (
|
|
160
|
-
|
|
166
|
+
|
|
167
|
+
if (value !== undefined) {
|
|
168
|
+
// Pre-commit validation check.
|
|
169
|
+
const result = await model.validateCellData(cell, value);
|
|
170
|
+
|
|
171
|
+
if (result.valid) {
|
|
172
|
+
setValidationError(null);
|
|
173
|
+
model.setCellData(cell, value);
|
|
174
|
+
setEditing(null);
|
|
175
|
+
onEnter?.(cell);
|
|
176
|
+
if (event && onFocus) {
|
|
177
|
+
onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
setValidationError(result.error);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
setValidationError(null);
|
|
184
|
+
setEditing(null);
|
|
185
|
+
if (event && onFocus) {
|
|
186
|
+
onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
|
|
187
|
+
}
|
|
161
188
|
}
|
|
162
189
|
},
|
|
163
|
-
[model, editing, onFocus, onEnter, fieldProjection],
|
|
190
|
+
[model, editing, onFocus, onEnter, fieldProjection, setEditing],
|
|
164
191
|
);
|
|
165
192
|
|
|
166
193
|
const extension = useMemo(() => {
|
|
@@ -209,9 +236,9 @@ export const TableCellEditor = ({
|
|
|
209
236
|
return;
|
|
210
237
|
}
|
|
211
238
|
if (mode === 'single-select') {
|
|
212
|
-
handleEnter(ids[0]);
|
|
239
|
+
void handleEnter(ids[0]);
|
|
213
240
|
} else {
|
|
214
|
-
handleEnter(ids);
|
|
241
|
+
void handleEnter(ids);
|
|
215
242
|
}
|
|
216
243
|
}
|
|
217
244
|
},
|
|
@@ -238,12 +265,12 @@ export const TableCellEditor = ({
|
|
|
238
265
|
[field.referencePath!]: data.text,
|
|
239
266
|
},
|
|
240
267
|
(data) => {
|
|
241
|
-
handleEnter(data);
|
|
268
|
+
void handleEnter(data);
|
|
242
269
|
},
|
|
243
270
|
);
|
|
244
271
|
}
|
|
245
272
|
} else {
|
|
246
|
-
handleEnter(data);
|
|
273
|
+
void handleEnter(data);
|
|
247
274
|
}
|
|
248
275
|
}
|
|
249
276
|
},
|
|
@@ -312,7 +339,12 @@ export const TableCellEditor = ({
|
|
|
312
339
|
}
|
|
313
340
|
}, [model, editing]);
|
|
314
341
|
|
|
315
|
-
return
|
|
342
|
+
return (
|
|
343
|
+
<>
|
|
344
|
+
<CellValidationMessage validationError={_validationError} __gridScope={__gridScope} />
|
|
345
|
+
<GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} />
|
|
346
|
+
</>
|
|
347
|
+
);
|
|
316
348
|
};
|
|
317
349
|
|
|
318
350
|
const determineNavigationAxis = ({ key }: EditorKeyEvent): 'col' | 'row' | undefined => {
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
import { Schema } from 'effect';
|
|
6
6
|
import { useEffect } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
9
|
+
import { FormatEnum, ObjectId, setValue, toJsonSchema, TypedObject, TypeEnum } from '@dxos/echo-schema';
|
|
9
10
|
import { faker } from '@dxos/random';
|
|
10
|
-
import { live
|
|
11
|
+
import { live } from '@dxos/react-client/echo';
|
|
11
12
|
import { createView, type ViewProjection } from '@dxos/schema';
|
|
12
13
|
|
|
13
14
|
import { TableType } from '../types';
|
|
@@ -21,8 +22,8 @@ export const TestSchema = TypedObject({ typename: 'example.com/type/Test', versi
|
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
export const createTable = (schema = TestSchema) => {
|
|
24
|
-
return
|
|
25
|
-
view:
|
|
25
|
+
return Obj.make(TableType, {
|
|
26
|
+
view: Ref.make(
|
|
26
27
|
createView({
|
|
27
28
|
name: 'Test',
|
|
28
29
|
typename: schema.typename,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { effect } from '@preact/signals-core';
|
|
6
|
+
import orderBy from 'lodash.orderby';
|
|
6
7
|
import { useEffect, useState } from 'react';
|
|
7
8
|
|
|
8
9
|
import { fullyQualifiedId, getSpace, type Live } from '@dxos/react-client/echo';
|
|
@@ -66,7 +67,13 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
66
67
|
// Update data.
|
|
67
68
|
useEffect(() => {
|
|
68
69
|
if (rows) {
|
|
69
|
-
|
|
70
|
+
// TODO(ZaymonFC): Remove this workaround once unstable query ordering issue is resolved
|
|
71
|
+
/*
|
|
72
|
+
* Sort all objects by string id field as a temporary workaround for query ordering issues
|
|
73
|
+
* Reference: https://github.com/dxos/dxos/pull/9409
|
|
74
|
+
*/
|
|
75
|
+
const sortedRows = orderBy(rows, [(row) => String(row.id)], ['asc']);
|
|
76
|
+
model?.setRows(sortedRows);
|
|
70
77
|
}
|
|
71
78
|
}, [model, rows]);
|
|
72
79
|
|
|
@@ -6,11 +6,12 @@ import { computed } from '@preact/signals-core';
|
|
|
6
6
|
import { Schema } from 'effect';
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
8
|
|
|
9
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
9
10
|
import { TypedObject } from '@dxos/echo-schema';
|
|
10
11
|
import { updateCounter } from '@dxos/echo-schema/testing';
|
|
11
12
|
import { registerSignalsRuntime } from '@dxos/echo-signals';
|
|
12
13
|
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
13
|
-
import { live
|
|
14
|
+
import { live } from '@dxos/react-client/echo';
|
|
14
15
|
import { createView, ViewProjection } from '@dxos/schema';
|
|
15
16
|
|
|
16
17
|
import { TableModel, type TableModelProps } from './table-model';
|
|
@@ -108,6 +109,6 @@ const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|
|
|
108
109
|
const schema = createEchoSchema(Test);
|
|
109
110
|
const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
|
|
110
111
|
const projection = new ViewProjection(schema.jsonSchema, view);
|
|
111
|
-
const table =
|
|
112
|
+
const table = Obj.make(TableType, { view: Ref.make(view) });
|
|
112
113
|
return new TableModel({ id: table.id, space: undefined, view, projection, ...props });
|
|
113
114
|
};
|
package/src/model/table-model.ts
CHANGED
|
@@ -6,7 +6,15 @@ import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-c
|
|
|
6
6
|
|
|
7
7
|
import { type Space } from '@dxos/client/echo';
|
|
8
8
|
import { Resource } from '@dxos/context';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type FieldSortType,
|
|
11
|
+
FormatEnum,
|
|
12
|
+
getValue,
|
|
13
|
+
setValue,
|
|
14
|
+
type JsonProp,
|
|
15
|
+
getSnapshot,
|
|
16
|
+
getSchema,
|
|
17
|
+
} from '@dxos/echo-schema';
|
|
10
18
|
import { invariant } from '@dxos/invariant';
|
|
11
19
|
import { isLiveObject, makeRef } from '@dxos/live-object';
|
|
12
20
|
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
@@ -16,13 +24,16 @@ import {
|
|
|
16
24
|
type DxGridPlanePosition,
|
|
17
25
|
type DxGridPosition,
|
|
18
26
|
} from '@dxos/react-ui-grid';
|
|
19
|
-
import { type ViewType, type ViewProjection } from '@dxos/schema';
|
|
27
|
+
import { type ViewType, type ViewProjection, type PropertyType, validateSchema } from '@dxos/schema';
|
|
20
28
|
|
|
21
29
|
import { type SelectionMode, SelectionModel } from './selection-model';
|
|
22
30
|
import { TableSorting } from './table-sorting';
|
|
23
31
|
import { touch } from '../util';
|
|
24
32
|
import { extractTagIds } from '../util/tag';
|
|
25
33
|
|
|
34
|
+
// TODO(ZaymonFC): Use a common type?
|
|
35
|
+
export type ValidationResult = { valid: true } | { valid: false; error: string };
|
|
36
|
+
|
|
26
37
|
// TODO(burdon): Use schema types.
|
|
27
38
|
export type TableRow = Record<JsonProp, any> & { id: string };
|
|
28
39
|
|
|
@@ -330,54 +341,58 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
330
341
|
}
|
|
331
342
|
};
|
|
332
343
|
|
|
333
|
-
public
|
|
344
|
+
public validateCellData = async ({ col, row }: DxGridPlanePosition, value: any): Promise<ValidationResult> => {
|
|
334
345
|
const rowIdx = this._sorting.getDataIndex(row);
|
|
335
346
|
const fields = this._view?.fields ?? [];
|
|
336
347
|
if (col < 0 || col >= fields.length) {
|
|
337
|
-
return;
|
|
348
|
+
return { valid: false, error: 'Invalid column index' };
|
|
338
349
|
}
|
|
339
350
|
|
|
340
351
|
const field = fields[col];
|
|
341
352
|
const { props } = this._projection.getFieldProjection(field.id);
|
|
342
|
-
switch (props.format) {
|
|
343
|
-
case FormatEnum.Ref: {
|
|
344
|
-
// TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
|
|
345
|
-
// plain string value. Maybe onBlur should be called with the actual value?
|
|
346
|
-
if (isLiveObject(value)) {
|
|
347
|
-
setValue(this._rows.value[rowIdx], field.path, makeRef(value));
|
|
348
|
-
}
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
353
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (ids && ids.length > 0) {
|
|
355
|
-
setValue(this._rows.value[rowIdx], field.path, ids[0]);
|
|
356
|
-
}
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
354
|
+
const currentRow = this._rows.value[rowIdx];
|
|
355
|
+
invariant(currentRow, 'Invalid row index');
|
|
359
356
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
setValue(this._rows.value[rowIdx], field.path, ids);
|
|
364
|
-
}
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
357
|
+
// Get a snapshot of the current object.
|
|
358
|
+
const snapshot = getSnapshot(currentRow);
|
|
359
|
+
const transformedValue = editorTextToCellValue(props, value);
|
|
367
360
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
361
|
+
// Set the proposed value.
|
|
362
|
+
setValue(snapshot, field.path, transformedValue);
|
|
363
|
+
|
|
364
|
+
const schema = getSchema(currentRow);
|
|
365
|
+
invariant(schema);
|
|
366
|
+
|
|
367
|
+
const validationResult = validateSchema(schema, snapshot);
|
|
368
|
+
if (validationResult && validationResult.length > 0) {
|
|
369
|
+
const error = validationResult[0];
|
|
370
|
+
invariant(error.path === field.path);
|
|
371
|
+
return { valid: false, error: error.message };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return { valid: true };
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
|
|
378
|
+
const rowIdx = this._sorting.getDataIndex(row);
|
|
379
|
+
const fields = this._view?.fields ?? [];
|
|
380
|
+
if (col < 0 || col >= fields.length) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const field = fields[col];
|
|
385
|
+
const { props } = this._projection.getFieldProjection(field.id);
|
|
386
|
+
const transformedValue = editorTextToCellValue(props, value);
|
|
387
|
+
|
|
388
|
+
// Special handling for Ref format to preserve existing behavior
|
|
389
|
+
if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
|
|
390
|
+
// TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
|
|
391
|
+
// plain string value. Maybe onBlur should be called with the actual value?
|
|
392
|
+
return;
|
|
380
393
|
}
|
|
394
|
+
|
|
395
|
+
setValue(this._rows.value[rowIdx], field.path, transformedValue);
|
|
381
396
|
};
|
|
382
397
|
|
|
383
398
|
/**
|
|
@@ -446,3 +461,37 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
446
461
|
this._sorting.save();
|
|
447
462
|
}
|
|
448
463
|
}
|
|
464
|
+
|
|
465
|
+
const editorTextToCellValue = (props: PropertyType, value: any): any => {
|
|
466
|
+
switch (props.format) {
|
|
467
|
+
case FormatEnum.Ref: {
|
|
468
|
+
if (isLiveObject(value)) {
|
|
469
|
+
return makeRef(value);
|
|
470
|
+
} else {
|
|
471
|
+
return value;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
case FormatEnum.SingleSelect: {
|
|
476
|
+
const ids = extractTagIds(value);
|
|
477
|
+
if (ids && ids.length > 0) {
|
|
478
|
+
return ids[0];
|
|
479
|
+
} else {
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
case FormatEnum.MultiSelect: {
|
|
485
|
+
const ids = extractTagIds(value);
|
|
486
|
+
return ids || value;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
default: {
|
|
490
|
+
return parseValue({
|
|
491
|
+
type: props.type,
|
|
492
|
+
format: props.format,
|
|
493
|
+
value,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
import { Schema } from 'effect';
|
|
6
6
|
import { describe, expect, it, beforeEach } from 'vitest';
|
|
7
7
|
|
|
8
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
8
9
|
import { TypedObject } from '@dxos/echo-schema';
|
|
9
|
-
import { live
|
|
10
|
+
import { live } from '@dxos/live-object';
|
|
10
11
|
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
11
12
|
import { createView, ViewProjection } from '@dxos/schema';
|
|
12
13
|
|
|
@@ -98,6 +99,6 @@ const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|
|
|
98
99
|
const schema = createEchoSchema(Test);
|
|
99
100
|
const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
|
|
100
101
|
const projection = new ViewProjection(schema.jsonSchema, view);
|
|
101
|
-
const table =
|
|
102
|
+
const table = Obj.make(TableType, { view: Ref.make(view) });
|
|
102
103
|
return new TableModel({ id: table.id, view, projection, ...props });
|
|
103
104
|
};
|