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

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 (113) hide show
  1. package/dist/lib/browser/{chunk-V7A7ZA5E.mjs → chunk-GTGYBRJA.mjs} +606 -641
  2. package/dist/lib/browser/chunk-GTGYBRJA.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-CJQ6T4LL.mjs → chunk-OZBLY7XM.mjs} +4 -5
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1082 -969
  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 +20 -16
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +1 -1
  11. package/dist/lib/node-esm/{chunk-P73ALX3N.mjs → chunk-DPNSZHU5.mjs} +606 -640
  12. package/dist/lib/node-esm/chunk-DPNSZHU5.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-T6PXZQ6W.mjs → chunk-WNZSKIRF.mjs} +4 -5
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1082 -969
  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 +20 -16
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +1 -1
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/DynamicTable.d.ts +3 -2
  24. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  26. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/Relations.stories.d.ts +2 -2
  28. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  30. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/Table.stories.d.ts +2 -2
  32. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  33. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +1 -2
  34. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  36. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
  37. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  38. package/dist/types/src/components/testing.d.ts +22 -13
  39. package/dist/types/src/components/testing.d.ts.map +1 -1
  40. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  41. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  42. package/dist/types/src/hooks/useProjectionModel.d.ts +2 -1
  43. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useTableModel.d.ts +6 -5
  45. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  46. package/dist/types/src/model/modal-controller.d.ts +5 -1
  47. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  48. package/dist/types/src/model/selection-model.d.ts +17 -8
  49. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  50. package/dist/types/src/model/table-model.d.ts +99 -18
  51. package/dist/types/src/model/table-model.d.ts.map +1 -1
  52. package/dist/types/src/model/table-presentation.d.ts +3 -1
  53. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  54. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  55. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  56. package/dist/types/src/testing/useTestTableModel.d.ts +4 -5
  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/translations.d.ts.map +1 -1
  60. package/dist/types/src/types/Table.d.ts +17 -14
  61. package/dist/types/src/types/Table.d.ts.map +1 -1
  62. package/dist/types/src/util/dynamic-table.d.ts +10 -7
  63. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  64. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  65. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  66. package/dist/types/src/util/index.d.ts +2 -0
  67. package/dist/types/src/util/index.d.ts.map +1 -1
  68. package/dist/types/src/util/query-ast.d.ts +7 -0
  69. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  70. package/dist/types/src/util/sort.d.ts +12 -0
  71. package/dist/types/src/util/sort.d.ts.map +1 -0
  72. package/dist/types/tsconfig.tsbuildinfo +1 -1
  73. package/package.json +53 -54
  74. package/src/components/Table/ColumnActionsMenu.tsx +6 -5
  75. package/src/components/Table/ColumnSettings.tsx +4 -3
  76. package/src/components/Table/DynamicTable.stories.tsx +10 -8
  77. package/src/components/Table/DynamicTable.tsx +18 -18
  78. package/src/components/Table/Relations.stories.tsx +23 -19
  79. package/src/components/Table/RowActionsMenu.tsx +3 -2
  80. package/src/components/Table/Table.stories.tsx +47 -31
  81. package/src/components/Table/Table.tsx +34 -17
  82. package/src/components/TableCellEditor/FormCellEditor.tsx +49 -65
  83. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +3 -3
  84. package/src/components/TableCellEditor/TableCellEditor.tsx +2 -2
  85. package/src/components/TableToolbar/TableToolbar.tsx +18 -12
  86. package/src/components/testing.ts +37 -11
  87. package/src/hooks/useAddRow.ts +8 -10
  88. package/src/hooks/useProjectionModel.ts +28 -8
  89. package/src/hooks/useTableModel.ts +27 -23
  90. package/src/model/modal-controller.ts +23 -12
  91. package/src/model/selection-model.ts +75 -44
  92. package/src/model/table-model.test.ts +80 -52
  93. package/src/model/table-model.ts +409 -138
  94. package/src/model/table-presentation.test.ts +57 -47
  95. package/src/model/table-presentation.ts +23 -18
  96. package/src/playwright/TableManager.ts +10 -3
  97. package/src/playwright/playwright.config.ts +1 -1
  98. package/src/playwright/smoke.spec.ts +48 -4
  99. package/src/testing/useTestTableModel.ts +23 -20
  100. package/src/translations.ts +1 -1
  101. package/src/types/Table.ts +8 -8
  102. package/src/util/dynamic-table.test.ts +70 -0
  103. package/src/util/dynamic-table.ts +31 -13
  104. package/src/util/index.ts +2 -0
  105. package/src/util/query-ast.ts +19 -0
  106. package/src/util/sort.ts +90 -0
  107. package/dist/lib/browser/chunk-CJQ6T4LL.mjs.map +0 -7
  108. package/dist/lib/browser/chunk-V7A7ZA5E.mjs.map +0 -7
  109. package/dist/lib/node-esm/chunk-P73ALX3N.mjs.map +0 -7
  110. package/dist/lib/node-esm/chunk-T6PXZQ6W.mjs.map +0 -7
  111. package/dist/types/src/model/table-sorting.d.ts +0 -56
  112. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  113. package/src/model/table-sorting.ts +0 -234
@@ -6,18 +6,19 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback } from 'react';
8
8
 
9
- import { Annotation, Format, Obj, type QueryAST, Type } from '@dxos/echo';
10
- import { PropertyMetaAnnotationId } from '@dxos/echo/internal';
9
+ import { Annotation, type Database, Format, Obj, type QueryAST, Type } from '@dxos/echo';
10
+ import { type Mutable, PropertyMetaAnnotationId } from '@dxos/echo/internal';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { faker } from '@dxos/random';
13
13
  import { PublicKey } from '@dxos/react-client';
14
- import { type Space } from '@dxos/react-client/echo';
15
14
  import { withClientProvider } from '@dxos/react-client/testing';
15
+ import { ScrollArea } from '@dxos/react-ui';
16
16
  import { withTheme } from '@dxos/react-ui/testing';
17
17
  import { ViewEditor, translations as formTranslations } from '@dxos/react-ui-form';
18
18
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
19
19
  import { View, getSchemaFromPropertyDefinitions, getTypenameFromQuery } from '@dxos/schema';
20
20
  import { TestSchema, createObjectFactory } from '@dxos/schema/testing';
21
+ import { withRegistry } from '@dxos/storybook-utils';
21
22
 
22
23
  import { useTestTableModel } from '../../testing';
23
24
  import { translations } from '../../translations';
@@ -51,7 +52,7 @@ const Example = Schema.Struct({
51
52
  title: 'Parent',
52
53
  }),
53
54
  }).pipe(
54
- Type.Obj({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
55
+ Type.object({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
55
56
  Annotation.LabelAnnotation.set(['name']),
56
57
  );
57
58
  interface Example extends Schema.Schema.Type<typeof Example> {}
@@ -59,12 +60,12 @@ interface Example extends Schema.Schema.Type<typeof Example> {}
59
60
  const StoryViewEditor = ({
60
61
  view,
61
62
  schema,
62
- space,
63
+ db,
63
64
  handleDeleteColumn,
64
65
  }: {
65
66
  view?: View.View;
66
67
  schema?: Schema.Schema.AnyNoContext;
67
- space?: Space;
68
+ db?: Database.Database;
68
69
  handleDeleteColumn: (fieldId: string) => void;
69
70
  }) => {
70
71
  const handleQueryChanged = useCallback(
@@ -73,7 +74,9 @@ const StoryViewEditor = ({
73
74
  invariant(Type.isMutable(schema));
74
75
  schema.updateTypename(getTypenameFromQuery(newQuery));
75
76
  invariant(view);
76
- view.query.ast = newQuery;
77
+ Obj.change(view, (v) => {
78
+ v.query.ast = newQuery as Mutable<typeof newQuery>;
79
+ });
77
80
  },
78
81
  [schema, view],
79
82
  );
@@ -84,7 +87,7 @@ const StoryViewEditor = ({
84
87
 
85
88
  return (
86
89
  <ViewEditor
87
- registry={space?.db.schemaRegistry}
90
+ registry={db?.schemaRegistry}
88
91
  schema={schema}
89
92
  view={view}
90
93
  onQueryChanged={handleQueryChanged}
@@ -98,9 +101,18 @@ const StoryViewEditor = ({
98
101
  //
99
102
 
100
103
  const DefaultStory = () => {
101
- const { space, schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView, handleDeleteColumn } =
104
+ const { db, schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView, handleDeleteColumn } =
102
105
  useTestTableModel();
103
106
 
107
+ const handleRowClick = useCallback(
108
+ (row: any) => {
109
+ if (model?.getDraftRowCount() === 0 && ['frozenRowsEnd', 'fixedEndStart', 'fixedEndEnd'].includes(row?.plane)) {
110
+ handleInsertRow();
111
+ }
112
+ },
113
+ [model, handleInsertRow],
114
+ );
115
+
104
116
  if (!schema || !table?.view.target) {
105
117
  return <div />;
106
118
  }
@@ -115,21 +127,19 @@ const DefaultStory = () => {
115
127
  schema={schema}
116
128
  model={model}
117
129
  presentation={presentation}
130
+ onRowClick={handleRowClick}
118
131
  ignoreAttention
119
132
  />
120
133
  </TableComponent.Root>
121
134
  </div>
122
- <div className='flex flex-col bs-full border-l border-separator overflow-y-auto'>
123
- <StoryViewEditor
124
- view={table.view.target}
125
- schema={schema}
126
- space={space}
127
- handleDeleteColumn={handleDeleteColumn}
128
- />
129
- <SyntaxHighlighter language='json' className='text-xs'>
130
- {JSON.stringify({ view: table.view.target, schema }, null, 2)}
131
- </SyntaxHighlighter>
132
- </div>
135
+ <ScrollArea.Root orientation='vertical' classNames='bs-full border-l border-separator'>
136
+ <ScrollArea.Viewport>
137
+ <StoryViewEditor view={table.view.target} schema={schema} db={db} handleDeleteColumn={handleDeleteColumn} />
138
+ <SyntaxHighlighter language='json' className='text-xs'>
139
+ {JSON.stringify({ view: table.view.target, schema }, null, 2)}
140
+ </SyntaxHighlighter>
141
+ </ScrollArea.Viewport>
142
+ </ScrollArea.Root>
133
143
  </div>
134
144
  );
135
145
  };
@@ -146,19 +156,22 @@ const meta = {
146
156
  title: 'ui/react-ui-table/Table',
147
157
  render: DefaultStory,
148
158
  decorators: [
149
- withTheme,
159
+ withTheme(),
160
+ withRegistry,
150
161
  withClientProvider({
151
162
  types: [View.View, Table.Table],
152
163
  createIdentity: true,
153
164
  createSpace: true,
154
165
  onCreateSpace: async ({ space }) => {
155
166
  const [schema] = await space.db.schemaRegistry.register([Example]);
156
- const { view, jsonSchema } = await View.makeFromSpace({ space, typename: schema.typename });
167
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: schema.typename });
157
168
  const table = Table.make({ view, jsonSchema });
158
- view.projection.fields = [
159
- view.projection.fields.find((field: any) => field.path === 'name')!,
160
- ...view.projection.fields.filter((field: any) => field.path !== 'name'),
161
- ];
169
+ Obj.change(view, (v) => {
170
+ v.projection.fields = [
171
+ v.projection.fields.find((field) => field.path === 'name')!,
172
+ ...v.projection.fields.filter((field) => field.path !== 'name'),
173
+ ];
174
+ });
162
175
 
163
176
  space.db.add(table);
164
177
 
@@ -197,7 +210,10 @@ export const StaticSchema: StoryObj = {
197
210
  createIdentity: true,
198
211
  createSpace: true,
199
212
  onCreateSpace: async ({ space }) => {
200
- const { view, jsonSchema } = await View.makeFromSpace({ space, typename: TestSchema.Person.typename });
213
+ const { view, jsonSchema } = await View.makeFromDatabase({
214
+ db: space.db,
215
+ typename: TestSchema.Person.typename,
216
+ });
201
217
  const table = Table.make({ view, jsonSchema });
202
218
  space.db.add(table);
203
219
 
@@ -226,7 +242,7 @@ const ContactWithArrayOfEmails = Schema.Struct({
226
242
  ),
227
243
  ),
228
244
  }).pipe(
229
- Type.Obj({
245
+ Type.object({
230
246
  typename: 'dxos.org/type/ContactWithArrayOfEmails',
231
247
  version: '0.1.0',
232
248
  }),
@@ -240,8 +256,8 @@ export const ArrayOfObjects: StoryObj = {
240
256
  createIdentity: true,
241
257
  createSpace: true,
242
258
  onCreateSpace: async ({ space }) => {
243
- const { view, jsonSchema } = await View.makeFromSpace({
244
- space,
259
+ const { view, jsonSchema } = await View.makeFromDatabase({
260
+ db: space.db,
245
261
  typename: ContactWithArrayOfEmails.typename,
246
262
  });
247
263
  const table = Table.make({ view, jsonSchema });
@@ -298,7 +314,7 @@ export const Tags: Meta<StoryProps> = {
298
314
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
299
315
 
300
316
  // Initialize table.
301
- const { view, jsonSchema } = await View.makeFromSpace({ space, typename });
317
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename });
302
318
  const table = Table.make({ view, jsonSchema });
303
319
  space.db.add(table);
304
320
 
@@ -2,6 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
5
6
  import * as String from 'effect/String';
6
7
  import React, {
7
8
  type JSX,
@@ -11,6 +12,7 @@ import React, {
11
12
  type WheelEvent,
12
13
  forwardRef,
13
14
  useCallback,
15
+ useContext,
14
16
  useEffect,
15
17
  useImperativeHandle,
16
18
  useMemo,
@@ -18,8 +20,10 @@ import React, {
18
20
  } from 'react';
19
21
 
20
22
  import { type Type } from '@dxos/echo';
23
+ import { log } from '@dxos/log';
21
24
  import { useAttention } from '@dxos/react-ui-attention';
22
25
  import {
26
+ type DxGridAxisMeta,
23
27
  type DxGridElement,
24
28
  type DxGridPlane,
25
29
  type DxGridPlaneRange,
@@ -31,7 +35,7 @@ import {
31
35
  gridSeparatorInlineEnd,
32
36
  } from '@dxos/react-ui-grid';
33
37
  import { DxEditRequest } from '@dxos/react-ui-grid';
34
- import { mx } from '@dxos/react-ui-theme';
38
+ import { mx } from '@dxos/ui-theme';
35
39
 
36
40
  import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
37
41
  import { tableButtons, tableControls } from '../../util';
@@ -43,6 +47,7 @@ import { RowActionsMenu } from './RowActionsMenu';
43
47
 
44
48
  const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
45
49
  const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
50
+ const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
46
51
 
47
52
  //
48
53
  // Table.Root
@@ -88,13 +93,24 @@ export type TableMainProps<T extends Type.Entity.Any = Type.Entity.Any> = {
88
93
  testId?: string;
89
94
  };
90
95
 
96
+ const emptyRowsAtom = Atom.make<unknown[]>([]);
97
+ const emptyCellUpdateAtom = Atom.make<number>(0);
98
+
91
99
  const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
92
100
  { schema, model, presentation, ignoreAttention, onCreate, onRowClick, testId }: TableMainProps<T>,
93
101
  forwardedRef: Ref<TableController>,
94
102
  ) => {
103
+ const registry = useContext(RegistryContext);
95
104
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
96
105
  const { hasAttention } = useAttention(model?.id ?? 'table');
97
- const modals = useMemo(() => new ModalController(), []);
106
+ const modals = useMemo(() => new ModalController(registry), [registry]);
107
+ const columnMeta = useAtomValue(model?.columnMeta ?? emptyColumnMeta);
108
+ // Subscribe to rows atom to trigger re-render when rows change.
109
+ const rows = useAtomValue(model?.rows ?? emptyRowsAtom);
110
+ // Subscribe to cell update counter to trigger re-render when individual cells change.
111
+ const cellUpdateCounter = useAtomValue(model?.cellUpdate ?? emptyCellUpdateAtom);
112
+ // Derive column count from columnMeta (reactive) with fallback.
113
+ const columnCount = Object.keys(columnMeta.grid).length || model?.projection.getFields().length || 0;
98
114
 
99
115
  const draftRowCount = model?.getDraftRowCount() ?? 0;
100
116
 
@@ -117,15 +133,6 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
117
133
  [presentation],
118
134
  );
119
135
 
120
- // Set initial sort order.
121
- // TODO(burdon): Save sort order to local storage.
122
- useEffect(() => {
123
- const fieldId = model?.projection?.fields?.[0]?.id;
124
- if (fieldId) {
125
- model.sorting.setSort(fieldId, 'asc');
126
- }
127
- }, [model]);
128
-
129
136
  useEffect(() => {
130
137
  if (!dxGrid || !presentation) {
131
138
  return;
@@ -134,6 +141,16 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
134
141
  dxGrid.getCells = getCells;
135
142
  }, [dxGrid, presentation, getCells]);
136
143
 
144
+ // Trigger grid update when rows or cell values change.
145
+ useEffect(() => {
146
+ if (!dxGrid) {
147
+ return;
148
+ }
149
+
150
+ dxGrid.updateCells(true);
151
+ dxGrid.requestUpdate();
152
+ }, [dxGrid, rows, cellUpdateCounter]);
153
+
137
154
  const handleInsertRowResult = useCallback(
138
155
  (insertResult?: InsertRowResult) => {
139
156
  if (insertResult === 'draft') {
@@ -220,7 +237,7 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
220
237
  break;
221
238
  }
222
239
  case 'sort': {
223
- model?.sorting?.toggleSort(data.fieldId);
240
+ model?.toggleSort(data.fieldId);
224
241
  break;
225
242
  }
226
243
  case 'saveDraftRow': {
@@ -246,7 +263,7 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
246
263
  switch (data.type) {
247
264
  case 'checkbox': {
248
265
  if (data.header) {
249
- model?.selection.setSelection(model.selection.allRowsSeleted.value ? 'none' : 'all');
266
+ model?.selection.setSelection(model.selection.allRowsSelected ? 'none' : 'all');
250
267
  } else {
251
268
  model?.selection.toggleSelectionForRowIndex(data.rowIndex);
252
269
  }
@@ -302,7 +319,7 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
302
319
  void navigator.clipboard.writeText(textContent);
303
320
  event.preventDefault();
304
321
  } catch (error) {
305
- console.warn('Failed to copy cell content:', error);
322
+ log.catch(error);
306
323
  }
307
324
  break;
308
325
  }
@@ -423,11 +440,11 @@ const TableMainInner = <T extends Type.Entity.Any = Type.Entity.Any>(
423
440
  <Grid.Content
424
441
  className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
425
442
  frozen={frozen}
426
- columns={model.columnMeta.value}
443
+ columns={columnMeta}
427
444
  columnDefault={columnDefault}
428
445
  rowDefault={rowDefault}
429
- limitRows={model.getRowCount() ?? 0}
430
- limitColumns={model.projection.fields.length}
446
+ limitRows={rows.length}
447
+ limitColumns={columnCount}
431
448
  overscroll='trap'
432
449
  onAxisResize={handleAxisResize}
433
450
  onClick={handleGridClick}
@@ -5,13 +5,11 @@
5
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, type Type } from '@dxos/echo';
9
- import { Format, Ref, getValue } from '@dxos/echo/internal';
8
+ import { Entity, type Type } from '@dxos/echo';
9
+ import { Ref, getValue } from '@dxos/echo/internal';
10
10
  import { invariant } from '@dxos/invariant';
11
- import { getSnapshot } from '@dxos/live-object';
12
- import { getSpace } from '@dxos/react-client/echo';
13
11
  import { type Label, Popover } from '@dxos/react-ui';
14
- import { Form, type FormRootProps, omitId } from '@dxos/react-ui-form';
12
+ import { Form, type FormRootProps, type RefFieldProps } from '@dxos/react-ui-form';
15
13
  import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
16
14
  import { type FieldProjection } from '@dxos/schema';
17
15
  import { getDeep, isTruthy, setDeep } from '@dxos/util';
@@ -22,7 +20,7 @@ import { narrowSchema } from '../../util';
22
20
 
23
21
  const createOptionLabel: Label = ['create new object label', { ns: translationKey }];
24
22
 
25
- export type OnCreateHandler = (schema: Schema.Schema.AnyNoContext, values: any) => Parameters<typeof Ref.make>[0];
23
+ export type OnCreateHandler = (schema: Type.Entity.Any, values: any) => Parameters<typeof Ref.make>[0];
26
24
 
27
25
  export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
28
26
  __gridScope: any;
@@ -67,33 +65,12 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
67
65
  return narrowSchema(schema, [fieldProjection.field.path]);
68
66
  }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
69
67
 
70
- const getSchema = useCallback(
71
- ({ typename }: { typename: string }) => {
72
- const space = getSpace(model!.view);
73
- invariant(space);
74
- const schema = space.db.schemaRegistry.query({ typename, location: ['database', 'runtime'] }).runSync()[0];
75
- return { space, schema };
76
- },
77
- [model],
78
- );
79
-
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
-
86
- return null;
87
- }, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
88
-
89
- const createSchema = useMemo(() => (refSchema ? omitId(refSchema) : null), [refSchema]);
90
-
91
68
  const originalRow = useMemo<TableRow | undefined>(() => {
92
69
  if (model && contextEditing) {
93
70
  // Check if this is a draft cell and get the appropriate row data
94
71
  const cell = parseCellIndex(contextEditing.index);
95
72
  if (model.isDraftCell(cell)) {
96
- const draftRow = model.draftRows.value[cell.row];
73
+ const draftRow = model.getDraftRows()[cell.row];
97
74
  invariant(draftRow);
98
75
  return draftRow.data;
99
76
  } else {
@@ -106,8 +83,9 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
106
83
  return undefined;
107
84
  }, [model, contextEditing]);
108
85
 
109
- // NOTE: Important to get a snapshot to eject from the live object.
110
- const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
86
+ // NOTE: Important to get a mutable deep clone to eject from the echo object.
87
+ // TODO(wittjosiah): Consider using something like Obj.clone for this use case.
88
+ const formValues = useMemo(() => (originalRow ? JSON.parse(JSON.stringify(originalRow)) : {}), [originalRow]);
111
89
 
112
90
  const handleOpenChange = useCallback((nextOpen: boolean) => {
113
91
  if (nextOpen === false) {
@@ -119,24 +97,39 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
119
97
 
120
98
  const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>(
121
99
  (values) => {
100
+ if (!originalRow) {
101
+ return;
102
+ }
122
103
  const path = fieldProjection.field.path;
123
104
  const value = getDeep(values, [path]);
124
- setDeep(originalRow, [path], value);
105
+ // Use model's changeRow for consistent mutation handling.
106
+ if (model) {
107
+ model.changeRow(originalRow, (mutableRow) => {
108
+ setDeep(mutableRow, [path], value);
109
+ });
110
+ } else {
111
+ setDeep(originalRow, [path], value);
112
+ }
125
113
  contextEditing?.cellElement?.focus();
126
114
  setEditing(null);
127
115
  setLocalEditing(false);
128
116
  onSave?.();
129
117
  },
130
- [fieldProjection.field.path, onSave, contextEditing, originalRow],
118
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, model],
131
119
  );
132
120
 
133
121
  const handleCreate = useCallback<NonNullable<FormRootProps<any>['onCreate']>>(
134
- (values) => {
135
- if (refSchema && onCreate) {
136
- const objectWithId = onCreate(refSchema, values);
137
- if (objectWithId) {
138
- const ref = Ref.make(objectWithId);
139
- const path = fieldProjection.field.path;
122
+ (schema, values) => {
123
+ const objectWithId = onCreate?.(schema, values);
124
+ if (objectWithId && originalRow) {
125
+ const ref = Ref.make(objectWithId);
126
+ const path = fieldProjection.field.path;
127
+ // Use model's changeRow for consistent mutation handling.
128
+ if (model) {
129
+ model.changeRow(originalRow, (mutableRow) => {
130
+ setDeep(mutableRow, [path], ref);
131
+ });
132
+ } else {
140
133
  setDeep(originalRow, [path], ref);
141
134
  }
142
135
  }
@@ -145,27 +138,20 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
145
138
  setLocalEditing(false);
146
139
  onSave?.();
147
140
  },
148
- [fieldProjection.field.path, onSave, contextEditing, originalRow, refSchema, onCreate],
141
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, onCreate, model],
149
142
  );
150
143
 
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],
144
+ const getOptions = useCallback<NonNullable<RefFieldProps['getOptions']>>(
145
+ (results) =>
146
+ results
147
+ .map((obj) => {
148
+ return {
149
+ id: Entity.getDXN(obj).toString(),
150
+ label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
151
+ };
152
+ })
153
+ .filter(isTruthy),
154
+ [fieldProjection],
169
155
  );
170
156
 
171
157
  if (!editing) {
@@ -184,15 +170,13 @@ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
184
170
  autoFocus
185
171
  schema={narrowedSchema}
186
172
  values={formValues}
173
+ createInitialValuePath={fieldProjection.field.referencePath}
174
+ createOptionIcon='ph--plus--regular'
175
+ createOptionLabel={createOptionLabel}
176
+ db={model?.db}
177
+ getOptions={getOptions}
178
+ onCreate={handleCreate}
187
179
  onSave={handleSave}
188
- onQueryRefOptions={handleQueryRefOptions}
189
- {...(createSchema && {
190
- createSchema,
191
- createInitialValuePath: fieldProjection.field.referencePath,
192
- createOptionIcon: 'ph--plus--regular',
193
- createOptionLabel,
194
- onCreate: handleCreate,
195
- })}
196
180
  >
197
181
  <Form.Viewport>
198
182
  <Form.Content>
@@ -45,14 +45,14 @@ const meta = {
45
45
  component: DefaultStory,
46
46
  render: DefaultStory,
47
47
  decorators: [
48
- withTheme,
49
- withLayout({ container: 'column' }),
48
+ withTheme(),
49
+ withLayout({ layout: 'column' }),
50
50
  withClientProvider({
51
51
  types: [View.View, Task.Task, Table.Table],
52
52
  createIdentity: true,
53
53
  createSpace: true,
54
54
  onCreateSpace: async ({ space }) => {
55
- const { view, jsonSchema } = await View.makeFromSpace({ space, typename: Task.Task.typename });
55
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: Task.Task.typename });
56
56
  const table = Table.make({ view, jsonSchema });
57
57
  space.db.add(table);
58
58
  Array.from({ length: 10 }).forEach(() => {
@@ -68,7 +68,7 @@ export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
68
68
  }
69
69
 
70
70
  const { col } = parseCellIndex(editing.index);
71
- const field = model.projection.fields[col];
71
+ const field = model.projection.getFields()[col];
72
72
  const fieldProjection = model.projection.getFieldProjection(field.id);
73
73
  invariant(fieldProjection);
74
74
  return fieldProjection;
@@ -115,7 +115,7 @@ 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;
@@ -2,10 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Atom } from '@effect-atom/atom-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 { atomFromSignal } from '@dxos/react-ui-menu';
22
20
 
23
21
  import { translationKey } from '../../translations';
24
22
 
@@ -34,20 +32,20 @@ type TableToolbarState = Partial<{ viewDirty: boolean }>;
34
32
 
35
33
  export type TableToolbarProps = ThemedClassName<
36
34
  TableToolbarState & {
37
- onAdd: () => void;
38
- onSave: () => void;
35
+ onAdd?: () => void;
36
+ onSave?: () => void;
39
37
  attendableId?: string;
40
38
  customActions?: Atom.Atom<ActionGraphProps>;
41
39
  }
42
40
  >;
43
41
 
44
42
  const createTableToolbarActions = ({
45
- state,
43
+ stateAtom,
46
44
  onAdd,
47
45
  onSave,
48
46
  customActions,
49
47
  }: {
50
- state: TableToolbarState;
48
+ stateAtom: Atom.Atom<TableToolbarState>;
51
49
  onAdd?: () => void;
52
50
  onSave?: () => void;
53
51
  customActions?: Atom.Atom<ActionGraphProps>;
@@ -64,13 +62,14 @@ const createTableToolbarActions = ({
64
62
  nodes.push(add);
65
63
  }
66
64
  if (onSave) {
65
+ const state = get(stateAtom);
67
66
  const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
68
67
  type: 'save-view' as const,
69
68
  icon: 'ph--floppy-disk--regular',
70
69
  label: ['save view label', { ns: translationKey }],
71
70
  testId: 'table.toolbar.save-view',
72
71
  iconOnly: false,
73
- hidden: get(atomFromSignal(() => !state.viewDirty)),
72
+ hidden: !state.viewDirty,
74
73
  });
75
74
  nodes.push(save);
76
75
  }
@@ -97,10 +96,17 @@ export const TableToolbar = ({
97
96
  onSave,
98
97
  customActions,
99
98
  }: TableToolbarProps) => {
100
- 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
+
101
107
  const actionsCreator = useMemo(
102
- () => createTableToolbarActions({ state, onAdd, onSave, customActions }),
103
- [state, onAdd, onSave, customActions],
108
+ () => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
109
+ [stateAtom, onAdd, onSave, customActions],
104
110
  );
105
111
  const menu = useMenuActions(actionsCreator);
106
112