@dxos/react-ui-table 0.8.4-main.2c6827d → 0.8.4-main.3c1ae3b

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 (103) hide show
  1. package/dist/lib/browser/chunk-CJQ6T4LL.mjs +77 -0
  2. package/dist/lib/browser/chunk-CJQ6T4LL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-GS2UNGJK.mjs → chunk-V7A7ZA5E.mjs} +119 -111
  4. package/dist/lib/browser/chunk-V7A7ZA5E.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +162 -147
  6. package/dist/lib/browser/index.mjs.map +3 -3
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +15 -31
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/{chunk-TRR3C5NX.mjs → chunk-P73ALX3N.mjs} +119 -111
  12. package/dist/lib/node-esm/chunk-P73ALX3N.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-T6PXZQ6W.mjs +78 -0
  14. package/dist/lib/node-esm/chunk-T6PXZQ6W.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +162 -147
  16. package/dist/lib/node-esm/index.mjs.map +3 -3
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +15 -31
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +4 -5
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +3 -17
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +10 -6
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/Table.d.ts +8 -8
  32. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.stories.d.ts +70 -4
  34. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  36. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -11
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +3 -3
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/testing.d.ts +0 -2
  43. package/dist/types/src/components/testing.d.ts.map +1 -1
  44. package/dist/types/src/hooks/index.d.ts +2 -1
  45. package/dist/types/src/hooks/index.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
  47. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  48. package/dist/types/src/hooks/useTableModel.d.ts +4 -5
  49. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  50. package/dist/types/src/model/table-model.d.ts +6 -8
  51. package/dist/types/src/model/table-model.d.ts.map +1 -1
  52. package/dist/types/src/model/table-sorting.d.ts +7 -3
  53. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  54. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  55. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  56. package/dist/types/src/testing/useTestTableModel.d.ts +19 -15
  57. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  58. package/dist/types/src/translations.d.ts +2 -2
  59. package/dist/types/src/types/Table.d.ts +28 -0
  60. package/dist/types/src/types/Table.d.ts.map +1 -0
  61. package/dist/types/src/types/index.d.ts +1 -1
  62. package/dist/types/src/types/index.d.ts.map +1 -1
  63. package/dist/types/src/util/dynamic-table.d.ts +9 -9
  64. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +38 -38
  67. package/src/components/Table/ColumnActionsMenu.tsx +4 -1
  68. package/src/components/Table/ColumnSettings.tsx +2 -3
  69. package/src/components/Table/DynamicTable.stories.tsx +7 -9
  70. package/src/components/Table/DynamicTable.tsx +18 -10
  71. package/src/components/Table/Relations.stories.tsx +9 -18
  72. package/src/components/Table/Table.stories.tsx +52 -59
  73. package/src/components/Table/Table.tsx +317 -304
  74. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  75. package/src/components/TableCellEditor/FormCellEditor.tsx +101 -108
  76. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +11 -8
  77. package/src/components/TableCellEditor/TableCellEditor.tsx +29 -29
  78. package/src/components/TableToolbar/TableToolbar.tsx +1 -1
  79. package/src/components/testing.ts +3 -3
  80. package/src/hooks/index.ts +2 -1
  81. package/src/hooks/useProjectionModel.ts +19 -0
  82. package/src/hooks/useTableModel.ts +11 -13
  83. package/src/model/table-model.test.ts +4 -4
  84. package/src/model/table-model.ts +29 -38
  85. package/src/model/table-presentation.test.ts +4 -4
  86. package/src/model/table-presentation.ts +9 -9
  87. package/src/model/table-sorting.ts +21 -12
  88. package/src/playwright/TableManager.ts +5 -5
  89. package/src/playwright/smoke.spec.ts +1 -1
  90. package/src/testing/useTestTableModel.ts +31 -27
  91. package/src/translations.ts +2 -2
  92. package/src/types/Table.ts +90 -0
  93. package/src/types/index.ts +1 -1
  94. package/src/util/dynamic-table.ts +11 -16
  95. package/dist/lib/browser/chunk-2PFAPQL4.mjs +0 -66
  96. package/dist/lib/browser/chunk-2PFAPQL4.mjs.map +0 -7
  97. package/dist/lib/browser/chunk-GS2UNGJK.mjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-6POOFFYF.mjs +0 -67
  99. package/dist/lib/node-esm/chunk-6POOFFYF.mjs.map +0 -7
  100. package/dist/lib/node-esm/chunk-TRR3C5NX.mjs.map +0 -7
  101. package/dist/types/src/types/schema.d.ts +0 -28
  102. package/dist/types/src/types/schema.d.ts.map +0 -1
  103. package/src/types/schema.ts +0 -73
@@ -12,9 +12,9 @@ export type CellValidationMessageProps = {
12
12
  };
13
13
 
14
14
  export const CellValidationMessage = ({
15
+ __gridScope,
15
16
  validationError,
16
17
  variant = 'error',
17
- __gridScope,
18
18
  }: GridScopedProps<CellValidationMessageProps>) => {
19
19
  const { editing, editBox: box } = useGridContext('GridSheetCellEditor', __gridScope);
20
20
 
@@ -2,97 +2,96 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import * as Schema from 'effect/Schema';
5
+ import type * as Schema from 'effect/Schema';
6
6
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { Filter, Obj } from '@dxos/echo';
9
- import { FormatEnum, Ref, type TypeAnnotation, getValue } from '@dxos/echo/internal';
8
+ import { Filter, Obj, type Type } from '@dxos/echo';
9
+ import { Format, Ref, getValue } from '@dxos/echo/internal';
10
10
  import { invariant } from '@dxos/invariant';
11
11
  import { getSnapshot } from '@dxos/live-object';
12
- import { type Client } from '@dxos/react-client';
13
12
  import { getSpace } from '@dxos/react-client/echo';
14
- import { Popover } from '@dxos/react-ui';
15
- import { Form, type FormProps } from '@dxos/react-ui-form';
13
+ import { type Label, Popover } from '@dxos/react-ui';
14
+ import { Form, type FormRootProps, omitId } from '@dxos/react-ui-form';
16
15
  import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
17
16
  import { type FieldProjection } from '@dxos/schema';
18
17
  import { getDeep, isTruthy, setDeep } from '@dxos/util';
19
18
 
20
- import { type ModalController, type TableModel } from '../../model';
19
+ import { type ModalController, type TableModel, type TableRow } from '../../model';
21
20
  import { translationKey } from '../../translations';
22
21
  import { narrowSchema } from '../../util';
23
22
 
23
+ const createOptionLabel: Label = ['create new object label', { ns: translationKey }];
24
+
24
25
  export type OnCreateHandler = (schema: Schema.Schema.AnyNoContext, values: any) => Parameters<typeof Ref.make>[0];
25
26
 
26
- export type FormCellEditorProps = {
27
- fieldProjection: FieldProjection;
27
+ export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
28
+ __gridScope: any;
29
+ schema?: T;
28
30
  model?: TableModel;
29
- schema?: Schema.Schema.AnyNoContext;
31
+ fieldProjection: FieldProjection;
32
+ modals?: ModalController; // TODO(burdon): Not used. Remove?
30
33
  onSave?: () => void;
31
34
  onCreate?: OnCreateHandler;
32
- client?: Client;
33
- modals?: ModalController;
34
- __gridScope: any;
35
- } & Omit<FormProps<any>, 'values' | 'schema' | 'onCreate'>;
36
-
37
- const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
35
+ } & Omit<FormRootProps<any>, 'values' | 'schema' | 'onCreate'>;
38
36
 
39
- export const FormCellEditor = ({
40
- fieldProjection,
41
- model,
37
+ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
38
+ __gridScope,
42
39
  schema,
40
+ model,
41
+ fieldProjection,
43
42
  onSave,
44
- client,
45
- modals,
46
43
  onCreate,
47
- __gridScope,
48
44
  ...formProps
49
- }: FormCellEditorProps) => {
50
- const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
51
- const [editing, setLocalEditing] = useState(false);
45
+ }: FormCellEditorProps<T>) => {
52
46
  const anchorRef = useRef<HTMLButtonElement | null>(null);
47
+ const { id: _gridId, editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
48
+ const [editing, setLocalEditing] = useState(false);
49
+
50
+ useEffect(() => {
51
+ if (contextEditing && contextEditing.cellElement) {
52
+ anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
53
+ '.dx-grid__cell__content',
54
+ ) as HTMLButtonElement;
55
+ setLocalEditing(true);
56
+ } else {
57
+ anchorRef.current = null;
58
+ setLocalEditing(false);
59
+ }
60
+ }, [contextEditing]);
61
+
62
+ const narrowedSchema = useMemo<Schema.Schema.AnyNoContext | undefined>(() => {
63
+ if (!schema) {
64
+ return undefined;
65
+ }
66
+
67
+ return narrowSchema(schema, [fieldProjection.field.path]);
68
+ }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
53
69
 
54
70
  const getSchema = useCallback(
55
- (typeAnnotation: Pick<TypeAnnotation, 'typename'>) => {
71
+ ({ typename }: { typename: string }) => {
56
72
  const space = getSpace(model!.view);
57
73
  invariant(space);
58
-
59
- let schema;
60
- if (client) {
61
- schema = client.graph.schemaRegistry.getSchema(typeAnnotation.typename);
62
- }
63
- if (!schema) {
64
- schema = space.db.schemaRegistry.getSchema(typeAnnotation.typename);
65
- }
74
+ const schema = space.db.schemaRegistry.query({ typename, location: ['database', 'runtime'] }).runSync()[0];
66
75
  return { space, schema };
67
76
  },
68
- [client, model],
77
+ [model],
69
78
  );
70
79
 
71
- const handleQueryRefOptions = useCallback(
72
- async (typeAnnotation: TypeAnnotation) => {
73
- const { schema, space } = getSchema(typeAnnotation);
74
- if (model && schema && space) {
75
- const { objects } = await space.db.query(Filter.type(schema)).run();
76
- return objects
77
- .map((obj) => {
78
- return {
79
- dxn: Obj.getDXN(obj),
80
- label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
81
- };
82
- })
83
- .filter(isTruthy);
84
- }
80
+ const refSchema = useMemo(() => {
81
+ if (fieldProjection.props.format === Format.TypeFormat.Ref && fieldProjection.props.referenceSchema) {
82
+ const { schema } = getSchema({ typename: fieldProjection.props.referenceSchema });
83
+ return schema;
84
+ }
85
85
 
86
- return [];
87
- },
88
- [client, model],
89
- );
86
+ return null;
87
+ }, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
90
88
 
91
- const originalRow = useMemo(() => {
92
- if (model && contextEditing) {
93
- const cell = parseCellIndex(contextEditing.index);
89
+ const createSchema = useMemo(() => (refSchema ? omitId(refSchema) : null), [refSchema]);
94
90
 
91
+ const originalRow = useMemo<TableRow | undefined>(() => {
92
+ if (model && contextEditing) {
95
93
  // Check if this is a draft cell and get the appropriate row data
94
+ const cell = parseCellIndex(contextEditing.index);
96
95
  if (model.isDraftCell(cell)) {
97
96
  const draftRow = model.draftRows.value[cell.row];
98
97
  invariant(draftRow);
@@ -107,38 +106,6 @@ export const FormCellEditor = ({
107
106
  return undefined;
108
107
  }, [model, contextEditing]);
109
108
 
110
- const handleSave = useCallback(
111
- (values: any) => {
112
- const path = fieldProjection.field.path;
113
- const value = getDeep(values, [path]);
114
- setDeep(originalRow, [path], value);
115
- contextEditing?.cellElement?.focus();
116
- setEditing(null);
117
- setLocalEditing(false);
118
- onSave?.();
119
- },
120
- [fieldProjection.field.path, onSave, contextEditing, originalRow],
121
- );
122
-
123
- useEffect(() => {
124
- if (contextEditing && contextEditing.cellElement) {
125
- anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
126
- '.dx-grid__cell__content',
127
- ) as HTMLButtonElement;
128
- setLocalEditing(true);
129
- } else {
130
- anchorRef.current = null;
131
- setLocalEditing(false);
132
- }
133
- }, [contextEditing]);
134
-
135
- const narrowedSchema = useMemo(() => {
136
- if (!schema) {
137
- return undefined;
138
- }
139
- return narrowSchema(schema, [fieldProjection.field.path]);
140
- }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
141
-
142
109
  // NOTE: Important to get a snapshot to eject from the live object.
143
110
  const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
144
111
 
@@ -150,22 +117,21 @@ export const FormCellEditor = ({
150
117
  setLocalEditing(nextOpen);
151
118
  }, []);
152
119
 
153
- const refSchema = useMemo(() => {
154
- if (fieldProjection.props.format === FormatEnum.Ref && fieldProjection.props.referenceSchema) {
155
- const { schema: refSchema } = getSchema({
156
- typename: fieldProjection.props.referenceSchema,
157
- });
158
- return refSchema;
159
- }
160
- return null;
161
- }, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
162
-
163
- const createSchema = useMemo(() => {
164
- return refSchema ? Schema.omit<any, any, ['id']>('id')(refSchema) : null;
165
- }, [refSchema]);
120
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>(
121
+ (values) => {
122
+ const path = fieldProjection.field.path;
123
+ const value = getDeep(values, [path]);
124
+ setDeep(originalRow, [path], value);
125
+ contextEditing?.cellElement?.focus();
126
+ setEditing(null);
127
+ setLocalEditing(false);
128
+ onSave?.();
129
+ },
130
+ [fieldProjection.field.path, onSave, contextEditing, originalRow],
131
+ );
166
132
 
167
- const handleCreate = useCallback(
168
- (values: any) => {
133
+ const handleCreate = useCallback<NonNullable<FormRootProps<any>['onCreate']>>(
134
+ (values) => {
169
135
  if (refSchema && onCreate) {
170
136
  const objectWithId = onCreate(refSchema, values);
171
137
  if (objectWithId) {
@@ -182,6 +148,26 @@ export const FormCellEditor = ({
182
148
  [fieldProjection.field.path, onSave, contextEditing, originalRow, refSchema, onCreate],
183
149
  );
184
150
 
151
+ const handleQueryRefOptions = useCallback<NonNullable<FormRootProps<any>['onQueryRefOptions']>>(
152
+ async ({ typename }) => {
153
+ const { schema, space } = getSchema({ typename });
154
+ if (model && schema && space) {
155
+ const objects = await space.db.query(Filter.type(schema)).run();
156
+ return objects
157
+ .map((obj) => {
158
+ return {
159
+ dxn: Obj.getDXN(obj),
160
+ label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
161
+ };
162
+ })
163
+ .filter(isTruthy);
164
+ }
165
+
166
+ return [];
167
+ },
168
+ [model],
169
+ );
170
+
185
171
  if (!editing) {
186
172
  return null;
187
173
  }
@@ -193,21 +179,28 @@ export const FormCellEditor = ({
193
179
  <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
194
180
  <Popover.Arrow />
195
181
  <Popover.Viewport>
196
- <Form
182
+ <Form.Root
183
+ {...formProps}
197
184
  autoFocus
185
+ schema={narrowedSchema}
198
186
  values={formValues}
199
- schema={narrowedSchema as any}
200
187
  onSave={handleSave}
201
- {...formProps}
202
188
  onQueryRefOptions={handleQueryRefOptions}
203
189
  {...(createSchema && {
204
- onCreate: handleCreate,
205
190
  createSchema,
206
191
  createInitialValuePath: fieldProjection.field.referencePath,
207
192
  createOptionIcon: 'ph--plus--regular',
208
193
  createOptionLabel,
194
+ onCreate: handleCreate,
209
195
  })}
210
- />
196
+ >
197
+ <Form.Viewport>
198
+ <Form.Content>
199
+ <Form.FieldSet />
200
+ <Form.Actions />
201
+ </Form.Content>
202
+ </Form.Viewport>
203
+ </Form.Root>
211
204
  </Popover.Viewport>
212
205
  </Popover.Content>
213
206
  </Popover.Portal>
@@ -8,7 +8,7 @@ import React from 'react';
8
8
  import { Obj } from '@dxos/echo';
9
9
  import { faker } from '@dxos/random';
10
10
  import { withClientProvider } from '@dxos/react-client/testing';
11
- import { withTheme } from '@dxos/react-ui/testing';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
12
  import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
13
13
  import { View } from '@dxos/schema';
14
14
  import { Task } from '@dxos/types';
@@ -23,15 +23,16 @@ type StoryProps = {
23
23
  editing: GridEditing;
24
24
  };
25
25
 
26
+ // TODO(burdon): Broken layout.
26
27
  const DefaultStory = ({ editing }: StoryProps) => {
27
- const { model, view } = useTestTableModel();
28
+ const { model, table } = useTestTableModel();
28
29
 
29
- if (!model || !view) {
30
+ if (!model || !table) {
30
31
  return <div />;
31
32
  }
32
33
 
33
34
  return (
34
- <div className='flex is-[300px] border border-separator' style={{ height: defaultRowSize }}>
35
+ <div className='border border-separator' style={{ height: defaultRowSize }}>
35
36
  <Grid.Root id='test' editing={editing}>
36
37
  <TableCellEditor model={model} schema={Task.Task} />
37
38
  </Grid.Root>
@@ -45,13 +46,15 @@ const meta = {
45
46
  render: DefaultStory,
46
47
  decorators: [
47
48
  withTheme,
49
+ withLayout({ container: 'column' }),
48
50
  withClientProvider({
49
51
  types: [View.View, Task.Task, Table.Table],
50
52
  createIdentity: true,
51
53
  createSpace: true,
52
- onCreateSpace: async ({ client, space }) => {
53
- const { view } = await Table.makeView({ client, space, typename: Task.Task.typename });
54
- space.db.add(view);
54
+ onCreateSpace: async ({ space }) => {
55
+ const { view, jsonSchema } = await View.makeFromSpace({ space, typename: Task.Task.typename });
56
+ const table = Table.make({ view, jsonSchema });
57
+ space.db.add(table);
55
58
  Array.from({ length: 10 }).forEach(() => {
56
59
  space.db.add(
57
60
  Obj.make(Task.Task, {
@@ -65,8 +68,8 @@ const meta = {
65
68
  }),
66
69
  ],
67
70
  parameters: {
71
+ layout: 'fullscreen',
68
72
  translations,
69
- layout: 'centered',
70
73
  },
71
74
  } satisfies Meta<typeof DefaultStory>;
72
75
 
@@ -4,12 +4,11 @@
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
6
  import { EditorView } from '@codemirror/view';
7
- import type * as Schema from 'effect/Schema';
8
7
  import React, { useCallback, useMemo, useRef, useState } from 'react';
9
8
 
10
9
  import { debounce } from '@dxos/async';
11
- import type { Client } from '@dxos/client';
12
- import { FormatEnum, TypeEnum } from '@dxos/echo/internal';
10
+ import { type Type } from '@dxos/echo';
11
+ import { Format, TypeEnum } from '@dxos/echo/internal';
13
12
  import { invariant } from '@dxos/invariant';
14
13
  import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
15
14
  import {
@@ -30,34 +29,37 @@ import { type ModalController, type TableModel } from '../../model';
30
29
  import { CellValidationMessage } from './CellValidationMessage';
31
30
  import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
32
31
 
32
+ const editorSlots = {
33
+ scroll: {
34
+ className: '!plb-[--dx-grid-cell-editor-padding-block]',
35
+ },
36
+ };
37
+
33
38
  /**
34
39
  * Option to create new object/value.
35
40
  */
36
- export type QueryResult = Pick<Completion, 'label'> & { data: any };
41
+ export type QueryResult = Pick<Completion, 'label'> & {
42
+ data: any;
43
+ };
37
44
 
38
- export type TableCellEditorProps = {
45
+ export type TableCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
46
+ schema?: T;
39
47
  model?: TableModel;
40
48
  modals?: ModalController;
41
- schema?: Schema.Schema.AnyNoContext;
42
49
  onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
43
50
  onCreate?: OnCreateHandler;
44
51
  onSave?: () => void;
45
- client?: Client;
46
52
  };
47
53
 
48
- const adaptValidationMessage = (message: string | null) =>
49
- message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
50
-
51
- export const TableValueEditor = ({
54
+ export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
55
+ __gridScope,
56
+ schema,
52
57
  model,
53
58
  modals,
54
- schema,
55
59
  onFocus,
56
60
  onSave,
57
61
  onCreate,
58
- client,
59
- __gridScope,
60
- }: GridScopedProps<TableCellEditorProps>) => {
62
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
61
63
  const { editing } = useGridContext('TableValueEditor', __gridScope);
62
64
 
63
65
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -74,36 +76,33 @@ export const TableValueEditor = ({
74
76
 
75
77
  if (
76
78
  fieldProjection?.props.type === TypeEnum.Array ||
77
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
78
- fieldProjection?.props.format === FormatEnum.Ref
79
- // TODO(thure): Support `FormatEnum.MultiSelect`
79
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
80
+ fieldProjection?.props.format === Format.TypeFormat.Ref
81
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
80
82
  ) {
81
83
  return (
82
- <FormCellEditor
83
- fieldProjection={fieldProjection}
84
- model={model}
85
- schema={schema}
84
+ <FormCellEditor<T>
86
85
  __gridScope={__gridScope}
86
+ schema={schema}
87
+ model={model}
88
+ fieldProjection={fieldProjection}
89
+ modals={modals}
87
90
  onSave={onSave}
88
91
  onCreate={onCreate}
89
- client={client}
90
- modals={modals}
91
92
  />
92
93
  );
93
94
  }
94
95
 
95
- // For all other types, use the existing cell editor
96
+ // For all other types, use the existing cell editor.
96
97
  return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
97
98
  };
98
99
 
99
- const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
100
-
101
100
  export const TableCellEditor = ({
101
+ __gridScope,
102
102
  model,
103
103
  modals,
104
104
  onFocus,
105
105
  onSave,
106
- __gridScope,
107
106
  }: GridScopedProps<TableCellEditorProps>) => {
108
107
  const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
109
108
  const suppressNextBlur = useRef(false);
@@ -122,6 +121,7 @@ export const TableCellEditor = ({
122
121
  return fieldProjection;
123
122
  }, [model, editing]);
124
123
 
124
+ // TOOD(burdon): Attach to event handler?
125
125
  const handleEnter = useCallback(
126
126
  async (value: any) => {
127
127
  if (!model || !editing) {
@@ -138,7 +138,7 @@ export const TableCellEditor = ({
138
138
  setEditing(null);
139
139
  onSave?.();
140
140
  } else {
141
- setValidationError(adaptValidationMessage(validationResult.error));
141
+ setValidationError(validationResult.error);
142
142
  setValidationVariant('error');
143
143
  }
144
144
  },
@@ -58,7 +58,7 @@ const createTableToolbarActions = ({
58
58
  const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
59
59
  type: 'add-row' as const,
60
60
  icon: 'ph--plus--regular',
61
- label: ['add row', { ns: translationKey }],
61
+ label: ['add row label', { ns: translationKey }],
62
62
  testId: 'table.toolbar.add-row',
63
63
  });
64
64
  nodes.push(add);
@@ -5,13 +5,13 @@
5
5
  import * as Schema from 'effect/Schema';
6
6
  import { useEffect } from 'react';
7
7
 
8
- import { FormatEnum, ObjectId, TypeEnum, TypedObject, setValue } from '@dxos/echo/internal';
8
+ import { Format } from '@dxos/echo';
9
+ import { TypeEnum, TypedObject, setValue } from '@dxos/echo/internal';
9
10
  import { live } from '@dxos/live-object';
10
11
  import { faker } from '@dxos/random';
11
12
  import { type ProjectionModel } from '@dxos/schema';
12
13
 
13
14
  export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
14
- id: ObjectId,
15
15
  name: Schema.optional(Schema.String),
16
16
  age: Schema.optional(Schema.Number),
17
17
  active: Schema.optional(Schema.Boolean),
@@ -88,7 +88,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
88
88
 
89
89
  if (format) {
90
90
  switch (format) {
91
- case FormatEnum.Currency: {
91
+ case Format.TypeFormat.Currency: {
92
92
  item[path] = Math.floor(Math.random() * 1000);
93
93
  break;
94
94
  }
@@ -2,5 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './useTableModel';
6
5
  export * from './useAddRow';
6
+ export * from './useProjectionModel';
7
+ export * from './useTableModel';
@@ -0,0 +1,19 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useMemo } from 'react';
6
+
7
+ import { Type } from '@dxos/echo';
8
+ import { ProjectionModel } from '@dxos/schema';
9
+
10
+ import { type Table } from '../types';
11
+
12
+ export const useProjectionModel = <S extends Type.Entity.Any>(schema: S | undefined, table: Table.Table | undefined) =>
13
+ useMemo(() => {
14
+ if (schema && table?.view.target?.projection) {
15
+ const projection = new ProjectionModel(Type.toJsonSchema(schema), table.view.target.projection);
16
+ projection.normalizeView();
17
+ return projection;
18
+ }
19
+ }, [schema, table?.view.target?.projection]);
@@ -7,17 +7,16 @@ import orderBy from 'lodash.orderby';
7
7
  import { useEffect, useMemo, useState } from 'react';
8
8
 
9
9
  import { Obj } from '@dxos/echo';
10
- import { type JsonSchemaType } from '@dxos/echo/internal';
11
10
  import { type Live } from '@dxos/live-object';
12
11
  import { useSelected, useSelectionActions } from '@dxos/react-ui-attention';
13
- import { type ProjectionModel, type View } from '@dxos/schema';
12
+ import { type ProjectionModel } from '@dxos/schema';
14
13
  import { isNonNullable } from '@dxos/util';
15
14
 
16
15
  import { TableModel, type TableModelProps, type TableRow, type TableRowAction } from '../model';
16
+ import { type Table } from '../types';
17
17
 
18
18
  export type UseTableModelParams<T extends TableRow = TableRow> = {
19
- view?: View.View;
20
- schema?: JsonSchemaType;
19
+ object?: Table.Table;
21
20
  projection?: ProjectionModel;
22
21
  rows?: Live<T>[];
23
22
  rowActions?: TableRowAction[];
@@ -29,8 +28,7 @@ export type UseTableModelParams<T extends TableRow = TableRow> = {
29
28
  >;
30
29
 
31
30
  export const useTableModel = <T extends TableRow = TableRow>({
32
- view,
33
- schema,
31
+ object,
34
32
  projection,
35
33
  rows,
36
34
  rowActions,
@@ -39,20 +37,19 @@ export const useTableModel = <T extends TableRow = TableRow>({
39
37
  onRowAction,
40
38
  ...props
41
39
  }: UseTableModelParams<T>): TableModel<T> | undefined => {
42
- const selected = useSelected(view && Obj.getDXN(view).toString(), 'multi');
43
- const initialSelection = useMemo(() => selected, [view]);
40
+ const selected = useSelected(object && Obj.getDXN(object).toString(), 'multi');
41
+ const initialSelection = useMemo(() => selected, [object]);
44
42
 
45
43
  const [model, setModel] = useState<TableModel<T>>();
46
44
  useEffect(() => {
47
- if (!view || !schema) {
45
+ if (!object || !projection) {
48
46
  return;
49
47
  }
50
48
 
51
49
  let model: TableModel<T> | undefined;
52
50
  const t = setTimeout(async () => {
53
51
  model = new TableModel<T>({
54
- view,
55
- schema,
52
+ object,
56
53
  projection,
57
54
  features,
58
55
  rowActions,
@@ -68,16 +65,17 @@ export const useTableModel = <T extends TableRow = TableRow>({
68
65
  clearTimeout(t);
69
66
  void model?.close();
70
67
  };
71
- }, [view, schema, projection, features, rowActions, initialSelection]); // TODO(burdon): Trigger if callbacks change?
68
+ // TODO(burdon): Trigger if callbacks change?
69
+ }, [object, projection, features, rowActions, initialSelection]);
72
70
 
73
71
  // Update data.
74
72
  useEffect(() => {
75
73
  if (rows) {
76
- // TODO(ZaymonFC): Remove this workaround once unstable query ordering issue is resolved
77
74
  /*
78
75
  * Sort all objects by string id field as a temporary workaround for query ordering issues
79
76
  * Reference: https://github.com/dxos/dxos/pull/9409
80
77
  */
78
+ // TODO(ZaymonFC): Remove this workaround once unstable query ordering issue is resolved
81
79
  const sortedRows = orderBy(rows, [(row) => String(row.id)], ['asc']);
82
80
  model?.setRows(sortedRows);
83
81
  }
@@ -12,7 +12,7 @@ import { updateCounter } from '@dxos/echo/testing';
12
12
  import { createEchoSchema } from '@dxos/echo/testing';
13
13
  import { registerSignalsRuntime } from '@dxos/echo-signals';
14
14
  import { live } from '@dxos/live-object';
15
- import { View } from '@dxos/schema';
15
+ import { ProjectionModel, View } from '@dxos/schema';
16
16
 
17
17
  import { Table } from '../types';
18
18
 
@@ -112,11 +112,11 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
112
112
 
113
113
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
114
114
  const schema = createEchoSchema(Test);
115
- const table = Table.make();
116
115
  const view = View.make({
117
116
  query: Query.select(Filter.type(schema)),
118
117
  jsonSchema: schema.jsonSchema,
119
- presentation: table,
120
118
  });
121
- return new TableModel({ view, schema: schema.jsonSchema, ...props });
119
+ const object = Table.make({ view });
120
+ const projection = new ProjectionModel(schema.jsonSchema, view.projection);
121
+ return new TableModel({ object, projection, ...props });
122
122
  };