@dxos/react-ui-table 0.8.2-staging.7ac8446 → 0.8.3-main.672df60

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 (97) 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 +1115 -905
  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 +1113 -906
  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 +1115 -905
  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 +40 -40
  40. package/dist/types/src/components/testing.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useTableModel.d.ts +2 -2
  42. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  43. package/dist/types/src/model/modal-controller.d.ts +3 -3
  44. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  45. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  46. package/dist/types/src/model/table-model.d.ts.map +1 -1
  47. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  48. package/dist/types/src/types/table.d.ts +46 -45
  49. package/dist/types/src/types/table.d.ts.map +1 -1
  50. package/dist/types/src/util/dynamic-table.d.ts +4 -3
  51. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  52. package/dist/types/src/util/index.d.ts +1 -0
  53. package/dist/types/src/util/index.d.ts.map +1 -1
  54. package/dist/types/src/util/schema.d.ts +12 -0
  55. package/dist/types/src/util/schema.d.ts.map +1 -0
  56. package/dist/types/src/util/signals.d.ts.map +1 -1
  57. package/dist/types/src/util/table-buttons.d.ts +0 -14
  58. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  59. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  60. package/dist/types/src/util/table.d.ts +2 -2
  61. package/dist/types/src/util/table.d.ts.map +1 -1
  62. package/dist/types/src/util/tag.d.ts +11 -0
  63. package/dist/types/src/util/tag.d.ts.map +1 -0
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +41 -34
  66. package/src/components/Table/CreateRefPanel.tsx +5 -5
  67. package/src/components/Table/DynamicTable.stories.tsx +4 -4
  68. package/src/components/Table/DynamicTable.tsx +4 -5
  69. package/src/components/Table/Table.stories.tsx +85 -51
  70. package/src/components/Table/Table.tsx +25 -28
  71. package/src/components/Table/relations.stories.tsx +39 -33
  72. package/src/components/TableCellEditor/FormCellEditor.tsx +94 -0
  73. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -11
  74. package/src/components/TableCellEditor/TableCellEditor.tsx +45 -4
  75. package/src/components/TableToolbar/TableToolbar.tsx +65 -35
  76. package/src/components/testing.ts +10 -9
  77. package/src/hooks/useTableModel.ts +6 -4
  78. package/src/model/modal-controller.ts +3 -3
  79. package/src/model/selection-model.ts +1 -1
  80. package/src/model/table-model.test.ts +7 -6
  81. package/src/model/table-model.ts +20 -3
  82. package/src/model/table-presentation.test.ts +7 -6
  83. package/src/model/table-presentation.ts +54 -10
  84. package/src/types/table.ts +16 -11
  85. package/src/util/dynamic-table.ts +9 -6
  86. package/src/util/index.ts +1 -0
  87. package/src/util/schema.ts +36 -0
  88. package/src/util/table-buttons.ts +1 -24
  89. package/src/util/table.ts +13 -16
  90. package/src/util/tag.ts +33 -0
  91. package/dist/lib/browser/chunk-YWNRUF4J.mjs +0 -26
  92. package/dist/lib/browser/chunk-YWNRUF4J.mjs.map +0 -7
  93. package/dist/lib/node/chunk-TGG6OZ54.cjs.map +0 -7
  94. package/dist/lib/node-esm/chunk-KKLP54L3.mjs.map +0 -7
  95. package/dist/types/src/components/Table/RefPanel.d.ts +0 -9
  96. package/dist/types/src/components/Table/RefPanel.d.ts.map +0 -1
  97. package/src/components/Table/RefPanel.tsx +0 -52
@@ -5,14 +5,22 @@
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';
14
22
  import { useClient } from '@dxos/react-client';
15
- import { Filter, useQuery, useSchema, create } from '@dxos/react-client/echo';
23
+ import { Filter, useQuery, useSchema, live } from '@dxos/react-client/echo';
16
24
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
17
25
  import { ViewEditor } from '@dxos/react-ui-form';
18
26
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
@@ -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);
@@ -73,7 +80,7 @@ const useTestTableModel = () => {
73
80
 
74
81
  const handleInsertRow = useCallback(() => {
75
82
  if (space && schema) {
76
- space.db.add(create(schema, {}));
83
+ space.db.add(live(schema, {}));
77
84
  }
78
85
  }, [space, schema]);
79
86
 
@@ -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'>
@@ -215,26 +206,26 @@ type StoryProps = { rows?: number };
215
206
  const meta: Meta<StoryProps> = {
216
207
  title: 'ui/react-ui-table/Table',
217
208
  render: DefaultStory,
218
- parameters: { translations },
209
+ parameters: { translations, controls: { disable: true } },
219
210
  decorators: [
220
211
  withClientProvider({
221
212
  types: [TableType, ViewType],
222
213
  createIdentity: true,
223
214
  createSpace: true,
224
215
  onSpaceCreated: async ({ client, space }) => {
225
- const table = space.db.add(create(TableType, {}));
216
+ const table = space.db.add(live(TableType, {}));
226
217
  const schema = await initializeTable({ client, space, table, initialRow: false });
227
218
  Array.from({ length: 10 }).map(() => {
228
219
  return space.db.add(
229
- create(schema, {
220
+ live(schema, {
230
221
  name: faker.person.fullName(),
231
222
  }),
232
223
  );
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
 
@@ -247,22 +238,65 @@ export const StaticSchema: StoryObj = {
247
238
  parameters: { translations },
248
239
  decorators: [
249
240
  withClientProvider({
250
- types: [TableType, ViewType, Testing.Contact, Testing.Org],
241
+ types: [TableType, ViewType, Testing.Contact, Testing.Organization],
242
+ createIdentity: true,
243
+ createSpace: true,
244
+ onSpaceCreated: async ({ client, space }) => {
245
+ const table = space.db.add(live(TableType, {}));
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],
251
284
  createIdentity: true,
252
285
  createSpace: true,
253
286
  onSpaceCreated: async ({ client, space }) => {
254
- const table = space.db.add(create(TableType, {}));
255
- await initializeTable({ client, space, table, typename: Testing.Contact.typename });
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.Org, 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
 
@@ -303,13 +337,13 @@ export const Tags: Meta<StoryProps> = {
303
337
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
304
338
 
305
339
  // Initialize table.
306
- const table = space.db.add(create(TableType, {}));
340
+ const table = space.db.add(live(TableType, {}));
307
341
  await initializeTable({ client, space, table, initialRow: false, typename });
308
342
 
309
343
  // Populate.
310
344
  Array.from({ length: 10 }).map(() => {
311
345
  return space.db.add(
312
- create(storedSchema, {
346
+ live(storedSchema, {
313
347
  single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
314
348
  multiple: faker.helpers.randomSubset(selectOptionIds),
315
349
  }),
@@ -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,
@@ -19,26 +20,28 @@ import { invariant } from '@dxos/invariant';
19
20
  import { Filter } from '@dxos/react-client/echo';
20
21
  import { useTranslation } from '@dxos/react-ui';
21
22
  import { useAttention } from '@dxos/react-ui-attention';
22
- import { closestCell, type DxGridElement, type DxGridPosition, type GridContentProps, Grid } from '@dxos/react-ui-grid';
23
+ import {
24
+ closestCell,
25
+ type DxGridElement,
26
+ type DxGridPosition,
27
+ type GridContentProps,
28
+ Grid,
29
+ type DxGridPlane,
30
+ type DxGridPlaneRange,
31
+ gridSeparatorInlineEnd,
32
+ gridSeparatorBlockEnd,
33
+ } from '@dxos/react-ui-grid';
23
34
  import { mx } from '@dxos/react-ui-theme';
24
35
  import { isNotFalsy, safeParseInt } from '@dxos/util';
25
36
 
26
37
  import { ColumnActionsMenu } from './ColumnActionsMenu';
27
38
  import { ColumnSettings } from './ColumnSettings';
28
39
  import { CreateRefPanel } from './CreateRefPanel';
29
- import { RefPanel } from './RefPanel';
30
40
  import { RowActionsMenu } from './RowActionsMenu';
31
41
  import { ModalController, type TableModel, type TablePresentation } from '../../model';
32
42
  import { translationKey } from '../../translations';
33
43
  import { tableButtons, tableControls } from '../../util';
34
- import { createOption, TableCellEditor, type TableCellEditorProps } from '../TableCellEditor';
35
-
36
- // NOTE(Zan): These fragments add border to inline-end and block-end of the grid using pseudo-elements.
37
- // These are offset by 1px to avoid double borders in planks.
38
- const inlineEndLine =
39
- '[&>.dx-grid]:relative [&>.dx-grid]:after:absolute [&>.dx-grid]:after:inset-block-0 [&>.dx-grid]:after:-inline-end-px [&>.dx-grid]:after:is-px [&>.dx-grid]:after:bg-separator';
40
- const blockEndLine =
41
- '[&>.dx-grid]:before:absolute [&>.dx-grid]:before:inset-inline-0 [&>.dx-grid]:before:-block-end-px [&>.dx-grid]:before:bs-px [&>.dx-grid]:before:bg-separator';
44
+ import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
42
45
 
43
46
  //
44
47
  // Table.Root
@@ -73,13 +76,14 @@ export type TableController = {
73
76
  export type TableMainProps = {
74
77
  model?: TableModel;
75
78
  presentation?: TablePresentation;
79
+ schema?: Schema.AnyNoContext;
76
80
  // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
77
81
  ignoreAttention?: boolean;
78
82
  onRowClick?: (row: any) => void;
79
83
  };
80
84
 
81
85
  const TableMain = forwardRef<TableController, TableMainProps>(
82
- ({ model, presentation, ignoreAttention, onRowClick }, forwardedRef) => {
86
+ ({ model, presentation, ignoreAttention, schema, onRowClick }, forwardedRef) => {
83
87
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
84
88
  const { hasAttention } = useAttention(model?.id ?? 'table');
85
89
  const { t } = useTranslation(translationKey);
@@ -98,20 +102,17 @@ const TableMain = forwardRef<TableController, TableMainProps>(
98
102
  };
99
103
  }, [model]);
100
104
 
101
- // TODO(burdon): Replace useEffect below.
102
- // const getCells = useCallback<GridContentProps['getCells']>(
103
- // (range: DxGridRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
104
- // [presentation],
105
- // );
105
+ const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
106
+ (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
107
+ [presentation],
108
+ );
106
109
 
107
110
  useEffect(() => {
108
111
  if (!presentation || !dxGrid) {
109
112
  return;
110
113
  }
111
-
112
- // TODO(burdon): Pass to Grid.Content?
113
- dxGrid.getCells = (range, plane) => presentation.getCells(range, plane);
114
- }, [presentation, dxGrid]);
114
+ dxGrid.getCells = getCells;
115
+ }, [presentation, dxGrid, getCells]);
115
116
 
116
117
  /**
117
118
  * Provides an external controller that can be called to repaint the table.
@@ -181,10 +182,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
181
182
  modals.showColumnCreator();
182
183
  break;
183
184
  }
184
- case 'referencedCell': {
185
- modals.showReferencePanel(data.targetId, data.schemaId);
186
- break;
187
- }
188
185
  case 'sort': {
189
186
  model?.sorting?.toggleSort(data.fieldId);
190
187
  break;
@@ -312,7 +309,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
312
309
  invariant(space);
313
310
  const schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
314
311
  if (schema) {
315
- const { objects } = await space.db.query(Filter.schema(schema)).run();
312
+ const { objects } = await space.db.query(Filter.type(schema)).run();
316
313
  const options = objects
317
314
  .map((obj) => {
318
315
  const value = getValue(obj, field.referencePath!);
@@ -348,15 +345,16 @@ const TableMain = forwardRef<TableController, TableMainProps>(
348
345
 
349
346
  return (
350
347
  <Grid.Root id={model.id ?? 'table-grid'}>
351
- <TableCellEditor
348
+ <TableValueEditor
352
349
  model={model}
353
350
  modals={modals}
351
+ schema={schema}
354
352
  onEnter={handleEnter}
355
353
  onFocus={handleFocus}
356
354
  onQuery={handleQuery}
357
355
  />
358
356
  <Grid.Content
359
- className={mx('[--dx-grid-base:var(--surface-bg)]', inlineEndLine, blockEndLine)}
357
+ className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
360
358
  frozen={frozen}
361
359
  // getCells={getCells}
362
360
  columns={model.columnMeta.value}
@@ -373,7 +371,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
373
371
  <ColumnActionsMenu model={model} modals={modals} />
374
372
  <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
375
373
  <CreateRefPanel model={model} modals={modals} />
376
- <RefPanel model={model} modals={modals} />
377
374
  </Grid.Root>
378
375
  );
379
376
  },
@@ -5,20 +5,23 @@
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
- import { create, makeRef, type ReactiveObject } from '@dxos/react-client/echo';
14
- import { withClientProvider } from '@dxos/react-client/testing';
15
- import { createView, ViewProjection, ViewType } from '@dxos/schema';
16
- import { createGenerator, Testing, type ValueGenerator } from '@dxos/schema/testing';
16
+ import { live, makeRef } from '@dxos/react-client/echo';
17
+ import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
18
+ import { DataType, createView, ViewProjection, ViewType } from '@dxos/schema';
19
+ import { createAsyncGenerator, type ValueGenerator } from '@dxos/schema/testing';
17
20
  import { withLayout, withTheme } from '@dxos/storybook-utils';
18
21
 
19
22
  import { Table } from './Table';
20
23
  import { useTableModel } from '../../hooks';
21
- import { TablePresentation } from '../../model';
24
+ import { type TableFeatures, TablePresentation, type TableRow } from '../../model';
22
25
  import translations from '../../translations';
23
26
  import { TableType } from '../../types';
24
27
 
@@ -29,13 +32,16 @@ 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) => {
36
+ const { space } = useClientProvider();
37
+
38
+ const jsonSchema = useMemo(() => toJsonSchema(schema), [schema]);
33
39
  const table = useMemo(() => {
34
- // const { typename } = pipe(schema.ast, AST.getAnnotation<TypeAnnotation>(TypeAnnotationId), Option.getOrThrow);
35
- const typename = schema.typename;
36
- const name = getAnnotation<string>(AST.TitleAnnotationId)(schema.ast) ?? typename;
37
- const view = createView({ name, typename, jsonSchema: schema.jsonSchema });
38
- return create(TableType, { view: makeRef(view) });
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 });
44
+ return live(TableType, { view: makeRef(view) });
39
45
  }, [schema]);
40
46
 
41
47
  const projection = useMemo(() => {
@@ -44,19 +50,25 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
44
50
  }
45
51
 
46
52
  // TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
47
- return new ViewProjection(schema.jsonSchema, table.view.target);
53
+ return new ViewProjection(jsonSchema, table.view.target);
48
54
  }, [schema, table]);
49
55
 
50
- const model = useTableModel({ table, projection, rows: [] });
56
+ const features = useMemo<TableFeatures>(
57
+ () => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
58
+ [],
59
+ );
60
+
61
+ const model = useTableModel<TableRow>({ table, projection, rows: [], features });
51
62
  useEffect(() => {
52
- if (!model) {
63
+ if (!model || !space) {
53
64
  return;
54
65
  }
55
66
 
56
- const objectGenerator = createGenerator(generator, schema, { optional: true });
57
- const objects: ReactiveObject<T>[] = Array.from({ length: count }).map(() => objectGenerator.createObject());
58
- model.setRows(objects);
59
- }, [model]);
67
+ const objectGenerator = createAsyncGenerator(generator, schema, { db: space?.db, force: true });
68
+ void objectGenerator.createObjects(count).then((objects) => {
69
+ model.setRows(objects);
70
+ });
71
+ }, [model, space]);
60
72
 
61
73
  const presentation = useMemo(() => {
62
74
  if (!model) {
@@ -70,22 +82,16 @@ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count
70
82
  };
71
83
 
72
84
  const DefaultStory = () => {
73
- // TODO(burdon): Remove need for ImmutableSchema wrapper at API-level.
74
- const orgSchema = useMemo(() => new ImmutableSchema(Testing.Org), []);
75
- const { model: orgModel, presentation: orgPresentation } = useTestModel<Testing.Org>(orgSchema, 50);
76
-
77
- // TODO(burdon): Generate links with references.
78
- const contactSchema = useMemo(() => new ImmutableSchema(Testing.Contact), []);
79
- const { model: contactModel, presentation: contactPresentation } = useTestModel<Testing.Contact>(contactSchema, 50);
85
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
86
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
80
87
 
81
- // TODO(burdon): Scrolling isn't working.
82
88
  return (
83
- <div className='grow grid grid-cols-2 divide-x divide-separator'>
89
+ <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
84
90
  <Table.Root>
85
- <Table.Main model={orgModel} presentation={orgPresentation} />
91
+ <Table.Main model={orgModel} presentation={orgPresentation} ignoreAttention />
86
92
  </Table.Root>
87
93
  <Table.Root>
88
- <Table.Main model={contactModel} presentation={contactPresentation} />
94
+ <Table.Main model={contactModel} presentation={contactPresentation} ignoreAttention />
89
95
  </Table.Root>
90
96
  </div>
91
97
  );
@@ -94,15 +100,15 @@ const DefaultStory = () => {
94
100
  const meta: Meta<typeof DefaultStory> = {
95
101
  title: 'ui/react-ui-table/relations',
96
102
  render: DefaultStory,
97
- parameters: { translations },
103
+ parameters: { translations, controls: { disable: true } },
98
104
  decorators: [
99
105
  withClientProvider({
100
- types: [TableType, ViewType, Testing.Org, Testing.Contact],
106
+ types: [TableType, ViewType, DataType.Organization, DataType.Person],
101
107
  createIdentity: true,
102
108
  createSpace: true,
103
109
  }),
104
110
  withTheme,
105
- withLayout({ fullscreen: true, tooltips: true }),
111
+ withLayout({ fullscreen: true }),
106
112
  ],
107
113
  };
108
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
+ };