@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.
Files changed (61) hide show
  1. package/dist/lib/browser/{chunk-U6LL2AVU.mjs → chunk-TL7CKQQV.mjs} +4 -3
  2. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +272 -145
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-CXDGXPM6.cjs → chunk-O2VPBNS5.cjs} +6 -5
  8. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +260 -135
  10. package/dist/lib/node/index.cjs.map +4 -4
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types/index.cjs +3 -3
  13. package/dist/lib/node/types/index.cjs.map +1 -1
  14. package/dist/lib/node-esm/{chunk-EFSM52KQ.mjs → chunk-GBT6GD4G.mjs} +4 -3
  15. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +272 -145
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/types/index.mjs +1 -1
  20. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  21. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/Table.d.ts +2 -0
  23. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +7 -0
  27. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -0
  28. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  29. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/testing.d.ts +2 -2
  32. package/dist/types/src/components/testing.d.ts.map +1 -1
  33. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  34. package/dist/types/src/model/table-model.d.ts +7 -0
  35. package/dist/types/src/model/table-model.d.ts.map +1 -1
  36. package/dist/types/src/types/table.d.ts +5 -4
  37. package/dist/types/src/types/table.d.ts.map +1 -1
  38. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  39. package/package.json +34 -33
  40. package/src/components/Table/ColumnSettings.tsx +11 -11
  41. package/src/components/Table/CreateRefPanel.tsx +5 -3
  42. package/src/components/Table/DynamicTable.stories.tsx +3 -2
  43. package/src/components/Table/Table.stories.tsx +19 -16
  44. package/src/components/Table/Table.tsx +13 -3
  45. package/src/components/Table/relations.stories.tsx +15 -10
  46. package/src/components/TableCellEditor/CellValidationMessage.tsx +38 -0
  47. package/src/components/TableCellEditor/FormCellEditor.tsx +3 -1
  48. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -1
  49. package/src/components/TableCellEditor/TableCellEditor.tsx +54 -22
  50. package/src/components/testing.ts +5 -4
  51. package/src/hooks/useTableModel.ts +8 -1
  52. package/src/model/table-model.test.ts +3 -2
  53. package/src/model/table-model.ts +88 -39
  54. package/src/model/table-presentation.test.ts +3 -2
  55. package/src/model/table-presentation.ts +1 -1
  56. package/src/types/table.ts +3 -2
  57. package/src/util/dynamic-table.ts +2 -2
  58. package/src/util/table.ts +11 -10
  59. package/dist/lib/browser/chunk-U6LL2AVU.mjs.map +0 -7
  60. package/dist/lib/node/chunk-CXDGXPM6.cjs.map +0 -7
  61. 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
- {state?.type === 'createRefPanel' && schema && (
75
- <Form schema={schema} values={state.initialValues ?? {}} onSave={handleSave} onCancel={handleCancel} />
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, live } from '@dxos/react-client/echo';
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
- live(Testing.Contact, {
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
- assertEchoSchema,
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-separator' onAdd={handleInsertRow} onSave={handleSaveView} />
179
+ <TableToolbar classNames='border-be border-subduedSeparator' onAdd={handleInsertRow} onSave={handleSaveView} />
184
180
  <Table.Root>
185
- <Table.Main ref={tableRef} model={model} presentation={presentation} schema={schema} ignoreAttention />
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(live(TableType, {}));
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(live(TableType, {}));
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(live(TableType, {}));
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(live(TableType, {}));
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
- const schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
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 { type BaseObject, getSchemaTypename, type HasId, toJsonSchema } from '@dxos/echo-schema';
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 { live, makeRef } from '@dxos/react-client/echo';
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 = <T extends BaseObject & HasId>(schema: Schema.Schema<T>, count: number) => {
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 live(TableType, { view: makeRef(view) });
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.view?.target) {
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
- }, [schema, table]);
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
- <Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
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(live(TableType, {}));
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
- model.setCellData(cell, value);
126
- onEnter?.(cell);
127
- onFocus?.();
128
- setEditing(null);
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
- const cell = parseCellIndex(editing.index);
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
- onEnter?.(cell);
159
- if (event && onFocus) {
160
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
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 <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} />;
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 { setValue, toJsonSchema, TypeEnum, TypedObject, FormatEnum, ObjectId } from '@dxos/echo-schema';
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, makeRef } from '@dxos/react-client/echo';
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 live(TableType, {
25
- view: makeRef(
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
- model?.setRows(rows);
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, makeRef } from '@dxos/react-client/echo';
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 = live(TableType, { view: makeRef(view) });
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
  };