@dxos/react-ui-table 0.8.2-main.f11618f → 0.8.2-staging.42af850

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 (87) hide show
  1. package/dist/lib/browser/chunk-U6LL2AVU.mjs +26 -0
  2. package/dist/lib/browser/chunk-U6LL2AVU.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1074 -828
  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-TGG6OZ54.cjs → chunk-CXDGXPM6.cjs} +12 -12
  8. package/dist/lib/node/chunk-CXDGXPM6.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +1080 -837
  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-KKLP54L3.mjs → chunk-EFSM52KQ.mjs} +10 -10
  15. package/dist/lib/node-esm/chunk-EFSM52KQ.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +1074 -828
  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/ColumnActionsMenu.d.ts.map +1 -1
  21. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/DynamicTable.d.ts +3 -4
  24. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  26. package/dist/types/src/components/Table/Table.d.ts +2 -0
  27. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/Table.stories.d.ts +1 -0
  29. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -0
  32. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -0
  33. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -0
  34. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  37. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +6 -3
  38. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  39. package/dist/types/src/components/testing.d.ts +39 -39
  40. package/dist/types/src/components/testing.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  42. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  43. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  44. package/dist/types/src/model/table-model.d.ts.map +1 -1
  45. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  46. package/dist/types/src/types/table.d.ts +46 -45
  47. package/dist/types/src/types/table.d.ts.map +1 -1
  48. package/dist/types/src/util/dynamic-table.d.ts +4 -3
  49. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  50. package/dist/types/src/util/index.d.ts +1 -0
  51. package/dist/types/src/util/index.d.ts.map +1 -1
  52. package/dist/types/src/util/schema.d.ts +12 -0
  53. package/dist/types/src/util/schema.d.ts.map +1 -0
  54. package/dist/types/src/util/signals.d.ts.map +1 -1
  55. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  56. package/dist/types/src/util/table.d.ts +2 -2
  57. package/dist/types/src/util/table.d.ts.map +1 -1
  58. package/dist/types/src/util/tag.d.ts.map +1 -1
  59. package/dist/types/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +41 -34
  61. package/src/components/Table/CreateRefPanel.tsx +3 -3
  62. package/src/components/Table/DynamicTable.stories.tsx +2 -2
  63. package/src/components/Table/DynamicTable.tsx +4 -5
  64. package/src/components/Table/Table.stories.tsx +76 -42
  65. package/src/components/Table/Table.tsx +8 -5
  66. package/src/components/Table/relations.stories.tsx +17 -20
  67. package/src/components/TableCellEditor/FormCellEditor.tsx +94 -0
  68. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +10 -8
  69. package/src/components/TableCellEditor/TableCellEditor.tsx +45 -4
  70. package/src/components/TableToolbar/TableToolbar.tsx +63 -33
  71. package/src/components/testing.ts +7 -6
  72. package/src/hooks/useTableModel.ts +4 -2
  73. package/src/model/selection-model.ts +1 -1
  74. package/src/model/table-model.test.ts +4 -3
  75. package/src/model/table-model.ts +1 -1
  76. package/src/model/table-presentation.test.ts +4 -3
  77. package/src/model/table-presentation.ts +53 -7
  78. package/src/types/table.ts +16 -11
  79. package/src/util/dynamic-table.ts +7 -4
  80. package/src/util/index.ts +1 -0
  81. package/src/util/schema.ts +36 -0
  82. package/src/util/table-buttons.ts +1 -1
  83. package/src/util/table.ts +11 -10
  84. package/dist/lib/browser/chunk-YWNRUF4J.mjs +0 -26
  85. package/dist/lib/browser/chunk-YWNRUF4J.mjs.map +0 -7
  86. package/dist/lib/node/chunk-TGG6OZ54.cjs.map +0 -7
  87. package/dist/lib/node-esm/chunk-KKLP54L3.mjs.map +0 -7
@@ -5,9 +5,17 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type StoryObj, type Meta } from '@storybook/react';
8
+ import { Schema } from 'effect';
8
9
  import React, { useCallback, useMemo, useRef } from 'react';
9
10
 
10
- import { FormatEnum, ImmutableSchema } from '@dxos/echo-schema';
11
+ import {
12
+ assertEchoSchema,
13
+ FormatEnum,
14
+ isMutable,
15
+ toJsonSchema,
16
+ EchoObject,
17
+ GeneratorAnnotation,
18
+ } from '@dxos/echo-schema';
11
19
  import { invariant } from '@dxos/invariant';
12
20
  import { useGlobalFilteredObjects } from '@dxos/plugin-search';
13
21
  import { faker } from '@dxos/random';
@@ -28,8 +36,7 @@ import { TableType } from '../../types';
28
36
  import { initializeTable } from '../../util';
29
37
  import { TableToolbar } from '../TableToolbar';
30
38
 
31
- // NOTE(ZaymonFC): We rely on this seed being 0 in the smoke tests.
32
- faker.seed(0);
39
+ faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
33
40
 
34
41
  /**
35
42
  * Custom hook to create and manage a test table model for storybook demonstrations.
@@ -39,14 +46,14 @@ const useTestTableModel = () => {
39
46
  const client = useClient();
40
47
  const { space } = useClientProvider();
41
48
 
42
- const filter = useMemo(() => Filter.schema(TableType), []);
49
+ const filter = useMemo(() => Filter.type(TableType), []);
43
50
  const tables = useQuery(space, filter);
44
51
  const table = useMemo(() => tables.at(0), [tables]);
45
52
  const schema = useSchema(client, space, table?.view?.target?.query.typename);
46
53
 
47
54
  const projection = useMemo(() => {
48
55
  if (schema && table?.view?.target) {
49
- return new ViewProjection(schema.jsonSchema, table.view.target);
56
+ return new ViewProjection(toJsonSchema(schema), table.view.target);
50
57
  }
51
58
  }, [schema, table?.view?.target]);
52
59
 
@@ -54,12 +61,12 @@ const useTestTableModel = () => {
54
61
  () => ({
55
62
  selection: { enabled: true, mode: 'multiple' as const },
56
63
  dataEditable: true,
57
- schemaEditable: !(schema instanceof ImmutableSchema),
64
+ schemaEditable: schema && isMutable(schema),
58
65
  }),
59
66
  [schema],
60
67
  );
61
68
 
62
- const objects = useQuery(space, schema ? Filter.schema(schema) : Filter.nothing());
69
+ const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
63
70
  const filteredObjects = useGlobalFilteredObjects(objects);
64
71
 
65
72
  const tableRef = useRef<TableController>(null);
@@ -107,32 +114,16 @@ const useTestTableModel = () => {
107
114
  onRowOrderChange: handleRowOrderChange,
108
115
  });
109
116
 
117
+ const handleSaveView = useCallback(() => {
118
+ model?.saveView();
119
+ }, [model]);
120
+
110
121
  const presentation = useMemo(() => {
111
122
  if (model) {
112
123
  return new TablePresentation(model);
113
124
  }
114
125
  }, [model]);
115
126
 
116
- const handleToolbarAction = useCallback(
117
- (action: { type: string }) => {
118
- switch (action.type) {
119
- case 'on-thread-create': {
120
- console.log('Thread creation triggered');
121
- break;
122
- }
123
- case 'add-row': {
124
- handleInsertRow();
125
- break;
126
- }
127
- case 'save-view': {
128
- model?.saveView();
129
- break;
130
- }
131
- }
132
- },
133
- [table, model],
134
- );
135
-
136
127
  return {
137
128
  schema,
138
129
  table,
@@ -140,7 +131,8 @@ const useTestTableModel = () => {
140
131
  model,
141
132
  presentation,
142
133
  space,
143
- handleToolbarAction,
134
+ handleInsertRow,
135
+ handleSaveView,
144
136
  handleDeleteRows,
145
137
  handleDeleteColumn,
146
138
  };
@@ -151,11 +143,10 @@ const StoryViewEditor = () => {
151
143
 
152
144
  const handleTypenameChanged = useCallback(
153
145
  (typename: string) => {
154
- if (table?.view?.target) {
155
- invariant(schema);
156
- schema.mutable.updateTypename(typename);
157
- table.view.target.query.typename = typename;
158
- }
146
+ invariant(schema);
147
+ invariant(table?.view?.target);
148
+ assertEchoSchema(schema).updateTypename(typename);
149
+ table.view.target.query.typename = typename;
159
150
  },
160
151
  [schema, table?.view?.target],
161
152
  );
@@ -180,7 +171,7 @@ const StoryViewEditor = () => {
180
171
  //
181
172
 
182
173
  const DefaultStory = () => {
183
- const { schema, table, tableRef, model, presentation, handleToolbarAction } = useTestTableModel();
174
+ const { schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView } = useTestTableModel();
184
175
 
185
176
  if (!schema || !table) {
186
177
  return <div />;
@@ -189,9 +180,9 @@ const DefaultStory = () => {
189
180
  return (
190
181
  <div className='grow grid grid-cols-[1fr_350px]'>
191
182
  <div className='grid grid-rows-[min-content_1fr] min-bs-0 overflow-hidden'>
192
- <TableToolbar classNames='border-be border-separator' onAction={handleToolbarAction} />
183
+ <TableToolbar classNames='border-be border-separator' onAdd={handleInsertRow} onSave={handleSaveView} />
193
184
  <Table.Root>
194
- <Table.Main ref={tableRef} model={model} presentation={presentation} ignoreAttention />
185
+ <Table.Main ref={tableRef} model={model} presentation={presentation} schema={schema} ignoreAttention />
195
186
  </Table.Root>
196
187
  </div>
197
188
  <div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
@@ -233,8 +224,8 @@ const meta: Meta<StoryProps> = {
233
224
  });
234
225
  },
235
226
  }),
227
+ withLayout({ fullscreen: true }),
236
228
  withTheme,
237
- withLayout({ fullscreen: true, tooltips: true }),
238
229
  ],
239
230
  };
240
231
 
@@ -252,17 +243,60 @@ export const StaticSchema: StoryObj = {
252
243
  createSpace: true,
253
244
  onSpaceCreated: async ({ client, space }) => {
254
245
  const table = space.db.add(live(TableType, {}));
255
- await initializeTable({ client, space, table, typename: Testing.Contact.typename });
246
+ await initializeTable({ client, space, table, typename: Testing.Organization.typename });
247
+
248
+ const factory = createObjectFactory(space.db, faker as any);
249
+ await factory([
250
+ // { type: Testing.Contact, count: 10 },
251
+ // { type: Testing.Organization, count: 1 },
252
+ { type: ContactWithArrayOfEmails, count: 10 },
253
+ ]);
254
+ },
255
+ }),
256
+ withLayout({ fullscreen: true }),
257
+ withTheme,
258
+ ],
259
+ };
260
+
261
+ const ContactWithArrayOfEmails = Schema.Struct({
262
+ name: Schema.String.pipe(GeneratorAnnotation.set('person.fullName')),
263
+ emails: Schema.optional(
264
+ Schema.Array(
265
+ Schema.Struct({
266
+ value: Schema.String,
267
+ label: Schema.String.pipe(Schema.optional),
268
+ }),
269
+ ),
270
+ ),
271
+ }).pipe(
272
+ EchoObject({
273
+ typename: 'dxos.org/type/ContactWithArrayOfEmails',
274
+ version: '0.1.0',
275
+ }),
276
+ );
277
+
278
+ export const ArrayOfObjects: StoryObj = {
279
+ render: DefaultStory,
280
+ parameters: { translations },
281
+ decorators: [
282
+ withClientProvider({
283
+ types: [TableType, ViewType, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
284
+ createIdentity: true,
285
+ createSpace: true,
286
+ onSpaceCreated: async ({ client, space }) => {
287
+ const table = space.db.add(live(TableType, {}));
288
+ await initializeTable({ client, space, table, typename: ContactWithArrayOfEmails.typename });
256
289
 
257
290
  const factory = createObjectFactory(space.db, faker as any);
258
291
  await factory([
259
- { type: Testing.Contact, count: 10 },
260
- { type: Testing.Organization, count: 1 },
292
+ // { type: Testing.Contact, count: 10 },
293
+ // { type: Testing.Organization, count: 1 },
294
+ { type: ContactWithArrayOfEmails, count: 10 },
261
295
  ]);
262
296
  },
263
297
  }),
298
+ withLayout({ fullscreen: true }),
264
299
  withTheme,
265
- withLayout({ fullscreen: true, tooltips: true }),
266
300
  ],
267
301
  };
268
302
 
@@ -317,7 +351,7 @@ export const Tags: Meta<StoryProps> = {
317
351
  });
318
352
  },
319
353
  }),
354
+ withLayout({ fullscreen: true }),
320
355
  withTheme,
321
- withLayout({ fullscreen: true, tooltips: true }),
322
356
  ],
323
357
  };
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { type Schema } from 'effect/Schema';
5
6
  import React, {
6
7
  type MouseEvent,
7
8
  type PropsWithChildren,
@@ -40,7 +41,7 @@ import { RowActionsMenu } from './RowActionsMenu';
40
41
  import { ModalController, type TableModel, type TablePresentation } from '../../model';
41
42
  import { translationKey } from '../../translations';
42
43
  import { tableButtons, tableControls } from '../../util';
43
- import { createOption, TableCellEditor, type TableCellEditorProps } from '../TableCellEditor';
44
+ import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
44
45
 
45
46
  //
46
47
  // Table.Root
@@ -75,13 +76,14 @@ export type TableController = {
75
76
  export type TableMainProps = {
76
77
  model?: TableModel;
77
78
  presentation?: TablePresentation;
79
+ schema?: Schema.AnyNoContext;
78
80
  // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
79
81
  ignoreAttention?: boolean;
80
82
  onRowClick?: (row: any) => void;
81
83
  };
82
84
 
83
85
  const TableMain = forwardRef<TableController, TableMainProps>(
84
- ({ model, presentation, ignoreAttention, onRowClick }, forwardedRef) => {
86
+ ({ model, presentation, ignoreAttention, schema, onRowClick }, forwardedRef) => {
85
87
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
86
88
  const { hasAttention } = useAttention(model?.id ?? 'table');
87
89
  const { t } = useTranslation(translationKey);
@@ -307,7 +309,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
307
309
  invariant(space);
308
310
  const schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
309
311
  if (schema) {
310
- const { objects } = await space.db.query(Filter.schema(schema)).run();
312
+ const { objects } = await space.db.query(Filter.type(schema)).run();
311
313
  const options = objects
312
314
  .map((obj) => {
313
315
  const value = getValue(obj, field.referencePath!);
@@ -343,15 +345,16 @@ const TableMain = forwardRef<TableController, TableMainProps>(
343
345
 
344
346
  return (
345
347
  <Grid.Root id={model.id ?? 'table-grid'}>
346
- <TableCellEditor
348
+ <TableValueEditor
347
349
  model={model}
348
350
  modals={modals}
351
+ schema={schema}
349
352
  onEnter={handleEnter}
350
353
  onFocus={handleFocus}
351
354
  onQuery={handleQuery}
352
355
  />
353
356
  <Grid.Content
354
- className={mx('[--dx-grid-base:var(--surface-bg)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
357
+ className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
355
358
  frozen={frozen}
356
359
  // getCells={getCells}
357
360
  columns={model.columnMeta.value}
@@ -5,14 +5,17 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type StoryObj, type Meta } from '@storybook/react';
8
+ import { type Schema } from 'effect';
9
+ import { SchemaAST } from 'effect';
8
10
  import React, { useEffect, useMemo } from 'react';
9
11
 
10
- import { AST, type BaseObject, ImmutableSchema, type BaseSchema, type HasId } from '@dxos/echo-schema';
12
+ import { type BaseObject, getSchemaTypename, type HasId, toJsonSchema } from '@dxos/echo-schema';
11
13
  import { getAnnotation } from '@dxos/effect';
14
+ import { invariant } from '@dxos/invariant';
12
15
  import { faker } from '@dxos/random';
13
16
  import { live, makeRef } from '@dxos/react-client/echo';
14
17
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
15
- import { Contact, createView, Organization, ViewProjection, ViewType } from '@dxos/schema';
18
+ import { DataType, createView, ViewProjection, ViewType } from '@dxos/schema';
16
19
  import { createAsyncGenerator, type ValueGenerator } from '@dxos/schema/testing';
17
20
  import { withLayout, withTheme } from '@dxos/storybook-utils';
18
21
 
@@ -29,14 +32,15 @@ const generator: ValueGenerator = faker as any;
29
32
  // TODO(burdon): Mutable and immutable views.
30
33
  // TODO(burdon): Reconcile schemas types and utils (see API PR).
31
34
  // TODO(burdon): Base type for T (with id); see ECHO API PR?
32
- const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count: number) => {
35
+ const useTestModel = <T extends BaseObject & HasId>(schema: Schema.Schema<T>, count: number) => {
33
36
  const { space } = useClientProvider();
34
37
 
38
+ const jsonSchema = useMemo(() => toJsonSchema(schema), [schema]);
35
39
  const table = useMemo(() => {
36
- // const { typename } = pipe(schema.ast, AST.getAnnotation<TypeAnnotation>(TypeAnnotationId), Option.getOrThrow);
37
- const typename = schema.typename;
38
- const name = getAnnotation<string>(AST.TitleAnnotationId)(schema.ast) ?? typename;
39
- const view = createView({ name, typename, jsonSchema: schema.jsonSchema });
40
+ const typename = getSchemaTypename(schema);
41
+ invariant(typename);
42
+ const name = getAnnotation<string>(SchemaAST.TitleAnnotationId)(schema.ast) ?? typename;
43
+ const view = createView({ name, typename, jsonSchema });
40
44
  return live(TableType, { view: makeRef(view) });
41
45
  }, [schema]);
42
46
 
@@ -46,7 +50,7 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
46
50
  }
47
51
 
48
52
  // TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
49
- return new ViewProjection(schema.jsonSchema, table.view.target);
53
+ return new ViewProjection(jsonSchema, table.view.target);
50
54
  }, [schema, table]);
51
55
 
52
56
  const features = useMemo<TableFeatures>(
@@ -60,14 +64,12 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
60
64
  return;
61
65
  }
62
66
 
63
- const objectGenerator = createAsyncGenerator(generator, schema, { optional: true, db: space?.db });
67
+ const objectGenerator = createAsyncGenerator(generator, schema, { db: space?.db, force: true });
64
68
  void objectGenerator.createObjects(count).then((objects) => {
65
69
  model.setRows(objects);
66
70
  });
67
71
  }, [model, space]);
68
72
 
69
- console.log(projection?.getFieldProjections());
70
-
71
73
  const presentation = useMemo(() => {
72
74
  if (!model) {
73
75
  return;
@@ -80,13 +82,8 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
80
82
  };
81
83
 
82
84
  const DefaultStory = () => {
83
- // TODO(burdon): Remove need for ImmutableSchema wrapper at API-level.
84
- const orgSchema = useMemo(() => new ImmutableSchema(Organization), []);
85
- const { model: orgModel, presentation: orgPresentation } = useTestModel<Organization>(orgSchema, 50);
86
-
87
- // TODO(burdon): Generate links with references.
88
- const contactSchema = useMemo(() => new ImmutableSchema(Contact), []);
89
- const { model: contactModel, presentation: contactPresentation } = useTestModel<Contact>(contactSchema, 50);
85
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
86
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
90
87
 
91
88
  return (
92
89
  <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
@@ -106,12 +103,12 @@ const meta: Meta<typeof DefaultStory> = {
106
103
  parameters: { translations },
107
104
  decorators: [
108
105
  withClientProvider({
109
- types: [TableType, ViewType, Organization, Contact],
106
+ types: [TableType, ViewType, DataType.Organization, DataType.Person],
110
107
  createIdentity: true,
111
108
  createSpace: true,
112
109
  }),
113
110
  withTheme,
114
- withLayout({ fullscreen: true, tooltips: true }),
111
+ withLayout({ fullscreen: true }),
115
112
  ],
116
113
  };
117
114
 
@@ -0,0 +1,94 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Schema } from 'effect';
6
+ import React, { useCallback, useEffect, useMemo, useRef } from 'react';
7
+
8
+ import { getSnapshot } from '@dxos/echo-schema';
9
+ import { invariant } from '@dxos/invariant';
10
+ import { Popover } from '@dxos/react-ui';
11
+ import { Form } from '@dxos/react-ui-form';
12
+ import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
13
+ import { type FieldProjection } from '@dxos/schema';
14
+ import { getDeep, setDeep } from '@dxos/util';
15
+
16
+ import { type TableModel } from '../../model';
17
+ import { narrowSchema } from '../../util';
18
+
19
+ type FormCellEditorProps = {
20
+ fieldProjection: FieldProjection;
21
+ model?: TableModel;
22
+ schema?: Schema.Schema.AnyNoContext;
23
+ __gridScope: any;
24
+ };
25
+
26
+ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }: FormCellEditorProps) => {
27
+ const { editing, setEditing } = useGridContext('ArrayEditor', __gridScope);
28
+ const cellRef = useRef<HTMLButtonElement | null>(null);
29
+
30
+ useEffect(() => {
31
+ if (editing && editing.cellElement) {
32
+ cellRef.current = editing.cellElement as HTMLButtonElement;
33
+ }
34
+ }, [editing?.cellElement]);
35
+
36
+ const narrowedSchema = useMemo(() => {
37
+ if (!schema) {
38
+ return undefined;
39
+ }
40
+ return narrowSchema(schema, [fieldProjection.field.path]);
41
+ }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
42
+
43
+ const originalRow = useMemo(() => {
44
+ if (model && editing) {
45
+ const cell = parseCellIndex(editing.index);
46
+ const row = model.getRowAt(cell.row);
47
+ invariant(row);
48
+
49
+ return row;
50
+ }
51
+
52
+ return undefined;
53
+ }, [model, editing]);
54
+
55
+ const formValues = useMemo(() => {
56
+ if (originalRow) {
57
+ // NOTE(ZaymonFC): Important to get a snapshot to eject from the live object.
58
+ return getSnapshot(originalRow);
59
+ }
60
+ }, [originalRow]);
61
+
62
+ const handleSave = useCallback(
63
+ (values: any) => {
64
+ const path = fieldProjection.field.path;
65
+ const value = getDeep(values, [path]);
66
+ setDeep(originalRow, [path], value);
67
+ setEditing(null);
68
+ },
69
+ [fieldProjection.field.path, originalRow],
70
+ );
71
+
72
+ const handleOpenChange = useCallback(
73
+ (nextOpen: boolean) => {
74
+ if (nextOpen === false) {
75
+ setEditing(null);
76
+ }
77
+ },
78
+ [setEditing],
79
+ );
80
+
81
+ if (!editing) {
82
+ return null;
83
+ }
84
+
85
+ return (
86
+ <Popover.Root open={editing !== null} onOpenChange={handleOpenChange}>
87
+ <Popover.VirtualTrigger virtualRef={cellRef} />
88
+ <Popover.Content tabIndex={-1} classNames='popover-card-width'>
89
+ <Popover.Arrow />
90
+ <Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
91
+ </Popover.Content>
92
+ </Popover.Root>
93
+ );
94
+ };
@@ -7,12 +7,12 @@ 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 { ImmutableSchema, type EchoSchema } from '@dxos/echo-schema';
10
+ import { type EchoSchema, isMutable } from '@dxos/echo-schema';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { faker } from '@dxos/random';
13
13
  import { Filter, useQuery, live } from '@dxos/react-client/echo';
14
14
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
15
- import { defaultSizeRow, Grid, type GridEditing } from '@dxos/react-ui-grid';
15
+ import { defaultRowSize, Grid, type GridEditing } from '@dxos/react-ui-grid';
16
16
  import { ViewProjection, ViewType } from '@dxos/schema';
17
17
  import { withLayout, withTheme } from '@dxos/storybook-utils';
18
18
 
@@ -31,7 +31,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
31
31
  const { space } = useClientProvider();
32
32
  invariant(space);
33
33
 
34
- const tables = useQuery(space, Filter.schema(TableType));
34
+ const tables = useQuery(space, Filter.type(TableType));
35
35
  const [table, setTable] = useState<TableType>();
36
36
  const [schema, setSchema] = useState<EchoSchema>();
37
37
  useEffect(() => {
@@ -53,15 +53,16 @@ const DefaultStory = ({ editing }: StoryProps) => {
53
53
  () => ({
54
54
  selection: { enabled: true, mode: 'multiple' },
55
55
  dataEditable: true,
56
- schemaEditable: !(schema instanceof ImmutableSchema),
56
+ schemaEditable: schema && isMutable(schema),
57
57
  }),
58
- [],
58
+ [schema],
59
59
  );
60
60
 
61
61
  const model = useTableModel({ table, projection, features });
62
62
 
63
63
  const handleQuery: TableCellEditorProps['onQuery'] = async ({ field }) => {
64
- const { objects } = await space.db.query(schema).run();
64
+ // TODO(dmaretskyi): If no schema query nothing
65
+ const { objects } = await space.db.query(schema ? Filter.type(schema) : Filter.everything()).run();
65
66
  return objects.map((obj) => {
66
67
  const label = obj[field.referencePath ?? 'id'];
67
68
  return {
@@ -76,7 +77,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
76
77
  }
77
78
 
78
79
  return (
79
- <div className='flex w-[300px] border border-separator' style={{ height: defaultSizeRow }}>
80
+ <div className='flex w-[300px] border border-separator' style={{ height: defaultRowSize }}>
80
81
  <Grid.Root id='test' editing={editing}>
81
82
  <TableCellEditor model={model} onQuery={handleQuery} />
82
83
  </Grid.Root>
@@ -107,7 +108,7 @@ const meta: Meta<StoryProps> = {
107
108
  },
108
109
  }),
109
110
  withTheme,
110
- withLayout({ tooltips: true }),
111
+ withLayout(),
111
112
  ],
112
113
  };
113
114
 
@@ -120,6 +121,7 @@ export const Default: Story = {
120
121
  editing: {
121
122
  index: 'grid,0,3',
122
123
  initialContent: 'Test',
124
+ cellElement: null,
123
125
  },
124
126
  },
125
127
  };
@@ -1,11 +1,12 @@
1
1
  //
2
- // Copyright 2024 DXOS.org
2
+ // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
+ import { type Schema } from 'effect/Schema';
6
7
  import React, { useCallback, useMemo, useRef } from 'react';
7
8
 
8
- import { FormatEnum } from '@dxos/echo-schema';
9
+ import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
9
10
  import { invariant } from '@dxos/invariant';
10
11
  import { type DxGrid } from '@dxos/lit-grid';
11
12
  import { useThemeContext } from '@dxos/react-ui';
@@ -26,6 +27,7 @@ import {
26
27
  import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
27
28
  import { type FieldProjection } from '@dxos/schema';
28
29
 
30
+ import { FormCellEditor } from './FormCellEditor';
29
31
  import { completion } from './extension';
30
32
  import { type TableModel, type ModalController } from '../../model';
31
33
 
@@ -43,11 +45,52 @@ export type QueryResult = Pick<Completion, 'label'> & { data: any };
43
45
  export type TableCellEditorProps = {
44
46
  model?: TableModel;
45
47
  modals?: ModalController;
48
+ schema?: Schema.AnyNoContext;
46
49
  onEnter?: (cell: DxGridPlanePosition) => void;
47
50
  onFocus?: DxGrid['refocus'];
48
51
  onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
49
52
  };
50
53
 
54
+ export const TableValueEditor = ({
55
+ model,
56
+ modals,
57
+ schema,
58
+ onEnter,
59
+ onFocus,
60
+ onQuery,
61
+ __gridScope,
62
+ }: GridScopedProps<TableCellEditorProps>) => {
63
+ const { editing } = useGridContext('TableValueEditor', __gridScope);
64
+
65
+ const fieldProjection = useMemo<FieldProjection | undefined>(() => {
66
+ if (!model || !editing) {
67
+ return;
68
+ }
69
+
70
+ const { col } = parseCellIndex(editing.index);
71
+ const field = model.projection.view.fields[col];
72
+ const fieldProjection = model.projection.getFieldProjection(field.id);
73
+ invariant(fieldProjection);
74
+ return fieldProjection;
75
+ }, [model, editing]);
76
+
77
+ if (fieldProjection?.props.type === TypeEnum.Array) {
78
+ return <FormCellEditor fieldProjection={fieldProjection} model={model} schema={schema} __gridScope={__gridScope} />;
79
+ }
80
+
81
+ // For all other types, use the existing cell editor
82
+ return (
83
+ <TableCellEditor
84
+ model={model}
85
+ modals={modals}
86
+ onEnter={onEnter}
87
+ onFocus={onFocus}
88
+ onQuery={onQuery}
89
+ __gridScope={__gridScope}
90
+ />
91
+ );
92
+ };
93
+
51
94
  export const TableCellEditor = ({
52
95
  model,
53
96
  modals,
@@ -261,11 +304,9 @@ export const TableCellEditor = ({
261
304
  }
262
305
  }
263
306
 
264
- // Return empty string if no value
265
307
  return '';
266
308
  }
267
309
 
268
- // Default behavior for other formats
269
310
  const value = model.getCellData(cell);
270
311
  return value !== undefined ? String(value) : '';
271
312
  }