@dxos/react-ui-table 0.8.2-main.f081794 → 0.8.2-main.fbd8ed0

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 (91) hide show
  1. package/dist/lib/browser/{chunk-YWNRUF4J.mjs → chunk-PFFO5LYM.mjs} +7 -6
  2. package/dist/lib/browser/chunk-PFFO5LYM.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +323 -174
  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-VREQOYRW.cjs} +9 -8
  8. package/dist/lib/node/chunk-VREQOYRW.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +302 -156
  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-34JG3SDI.mjs} +7 -6
  15. package/dist/lib/node-esm/chunk-34JG3SDI.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +323 -174
  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 +2 -2
  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 +44 -43
  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-buttons.d.ts +0 -14
  56. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  57. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  58. package/dist/types/src/util/table.d.ts +2 -2
  59. package/dist/types/src/util/table.d.ts.map +1 -1
  60. package/dist/types/src/util/tag.d.ts +11 -0
  61. package/dist/types/src/util/tag.d.ts.map +1 -0
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +34 -33
  64. package/src/components/Table/CreateRefPanel.tsx +3 -3
  65. package/src/components/Table/DynamicTable.stories.tsx +2 -2
  66. package/src/components/Table/DynamicTable.tsx +4 -5
  67. package/src/components/Table/Table.stories.tsx +66 -20
  68. package/src/components/Table/Table.tsx +25 -28
  69. package/src/components/Table/relations.stories.tsx +37 -30
  70. package/src/components/TableCellEditor/FormCellEditor.tsx +94 -0
  71. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +8 -7
  72. package/src/components/TableCellEditor/TableCellEditor.tsx +45 -4
  73. package/src/components/TableToolbar/TableToolbar.tsx +10 -11
  74. package/src/components/testing.ts +7 -6
  75. package/src/model/table-model.test.ts +4 -3
  76. package/src/model/table-model.ts +17 -0
  77. package/src/model/table-presentation.test.ts +4 -3
  78. package/src/model/table-presentation.ts +54 -10
  79. package/src/types/table.ts +15 -7
  80. package/src/util/dynamic-table.ts +7 -4
  81. package/src/util/index.ts +1 -0
  82. package/src/util/schema.ts +36 -0
  83. package/src/util/table-buttons.ts +0 -23
  84. package/src/util/table.ts +12 -15
  85. package/src/util/tag.ts +33 -0
  86. package/dist/lib/browser/chunk-YWNRUF4J.mjs.map +0 -7
  87. package/dist/lib/node/chunk-TGG6OZ54.cjs.map +0 -7
  88. package/dist/lib/node-esm/chunk-KKLP54L3.mjs.map +0 -7
  89. package/dist/types/src/components/Table/RefPanel.d.ts +0 -9
  90. package/dist/types/src/components/Table/RefPanel.d.ts.map +0 -1
  91. package/src/components/Table/RefPanel.tsx +0 -52
@@ -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 { live, makeRef, type Live } 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, type TableRow } 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,12 +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) => {
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 });
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 });
38
44
  return live(TableType, { view: makeRef(view) });
39
45
  }, [schema]);
40
46
 
@@ -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<TableRow>({ 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: Live<T>[] = Array.from({ length: count }).map(() => objectGenerator.createObject());
58
- model.setRows(objects);
59
- }, [model]);
67
+ const objectGenerator = createAsyncGenerator(generator, schema, { optional: true, db: space?.db });
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,17 @@ 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
-
85
+ const { model: orgModel, presentation: orgPresentation } = useTestModel(DataType.Organization, 50);
77
86
  // 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);
87
+ const { model: contactModel, presentation: contactPresentation } = useTestModel(DataType.Person, 50);
80
88
 
81
- // TODO(burdon): Scrolling isn't working.
82
89
  return (
83
- <div className='grow grid grid-cols-2 divide-x divide-separator'>
90
+ <div className='is-full bs-full grid grid-cols-2 divide-x divide-separator'>
84
91
  <Table.Root>
85
- <Table.Main model={orgModel} presentation={orgPresentation} />
92
+ <Table.Main model={orgModel} presentation={orgPresentation} ignoreAttention />
86
93
  </Table.Root>
87
94
  <Table.Root>
88
- <Table.Main model={contactModel} presentation={contactPresentation} />
95
+ <Table.Main model={contactModel} presentation={contactPresentation} ignoreAttention />
89
96
  </Table.Root>
90
97
  </div>
91
98
  );
@@ -97,12 +104,12 @@ const meta: Meta<typeof DefaultStory> = {
97
104
  parameters: { translations },
98
105
  decorators: [
99
106
  withClientProvider({
100
- types: [TableType, ViewType, Testing.Org, Testing.Contact],
107
+ types: [TableType, ViewType, DataType.Organization, DataType.Person],
101
108
  createIdentity: true,
102
109
  createSpace: true,
103
110
  }),
104
111
  withTheme,
105
- withLayout({ fullscreen: true, tooltips: true }),
112
+ withLayout({ fullscreen: true }),
106
113
  ],
107
114
  };
108
115
 
@@ -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]);
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-consistent-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,9 +53,9 @@ 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 });
@@ -76,7 +76,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
76
76
  }
77
77
 
78
78
  return (
79
- <div className='flex w-[300px] border border-separator' style={{ height: defaultSizeRow }}>
79
+ <div className='flex w-[300px] border border-separator' style={{ height: defaultRowSize }}>
80
80
  <Grid.Root id='test' editing={editing}>
81
81
  <TableCellEditor model={model} onQuery={handleQuery} />
82
82
  </Grid.Root>
@@ -107,7 +107,7 @@ const meta: Meta<StoryProps> = {
107
107
  },
108
108
  }),
109
109
  withTheme,
110
- withLayout({ tooltips: true }),
110
+ withLayout(),
111
111
  ],
112
112
  };
113
113
 
@@ -120,6 +120,7 @@ export const Default: Story = {
120
120
  editing: {
121
121
  index: 'grid,0,3',
122
122
  initialContent: 'Test',
123
+ cellElement: null,
123
124
  },
124
125
  },
125
126
  };
@@ -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
  }
@@ -10,7 +10,6 @@ import {
10
10
  createGapSeparator,
11
11
  createMenuAction,
12
12
  type MenuAction,
13
- type MenuActionHandler,
14
13
  MenuProvider,
15
14
  ToolbarMenu,
16
15
  useMenuActions,
@@ -27,18 +26,18 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
27
26
  type TableToolbarState = Partial<{ viewDirty: boolean }>;
28
27
 
29
28
  export type TableToolbarProps = ThemedClassName<
30
- TableToolbarState & { onAction?: MenuActionHandler<TableToolbarAction>; attendableId?: string }
29
+ TableToolbarState & { onAction: (action: TableToolbarAction) => void; attendableId?: string }
31
30
  >;
32
31
 
33
- const createTableToolbarActions = (state: TableToolbarState) => {
34
- const add = createMenuAction<TableToolbarActionProperties>('add-row', {
35
- type: 'add-row',
32
+ const createTableToolbarActions = (state: TableToolbarState, onAction: (action: TableToolbarAction) => void) => {
33
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', () => onAction(add), {
34
+ type: 'add-row' as const,
36
35
  icon: 'ph--plus--regular',
37
36
  label: ['add row', { ns: translationKey }],
38
37
  testId: 'table.toolbar.add-row',
39
38
  });
40
- const save = createMenuAction<TableToolbarActionProperties>('save-view', {
41
- type: 'save-view',
39
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', () => onAction(save), {
40
+ type: 'save-view' as const,
42
41
  icon: 'ph--floppy-disk--regular',
43
42
  label: ['save view label', { ns: translationKey }],
44
43
  testId: 'table.toolbar.save-view',
@@ -46,8 +45,8 @@ const createTableToolbarActions = (state: TableToolbarState) => {
46
45
  hidden: !state.viewDirty,
47
46
  });
48
47
  const gap = createGapSeparator();
49
- const comment = createMenuAction('comment', {
50
- type: 'comment',
48
+ const comment = createMenuAction('comment', () => onAction(comment), {
49
+ type: 'comment' as const,
51
50
  icon: 'ph--chat-text--regular',
52
51
  label: ['create comment', { ns: translationKey }],
53
52
  testId: 'table.toolbar.comment',
@@ -61,11 +60,11 @@ const createTableToolbarActions = (state: TableToolbarState) => {
61
60
 
62
61
  export const TableToolbar = ({ classNames, viewDirty, attendableId, onAction }: TableToolbarProps) => {
63
62
  const state = useMemo(() => live<TableToolbarState>({ viewDirty }), []);
64
- const actionsCreator = useCallback(() => createTableToolbarActions(state), [state]);
63
+ const actionsCreator = useCallback(() => createTableToolbarActions(state, onAction), [state, onAction]);
65
64
  const menu = useMenuActions(actionsCreator);
66
65
 
67
66
  return (
68
- <MenuProvider {...menu} attendableId={attendableId} onAction={onAction as MenuActionHandler}>
67
+ <MenuProvider {...menu} attendableId={attendableId}>
69
68
  <ToolbarMenu classNames={classNames} />
70
69
  </MenuProvider>
71
70
  );
@@ -2,9 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Schema } from 'effect';
5
6
  import { useEffect } from 'react';
6
7
 
7
- import { setValue, toJsonSchema, S, TypeEnum, TypedObject, FormatEnum } from '@dxos/echo-schema';
8
+ import { setValue, toJsonSchema, TypeEnum, TypedObject, FormatEnum, ObjectId } from '@dxos/echo-schema';
8
9
  import { faker } from '@dxos/random';
9
10
  import { live, makeRef } from '@dxos/react-client/echo';
10
11
  import { createView, type ViewProjection } from '@dxos/schema';
@@ -12,11 +13,11 @@ import { createView, type ViewProjection } from '@dxos/schema';
12
13
  import { TableType } from '../types';
13
14
 
14
15
  export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
15
- id: S.String,
16
- name: S.optional(S.String),
17
- age: S.optional(S.Number),
18
- active: S.optional(S.Boolean),
19
- netWorth: S.optional(S.Number),
16
+ id: ObjectId,
17
+ name: Schema.optional(Schema.String),
18
+ age: Schema.optional(Schema.Number),
19
+ active: Schema.optional(Schema.Boolean),
20
+ netWorth: Schema.optional(Schema.Number),
20
21
  });
21
22
 
22
23
  export const createTable = (schema = TestSchema) => {
@@ -3,9 +3,10 @@
3
3
  //
4
4
 
5
5
  import { computed } from '@preact/signals-core';
6
+ import { Schema } from 'effect';
6
7
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7
8
 
8
- import { S, TypedObject } from '@dxos/echo-schema';
9
+ import { TypedObject } from '@dxos/echo-schema';
9
10
  import { updateCounter } from '@dxos/echo-schema/testing';
10
11
  import { registerSignalsRuntime } from '@dxos/echo-signals';
11
12
  import { createEchoSchema } from '@dxos/live-object/testing';
@@ -99,8 +100,8 @@ describe('TableModel', () => {
99
100
  });
100
101
 
101
102
  class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
102
- title: S.String,
103
- completed: S.Boolean,
103
+ title: Schema.String,
104
+ completed: Schema.Boolean,
104
105
  }) {}
105
106
 
106
107
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
@@ -21,6 +21,7 @@ import { type ViewType, type ViewProjection } from '@dxos/schema';
21
21
  import { type SelectionMode, SelectionModel } from './selection-model';
22
22
  import { TableSorting } from './table-sorting';
23
23
  import { touch } from '../util';
24
+ import { extractTagIds } from '../util/tag';
24
25
 
25
26
  // TODO(burdon): Use schema types.
26
27
  export type TableRow = Record<JsonProp, any> & { id: string };
@@ -348,6 +349,22 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
348
349
  break;
349
350
  }
350
351
 
352
+ case FormatEnum.SingleSelect: {
353
+ const ids = extractTagIds(value);
354
+ if (ids && ids.length > 0) {
355
+ setValue(this._rows.value[rowIdx], field.path, ids[0]);
356
+ }
357
+ break;
358
+ }
359
+
360
+ case FormatEnum.MultiSelect: {
361
+ const ids = extractTagIds(value);
362
+ if (ids) {
363
+ setValue(this._rows.value[rowIdx], field.path, ids);
364
+ }
365
+ break;
366
+ }
367
+
351
368
  default: {
352
369
  setValue(
353
370
  this._rows.value[rowIdx],
@@ -2,9 +2,10 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Schema } from 'effect';
5
6
  import { describe, expect, it, beforeEach } from 'vitest';
6
7
 
7
- import { S, TypedObject } from '@dxos/echo-schema';
8
+ import { TypedObject } from '@dxos/echo-schema';
8
9
  import { live, makeRef } from '@dxos/live-object';
9
10
  import { createEchoSchema } from '@dxos/live-object/testing';
10
11
  import { createView, ViewProjection } from '@dxos/schema';
@@ -89,8 +90,8 @@ describe('TablePresentation', () => {
89
90
  });
90
91
 
91
92
  class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
92
- title: S.String,
93
- completed: S.Boolean,
93
+ title: Schema.String,
94
+ completed: Schema.Boolean,
94
95
  }) {}
95
96
 
96
97
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {