@dxos/react-ui-table 0.8.3-main.672df60 → 0.8.3-staging.0fa589b
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 +272 -145
- 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 +260 -135
- 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 +272 -145
- 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/CreateRefPanel.d.ts.map +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/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.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 -33
- package/src/components/Table/ColumnSettings.tsx +11 -11
- package/src/components/Table/CreateRefPanel.tsx +5 -3
- package/src/components/Table/DynamicTable.stories.tsx +3 -2
- package/src/components/Table/Table.stories.tsx +19 -16
- 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/FormCellEditor.tsx +3 -1
- 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
|
@@ -71,9 +71,11 @@ export const CreateRefPanel = ({ model, modals, __gridScope }: GridScopedProps<C
|
|
|
71
71
|
<Popover.VirtualTrigger virtualRef={modals.trigger} />
|
|
72
72
|
<Popover.Portal>
|
|
73
73
|
<Popover.Content classNames='md:is-64' data-grid={gridId}>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
<Popover.Viewport>
|
|
75
|
+
{state?.type === 'createRefPanel' && schema && (
|
|
76
|
+
<Form schema={schema} values={state.initialValues ?? {}} onSave={handleSave} onCancel={handleCancel} />
|
|
77
|
+
)}
|
|
78
|
+
</Popover.Viewport>
|
|
77
79
|
<Popover.Arrow />
|
|
78
80
|
</Popover.Content>
|
|
79
81
|
</Popover.Portal>
|
|
@@ -7,10 +7,11 @@ import '@dxos-theme';
|
|
|
7
7
|
import { type StoryObj, type Meta } from '@storybook/react';
|
|
8
8
|
import React, { useMemo, useState } from 'react';
|
|
9
9
|
|
|
10
|
+
import { Obj } from '@dxos/echo';
|
|
10
11
|
import { FormatEnum, type JsonSchemaType } from '@dxos/echo-schema';
|
|
11
12
|
import { faker } from '@dxos/random';
|
|
12
13
|
import { useClient } from '@dxos/react-client';
|
|
13
|
-
import { Filter, useQuery, useSchema
|
|
14
|
+
import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
|
|
14
15
|
import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
|
|
15
16
|
import { type SchemaPropertyDefinition } from '@dxos/schema';
|
|
16
17
|
import { Testing } from '@dxos/schema/testing';
|
|
@@ -143,7 +144,7 @@ export const WithEchoSchema: StoryObj = {
|
|
|
143
144
|
onSpaceCreated: async ({ space }) => {
|
|
144
145
|
Array.from({ length: 10 }).forEach(() => {
|
|
145
146
|
space.db.add(
|
|
146
|
-
|
|
147
|
+
Obj.make(Testing.Contact, {
|
|
147
148
|
name: faker.person.fullName(),
|
|
148
149
|
email: faker.internet.email(),
|
|
149
150
|
}),
|
|
@@ -8,14 +8,8 @@ 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 {
|
|
12
|
-
|
|
13
|
-
FormatEnum,
|
|
14
|
-
isMutable,
|
|
15
|
-
toJsonSchema,
|
|
16
|
-
EchoObject,
|
|
17
|
-
GeneratorAnnotation,
|
|
18
|
-
} from '@dxos/echo-schema';
|
|
11
|
+
import { Obj, Type } from '@dxos/echo';
|
|
12
|
+
import { FormatEnum, isMutable, toJsonSchema, EchoObject, GeneratorAnnotation } from '@dxos/echo-schema';
|
|
19
13
|
import { invariant } from '@dxos/invariant';
|
|
20
14
|
import { useGlobalFilteredObjects } from '@dxos/plugin-search';
|
|
21
15
|
import { faker } from '@dxos/random';
|
|
@@ -131,6 +125,7 @@ const useTestTableModel = () => {
|
|
|
131
125
|
model,
|
|
132
126
|
presentation,
|
|
133
127
|
space,
|
|
128
|
+
client,
|
|
134
129
|
handleInsertRow,
|
|
135
130
|
handleSaveView,
|
|
136
131
|
handleDeleteRows,
|
|
@@ -144,8 +139,9 @@ const StoryViewEditor = () => {
|
|
|
144
139
|
const handleTypenameChanged = useCallback(
|
|
145
140
|
(typename: string) => {
|
|
146
141
|
invariant(schema);
|
|
142
|
+
invariant(Type.isMutable(schema));
|
|
143
|
+
schema.updateTypename(typename);
|
|
147
144
|
invariant(table?.view?.target);
|
|
148
|
-
assertEchoSchema(schema).updateTypename(typename);
|
|
149
145
|
table.view.target.query.typename = typename;
|
|
150
146
|
},
|
|
151
147
|
[schema, table?.view?.target],
|
|
@@ -171,7 +167,7 @@ const StoryViewEditor = () => {
|
|
|
171
167
|
//
|
|
172
168
|
|
|
173
169
|
const DefaultStory = () => {
|
|
174
|
-
const { schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView } = useTestTableModel();
|
|
170
|
+
const { schema, table, tableRef, model, presentation, client, handleInsertRow, handleSaveView } = useTestTableModel();
|
|
175
171
|
|
|
176
172
|
if (!schema || !table) {
|
|
177
173
|
return <div />;
|
|
@@ -180,9 +176,16 @@ const DefaultStory = () => {
|
|
|
180
176
|
return (
|
|
181
177
|
<div className='grow grid grid-cols-[1fr_350px]'>
|
|
182
178
|
<div className='grid grid-rows-[min-content_1fr] min-bs-0 overflow-hidden'>
|
|
183
|
-
<TableToolbar classNames='border-be border-
|
|
179
|
+
<TableToolbar classNames='border-be border-subduedSeparator' onAdd={handleInsertRow} onSave={handleSaveView} />
|
|
184
180
|
<Table.Root>
|
|
185
|
-
<Table.Main
|
|
181
|
+
<Table.Main
|
|
182
|
+
ref={tableRef}
|
|
183
|
+
model={model}
|
|
184
|
+
presentation={presentation}
|
|
185
|
+
schema={schema}
|
|
186
|
+
client={client}
|
|
187
|
+
ignoreAttention
|
|
188
|
+
/>
|
|
186
189
|
</Table.Root>
|
|
187
190
|
</div>
|
|
188
191
|
<div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
|
|
@@ -213,7 +216,7 @@ const meta: Meta<StoryProps> = {
|
|
|
213
216
|
createIdentity: true,
|
|
214
217
|
createSpace: true,
|
|
215
218
|
onSpaceCreated: async ({ client, space }) => {
|
|
216
|
-
const table = space.db.add(
|
|
219
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
217
220
|
const schema = await initializeTable({ client, space, table, initialRow: false });
|
|
218
221
|
Array.from({ length: 10 }).map(() => {
|
|
219
222
|
return space.db.add(
|
|
@@ -242,7 +245,7 @@ export const StaticSchema: StoryObj = {
|
|
|
242
245
|
createIdentity: true,
|
|
243
246
|
createSpace: true,
|
|
244
247
|
onSpaceCreated: async ({ client, space }) => {
|
|
245
|
-
const table = space.db.add(
|
|
248
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
246
249
|
await initializeTable({ client, space, table, typename: Testing.Organization.typename });
|
|
247
250
|
|
|
248
251
|
const factory = createObjectFactory(space.db, faker as any);
|
|
@@ -284,7 +287,7 @@ export const ArrayOfObjects: StoryObj = {
|
|
|
284
287
|
createIdentity: true,
|
|
285
288
|
createSpace: true,
|
|
286
289
|
onSpaceCreated: async ({ client, space }) => {
|
|
287
|
-
const table = space.db.add(
|
|
290
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
288
291
|
await initializeTable({ client, space, table, typename: ContactWithArrayOfEmails.typename });
|
|
289
292
|
|
|
290
293
|
const factory = createObjectFactory(space.db, faker as any);
|
|
@@ -337,7 +340,7 @@ export const Tags: Meta<StoryProps> = {
|
|
|
337
340
|
const [storedSchema] = await space.db.schemaRegistry.register([schema]);
|
|
338
341
|
|
|
339
342
|
// Initialize table.
|
|
340
|
-
const table = space.db.add(
|
|
343
|
+
const table = space.db.add(Obj.make(TableType, {}));
|
|
341
344
|
await initializeTable({ client, space, table, initialRow: false, typename });
|
|
342
345
|
|
|
343
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
|
+
};
|
|
@@ -87,7 +87,9 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
|
|
|
87
87
|
<Popover.VirtualTrigger virtualRef={cellRef} />
|
|
88
88
|
<Popover.Content tabIndex={-1} classNames='popover-card-width'>
|
|
89
89
|
<Popover.Arrow />
|
|
90
|
-
<
|
|
90
|
+
<Popover.Viewport>
|
|
91
|
+
<Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
|
|
92
|
+
</Popover.Viewport>
|
|
91
93
|
</Popover.Content>
|
|
92
94
|
</Popover.Root>
|
|
93
95
|
);
|
|
@@ -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
|
};
|