@dxos/react-ui-table 0.8.4-main.1da679c → 0.8.4-main.21d9917

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 (140) hide show
  1. package/dist/lib/browser/chunk-MNZKOFMA.mjs +1746 -0
  2. package/dist/lib/browser/chunk-MNZKOFMA.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1134 -1150
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +29 -39
  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-2ATAIIAO.mjs +1748 -0
  12. package/dist/lib/node-esm/chunk-2ATAIIAO.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1134 -1150
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +29 -39
  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 +5 -6
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +6 -20
  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 +77 -10
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/Table.d.ts +11 -8
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +76 -11
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -12
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -7
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +5 -6
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  44. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  45. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  47. package/dist/types/src/components/testing.d.ts +23 -16
  48. package/dist/types/src/components/testing.d.ts.map +1 -1
  49. package/dist/types/src/hooks/index.d.ts +2 -1
  50. package/dist/types/src/hooks/index.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  52. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  54. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -8
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/model/modal-controller.d.ts +5 -8
  58. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  59. package/dist/types/src/model/selection-model.d.ts +17 -8
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +103 -24
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +3 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  66. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  67. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  68. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  69. package/dist/types/src/translations.d.ts +4 -4
  70. package/dist/types/src/translations.d.ts.map +1 -1
  71. package/dist/types/src/types/Table.d.ts +31 -0
  72. package/dist/types/src/types/Table.d.ts.map +1 -0
  73. package/dist/types/src/types/index.d.ts +1 -1
  74. package/dist/types/src/types/index.d.ts.map +1 -1
  75. package/dist/types/src/util/dynamic-table.d.ts +13 -12
  76. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  77. package/dist/types/src/util/index.d.ts +2 -0
  78. package/dist/types/src/util/index.d.ts.map +1 -1
  79. package/dist/types/src/util/query-ast.d.ts +7 -0
  80. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  81. package/dist/types/src/util/schema.d.ts +1 -1
  82. package/dist/types/src/util/schema.d.ts.map +1 -1
  83. package/dist/types/src/util/sort.d.ts +12 -0
  84. package/dist/types/src/util/sort.d.ts.map +1 -0
  85. package/dist/types/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +56 -56
  87. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  88. package/src/components/Table/ColumnSettings.tsx +6 -6
  89. package/src/components/Table/DynamicTable.stories.tsx +18 -18
  90. package/src/components/Table/DynamicTable.tsx +23 -19
  91. package/src/components/Table/Relations.stories.tsx +143 -39
  92. package/src/components/Table/RowActionsMenu.tsx +24 -15
  93. package/src/components/Table/Table.stories.tsx +92 -95
  94. package/src/components/Table/Table.tsx +339 -307
  95. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  96. package/src/components/TableCellEditor/FormCellEditor.tsx +116 -129
  97. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +23 -47
  98. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -122
  99. package/src/components/TableCellEditor/index.ts +1 -0
  100. package/src/components/TableToolbar/TableToolbar.tsx +47 -31
  101. package/src/components/testing.ts +39 -13
  102. package/src/hooks/index.ts +2 -1
  103. package/src/hooks/useAddRow.ts +8 -10
  104. package/src/hooks/useProjectionModel.ts +40 -0
  105. package/src/hooks/useTableModel.ts +35 -33
  106. package/src/model/modal-controller.ts +23 -40
  107. package/src/model/selection-model.ts +75 -44
  108. package/src/model/table-model.test.ts +91 -58
  109. package/src/model/table-model.ts +429 -163
  110. package/src/model/table-presentation.test.ts +64 -57
  111. package/src/model/table-presentation.ts +34 -30
  112. package/src/playwright/TableManager.ts +5 -5
  113. package/src/playwright/playwright.config.ts +1 -1
  114. package/src/playwright/smoke.spec.ts +11 -8
  115. package/src/testing/useTestTableModel.ts +44 -35
  116. package/src/translations.ts +3 -3
  117. package/src/types/Table.ts +90 -0
  118. package/src/types/index.ts +1 -1
  119. package/src/util/dynamic-table.ts +41 -30
  120. package/src/util/index.ts +2 -0
  121. package/src/util/query-ast.ts +19 -0
  122. package/src/util/schema.ts +4 -4
  123. package/src/util/sort.ts +90 -0
  124. package/dist/lib/browser/chunk-63UUF4IG.mjs +0 -66
  125. package/dist/lib/browser/chunk-63UUF4IG.mjs.map +0 -7
  126. package/dist/lib/browser/chunk-BBXDTWCX.mjs +0 -1822
  127. package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-K734DJB7.mjs +0 -67
  129. package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +0 -7
  130. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs +0 -1823
  131. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +0 -7
  132. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  133. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  134. package/dist/types/src/model/table-sorting.d.ts +0 -52
  135. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  136. package/dist/types/src/types/schema.d.ts +0 -28
  137. package/dist/types/src/types/schema.d.ts.map +0 -1
  138. package/src/components/Table/CreateRefPanel.tsx +0 -103
  139. package/src/model/table-sorting.ts +0 -225
  140. package/src/types/schema.ts +0 -73
@@ -4,15 +4,13 @@
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
6
  import { EditorView } from '@codemirror/view';
7
- import { type 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-schema';
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
- import { createMarkdownExtensions } from '@dxos/react-ui-editor';
16
14
  import {
17
15
  type EditorBlurHandler,
18
16
  type EditorKeyEvent,
@@ -24,40 +22,44 @@ import {
24
22
  parseCellIndex,
25
23
  useGridContext,
26
24
  } from '@dxos/react-ui-grid';
27
- import { createLinks, tagPicker } from '@dxos/react-ui-tag-picker';
28
25
  import { type FieldProjection } from '@dxos/schema';
29
26
 
30
27
  import { type ModalController, type TableModel } from '../../model';
31
28
 
32
29
  import { CellValidationMessage } from './CellValidationMessage';
33
- import { FormCellEditor } from './FormCellEditor';
30
+ import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
31
+
32
+ const editorSlots = {
33
+ scroll: {
34
+ className: '!plb-[--dx-grid-cell-editor-padding-block]',
35
+ },
36
+ };
34
37
 
35
38
  /**
36
39
  * Option to create new object/value.
37
40
  */
38
- export type QueryResult = Pick<Completion, 'label'> & { data: any };
41
+ export type QueryResult = Pick<Completion, 'label'> & {
42
+ data: any;
43
+ };
39
44
 
40
- export type TableCellEditorProps = {
45
+ export type TableCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
46
+ schema?: T;
41
47
  model?: TableModel;
42
48
  modals?: ModalController;
43
- schema?: Schema.AnyNoContext;
44
49
  onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
50
+ onCreate?: OnCreateHandler;
45
51
  onSave?: () => void;
46
- client?: Client;
47
52
  };
48
53
 
49
- const adaptValidationMessage = (message: string | null) =>
50
- message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
51
-
52
- export const TableValueEditor = ({
54
+ export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
55
+ __gridScope,
56
+ schema,
53
57
  model,
54
58
  modals,
55
- schema,
56
59
  onFocus,
57
60
  onSave,
58
- client,
59
- __gridScope,
60
- }: GridScopedProps<TableCellEditorProps>) => {
61
+ onCreate,
62
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
61
63
  const { editing } = useGridContext('TableValueEditor', __gridScope);
62
64
 
63
65
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -66,7 +68,7 @@ export const TableValueEditor = ({
66
68
  }
67
69
 
68
70
  const { col } = parseCellIndex(editing.index);
69
- const field = model.projection.fields[col];
71
+ const field = model.projection.getFields()[col];
70
72
  const fieldProjection = model.projection.getFieldProjection(field.id);
71
73
  invariant(fieldProjection);
72
74
  return fieldProjection;
@@ -74,35 +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}
87
- onSave={onSave}
88
- client={client}
86
+ schema={schema}
87
+ model={model}
88
+ fieldProjection={fieldProjection}
89
89
  modals={modals}
90
+ onSave={onSave}
91
+ onCreate={onCreate}
90
92
  />
91
93
  );
92
94
  }
93
95
 
94
- // For all other types, use the existing cell editor
96
+ // For all other types, use the existing cell editor.
95
97
  return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
96
98
  };
97
99
 
98
- const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
99
-
100
100
  export const TableCellEditor = ({
101
+ __gridScope,
101
102
  model,
102
103
  modals,
103
104
  onFocus,
104
105
  onSave,
105
- __gridScope,
106
106
  }: GridScopedProps<TableCellEditorProps>) => {
107
107
  const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
108
108
  const suppressNextBlur = useRef(false);
@@ -115,12 +115,13 @@ export const TableCellEditor = ({
115
115
  }
116
116
 
117
117
  const { col } = parseCellIndex(editing.index);
118
- const field = model.projection.fields[col];
118
+ const field = model.projection.getFields()[col];
119
119
  const fieldProjection = model.projection.getFieldProjection(field.id);
120
120
  invariant(fieldProjection);
121
121
  return fieldProjection;
122
122
  }, [model, editing]);
123
123
 
124
+ // TOOD(burdon): Attach to event handler?
124
125
  const handleEnter = useCallback(
125
126
  async (value: any) => {
126
127
  if (!model || !editing) {
@@ -137,7 +138,7 @@ export const TableCellEditor = ({
137
138
  setEditing(null);
138
139
  onSave?.();
139
140
  } else {
140
- setValidationError(adaptValidationMessage(validationResult.error));
141
+ setValidationError(validationResult.error);
141
142
  setValidationVariant('error');
142
143
  }
143
144
  },
@@ -213,48 +214,6 @@ export const TableCellEditor = ({
213
214
  ...(editing?.initialContent && { onNav: handleClose }),
214
215
  }),
215
216
  ];
216
-
217
- const format = fieldProjection.props.format;
218
- if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
219
- // TODO(ZaymonFC): Reconcile this with the TagPicker component?
220
- const options = fieldProjection.props.options || [];
221
- const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
222
-
223
- // Add markdown extensions needed by tag picker.
224
- extensions.push(createMarkdownExtensions());
225
- extensions.push(
226
- tagPicker({
227
- mode,
228
- keymap: false,
229
- onSearch: (text, selectedIds) => {
230
- return options
231
- .filter(
232
- (option) =>
233
- selectedIds.indexOf(option.id) === -1 &&
234
- (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
235
- )
236
- .map((option) => ({
237
- id: option.id,
238
- label: option.title,
239
- hue: option.color as any,
240
- }));
241
- },
242
- onUpdate: (ids) => {
243
- if (model && editing) {
244
- if (ids.length === 0) {
245
- return;
246
- }
247
- if (mode === 'single-select') {
248
- void handleEnter(ids[0]);
249
- } else {
250
- void handleEnter(ids);
251
- }
252
- }
253
- },
254
- }),
255
- );
256
- }
257
-
258
217
  // Add validation extension to handle content changes.
259
218
  if (model && editing) {
260
219
  extensions.push(
@@ -283,52 +242,6 @@ export const TableCellEditor = ({
283
242
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
284
243
  if (model && editing) {
285
244
  const cell = parseCellIndex(editing.index);
286
- const { col } = cell;
287
- const field = model.projection.fields[col];
288
- const fieldProjection = model.projection.getFieldProjection(field.id);
289
-
290
- if (
291
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
292
- fieldProjection?.props.format === FormatEnum.MultiSelect
293
- ) {
294
- const value = model.getCellData(cell);
295
- if (value !== undefined) {
296
- const options = fieldProjection.props.options || [];
297
- if (fieldProjection.props.format === FormatEnum.MultiSelect) {
298
- const tagItems = value
299
- .split(',')
300
- .map((id: string) => {
301
- const option = options.find((o) => o.id === id);
302
- if (option) {
303
- return {
304
- id,
305
- label: option.title,
306
- hue: option.color as any,
307
- };
308
- }
309
- return undefined;
310
- })
311
- .filter((item: any): item is { id: any; label: string; hue: any } => item !== undefined);
312
-
313
- return createLinks(tagItems);
314
- } else {
315
- const option = options.find((o) => o.id === value);
316
-
317
- if (option) {
318
- const tagItem = {
319
- id: value,
320
- label: option.title,
321
- hue: option.color as any,
322
- };
323
-
324
- return createLinks([tagItem]);
325
- }
326
- }
327
- }
328
-
329
- return '';
330
- }
331
-
332
245
  const value = model.getCellData(cell);
333
246
  return value !== undefined ? String(value) : '';
334
247
  }
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './TableCellEditor';
6
+ export { type OnCreateHandler } from './FormCellEditor';
@@ -2,10 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Rx } from '@effect-rx/rx-react';
6
- import React, { useMemo } from 'react';
5
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
+ import React, { useContext, useEffect, useMemo } from 'react';
7
7
 
8
- import { live } from '@dxos/live-object';
9
8
  import { type ThemedClassName } from '@dxos/react-ui';
10
9
  import {
11
10
  type ActionGraphEdges,
@@ -18,7 +17,6 @@ import {
18
17
  createMenuAction,
19
18
  useMenuActions,
20
19
  } from '@dxos/react-ui-menu';
21
- import { rxFromSignal } from '@dxos/react-ui-menu';
22
20
 
23
21
  import { translationKey } from '../../translations';
24
22
 
@@ -30,49 +28,60 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
30
28
 
31
29
  type TableToolbarState = Partial<{ viewDirty: boolean }>;
32
30
 
31
+ // TODO(burdon): Radix style layout.
32
+
33
33
  export type TableToolbarProps = ThemedClassName<
34
34
  TableToolbarState & {
35
- onAdd: () => void;
36
- onSave: () => void;
35
+ onAdd?: () => void;
36
+ onSave?: () => void;
37
37
  attendableId?: string;
38
- customActions?: Rx.Rx<ActionGraphProps>;
38
+ customActions?: Atom.Atom<ActionGraphProps>;
39
39
  }
40
40
  >;
41
41
 
42
42
  const createTableToolbarActions = ({
43
- state,
43
+ stateAtom,
44
44
  onAdd,
45
45
  onSave,
46
46
  customActions,
47
47
  }: {
48
- state: TableToolbarState;
49
- onAdd: () => void;
50
- onSave: () => void;
51
- customActions?: Rx.Rx<ActionGraphProps>;
48
+ stateAtom: Atom.Atom<TableToolbarState>;
49
+ onAdd?: () => void;
50
+ onSave?: () => void;
51
+ customActions?: Atom.Atom<ActionGraphProps>;
52
52
  }) =>
53
- Rx.make((get) => {
54
- const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
55
- type: 'add-row' as const,
56
- icon: 'ph--plus--regular',
57
- label: ['add row', { ns: translationKey }],
58
- testId: 'table.toolbar.add-row',
59
- });
60
- const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
61
- type: 'save-view' as const,
62
- icon: 'ph--floppy-disk--regular',
63
- label: ['save view label', { ns: translationKey }],
64
- testId: 'table.toolbar.save-view',
65
- iconOnly: false,
66
- hidden: get(rxFromSignal(() => !state.viewDirty)),
67
- });
53
+ Atom.make((get) => {
54
+ const nodes: ActionGraphNodes = [];
55
+ if (onAdd) {
56
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
57
+ type: 'add-row' as const,
58
+ icon: 'ph--plus--regular',
59
+ label: ['add row label', { ns: translationKey }],
60
+ testId: 'table.toolbar.add-row',
61
+ });
62
+ nodes.push(add);
63
+ }
64
+ if (onSave) {
65
+ const state = get(stateAtom);
66
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
67
+ type: 'save-view' as const,
68
+ icon: 'ph--floppy-disk--regular',
69
+ label: ['save view label', { ns: translationKey }],
70
+ testId: 'table.toolbar.save-view',
71
+ iconOnly: false,
72
+ hidden: !state.viewDirty,
73
+ });
74
+ nodes.push(save);
75
+ }
68
76
  const gap = createGapSeparator();
69
- const nodes: ActionGraphNodes = [add, save, ...gap.nodes];
77
+ nodes.push(...gap.nodes);
70
78
  const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target }));
71
79
  if (customActions) {
72
80
  const custom = get(customActions);
73
81
  nodes.push(...custom.nodes);
74
82
  edges.push(...custom.edges);
75
83
  }
84
+
76
85
  return {
77
86
  nodes,
78
87
  edges,
@@ -87,10 +96,17 @@ export const TableToolbar = ({
87
96
  onSave,
88
97
  customActions,
89
98
  }: TableToolbarProps) => {
90
- const state = useMemo(() => live<TableToolbarState>({ viewDirty }), []);
99
+ const registry = useContext(RegistryContext);
100
+ const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
101
+
102
+ // Update state.viewDirty when the prop changes.
103
+ useEffect(() => {
104
+ registry.set(stateAtom, { viewDirty });
105
+ }, [registry, stateAtom, viewDirty]);
106
+
91
107
  const actionsCreator = useMemo(
92
- () => createTableToolbarActions({ state, onAdd, onSave, customActions }),
93
- [state, onAdd, onSave, customActions],
108
+ () => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
109
+ [stateAtom, onAdd, onSave, customActions],
94
110
  );
95
111
  const menu = useMenuActions(actionsCreator);
96
112
 
@@ -2,33 +2,59 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import * as Schema from 'effect/Schema';
6
7
  import { useEffect } from 'react';
7
8
 
8
- import { FormatEnum, ObjectId, TypeEnum, TypedObject, setValue } from '@dxos/echo-schema';
9
- import { live } from '@dxos/live-object';
9
+ import { Format, Type } from '@dxos/echo';
10
+ import { TypeEnum } from '@dxos/echo/internal';
11
+ import { setValue } from '@dxos/effect';
10
12
  import { faker } from '@dxos/random';
11
13
  import { type ProjectionModel } from '@dxos/schema';
12
14
 
13
- export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
14
- id: ObjectId,
15
+ export const TestSchema = Schema.Struct({
15
16
  name: Schema.optional(Schema.String),
16
17
  age: Schema.optional(Schema.Number),
17
18
  active: Schema.optional(Schema.Boolean),
18
19
  netWorth: Schema.optional(Schema.Number),
19
- });
20
+ }).pipe(
21
+ Type.object({
22
+ typename: 'example.com/type/Test',
23
+ version: '0.1.0',
24
+ }),
25
+ );
20
26
 
21
- export const createItems = (n: number) => {
22
- const { data } = live({
27
+ export type TestItem = {
28
+ name: string;
29
+ age: number;
30
+ active: boolean;
31
+ netWorth: number;
32
+ };
33
+
34
+ /**
35
+ * Creates an Atom containing test items data.
36
+ */
37
+ export const createItemsAtom = (n: number) => {
38
+ return Atom.make<{ data: TestItem[] }>({
23
39
  data: Array.from({ length: n }, () => ({
24
40
  name: faker.person.fullName(),
25
41
  age: faker.number.int({ min: 20, max: 70 }),
26
42
  active: faker.datatype.boolean(),
27
43
  netWorth: faker.number.int(),
28
44
  })),
29
- });
45
+ }).pipe(Atom.keepAlive);
46
+ };
30
47
 
31
- return data;
48
+ /**
49
+ * Creates plain test items (non-reactive).
50
+ */
51
+ export const createItems = (n: number): TestItem[] => {
52
+ return Array.from({ length: n }, () => ({
53
+ name: faker.person.fullName(),
54
+ age: faker.number.int({ min: 20, max: 70 }),
55
+ active: faker.datatype.boolean(),
56
+ netWorth: faker.number.int(),
57
+ }));
32
58
  };
33
59
 
34
60
  export type SimulatorProps = {
@@ -59,7 +85,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
59
85
 
60
86
  const i = setInterval(() => {
61
87
  const rowIdx = Math.floor(Math.random() * items.length);
62
- const fields = projection.fields ?? [];
88
+ const fields = projection.getFields() ?? [];
63
89
  const columnIdx = Math.floor(Math.random() * fields.length);
64
90
  const field = fields[columnIdx];
65
91
  const item = items[rowIdx];
@@ -88,7 +114,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
88
114
 
89
115
  if (format) {
90
116
  switch (format) {
91
- case FormatEnum.Currency: {
117
+ case Format.TypeFormat.Currency: {
92
118
  item[path] = Math.floor(Math.random() * 1000);
93
119
  break;
94
120
  }
@@ -98,5 +124,5 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
98
124
  }, updateInterval);
99
125
 
100
126
  return () => clearInterval(i);
101
- }, [items, projection.fields, updateInterval]);
127
+ }, [items, projection.getFields(), updateInterval]);
102
128
  };
@@ -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';
@@ -2,17 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect';
6
5
  import { useCallback } from 'react';
7
6
 
8
- import { type Space } from '@dxos/client/echo';
9
- import { Obj } from '@dxos/echo';
7
+ import { type Database, Obj, type Type } from '@dxos/echo';
10
8
 
11
9
  import { type InsertRowResult } from '../model';
12
10
 
13
- export type UseAddRowParams = {
14
- space?: Space;
15
- schema?: Schema.Schema.AnyNoContext;
11
+ export type UseAddRowProps = {
12
+ db?: Database.Database;
13
+ schema?: Type.Obj.Any;
16
14
  };
17
15
 
18
16
  /**
@@ -20,12 +18,12 @@ export type UseAddRowParams = {
20
18
  * Returns a callback that attempts to create an object and returns boolean success/failure.
21
19
  * Can accept optional data to create objects with specific content (used for committing draft rows).
22
20
  */
23
- export const useAddRow = ({ space, schema }: UseAddRowParams) => {
21
+ export const useAddRow = ({ db, schema }: UseAddRowProps) => {
24
22
  return useCallback(
25
23
  (data?: any): InsertRowResult => {
26
- if (space && schema) {
24
+ if (db && schema) {
27
25
  try {
28
- space.db.add(Obj.make(schema, data ?? {}));
26
+ db.add(Obj.make(schema, data ?? {}));
29
27
  return 'final';
30
28
  } catch (error) {
31
29
  return 'draft';
@@ -33,6 +31,6 @@ export const useAddRow = ({ space, schema }: UseAddRowParams) => {
33
31
  }
34
32
  return 'final';
35
33
  },
36
- [space, schema],
34
+ [db, schema],
37
35
  );
38
36
  };
@@ -0,0 +1,40 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Registry } from '@effect-atom/atom-react';
6
+ import { useState } from 'react';
7
+
8
+ import { Type } from '@dxos/echo';
9
+ import { useAsyncEffect } from '@dxos/react-ui';
10
+ import { ProjectionModel, createDirectChangeCallback, createEchoChangeCallback } from '@dxos/schema';
11
+
12
+ import { type Table } from '../types';
13
+
14
+ export const useProjectionModel = <S extends Type.Entity.Any>(
15
+ schema: S | undefined,
16
+ table: Table.Table | undefined,
17
+ registry: Registry.Registry,
18
+ ) => {
19
+ const [projection, setProjection] = useState<ProjectionModel | undefined>();
20
+
21
+ useAsyncEffect(async () => {
22
+ if (schema && table) {
23
+ const view = await table.view.load();
24
+ // For mutable schemas (EchoSchema), use the live jsonSchema reference for reactivity.
25
+ // For immutable schemas, create a snapshot.
26
+ const jsonSchema = Type.isMutable(schema) ? schema.jsonSchema : Type.toJsonSchema(schema);
27
+
28
+ // Use createEchoChangeCallback for mutable schemas (EchoSchema), otherwise use direct mutation.
29
+ const change = Type.isMutable(schema)
30
+ ? createEchoChangeCallback(view, schema)
31
+ : createDirectChangeCallback(view.projection, jsonSchema);
32
+
33
+ const projection = new ProjectionModel({ registry, view, baseSchema: jsonSchema, change });
34
+ projection.normalizeView();
35
+ setProjection(projection);
36
+ }
37
+ }, [schema, table, registry]);
38
+
39
+ return projection;
40
+ };