@dxos/react-ui-table 0.8.4-main.ae835ea → 0.8.4-main.bc674ce

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 (129) hide show
  1. package/dist/lib/browser/{chunk-G7MGPBSR.mjs → chunk-I5IFW7UF.mjs} +633 -663
  2. package/dist/lib/browser/chunk-I5IFW7UF.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 +1131 -999
  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 -41
  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-LD5JEBA6.mjs → chunk-DDJKYJFY.mjs} +633 -662
  12. package/dist/lib/node-esm/chunk-DDJKYJFY.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 +1131 -999
  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 -41
  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 +4 -18
  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 +12 -9
  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 +8 -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 +71 -5
  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 +10 -12
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -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 +4 -4
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  44. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  45. package/dist/types/src/components/testing.d.ts +22 -15
  46. package/dist/types/src/components/testing.d.ts.map +1 -1
  47. package/dist/types/src/hooks/index.d.ts +2 -1
  48. package/dist/types/src/hooks/index.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  50. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  52. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  53. package/dist/types/src/hooks/useTableModel.d.ts +8 -8
  54. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  55. package/dist/types/src/model/modal-controller.d.ts +5 -1
  56. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  57. package/dist/types/src/model/selection-model.d.ts +17 -8
  58. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  59. package/dist/types/src/model/table-model.d.ts +102 -23
  60. package/dist/types/src/model/table-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-presentation.d.ts +3 -1
  62. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  63. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  64. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  65. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  66. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  67. package/dist/types/src/translations.d.ts +4 -4
  68. package/dist/types/src/translations.d.ts.map +1 -1
  69. package/dist/types/src/types/Table.d.ts +31 -0
  70. package/dist/types/src/types/Table.d.ts.map +1 -0
  71. package/dist/types/src/types/index.d.ts +1 -1
  72. package/dist/types/src/types/index.d.ts.map +1 -1
  73. package/dist/types/src/util/dynamic-table.d.ts +13 -11
  74. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  75. package/dist/types/src/util/index.d.ts +2 -0
  76. package/dist/types/src/util/index.d.ts.map +1 -1
  77. package/dist/types/src/util/query-ast.d.ts +7 -0
  78. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  79. package/dist/types/src/util/sort.d.ts +12 -0
  80. package/dist/types/src/util/sort.d.ts.map +1 -0
  81. package/dist/types/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +53 -53
  83. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  84. package/src/components/Table/ColumnSettings.tsx +6 -6
  85. package/src/components/Table/DynamicTable.stories.tsx +12 -13
  86. package/src/components/Table/DynamicTable.tsx +21 -14
  87. package/src/components/Table/Relations.stories.tsx +35 -39
  88. package/src/components/Table/RowActionsMenu.tsx +24 -15
  89. package/src/components/Table/Table.stories.tsx +71 -75
  90. package/src/components/Table/Table.tsx +335 -305
  91. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  92. package/src/components/TableCellEditor/FormCellEditor.tsx +110 -133
  93. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +16 -12
  94. package/src/components/TableCellEditor/TableCellEditor.tsx +31 -31
  95. package/src/components/TableToolbar/TableToolbar.tsx +44 -31
  96. package/src/components/testing.ts +38 -12
  97. package/src/hooks/index.ts +2 -1
  98. package/src/hooks/useAddRow.ts +8 -10
  99. package/src/hooks/useProjectionModel.ts +39 -0
  100. package/src/hooks/useTableModel.ts +35 -33
  101. package/src/model/modal-controller.ts +23 -12
  102. package/src/model/selection-model.ts +75 -44
  103. package/src/model/table-model.test.ts +82 -54
  104. package/src/model/table-model.ts +428 -163
  105. package/src/model/table-presentation.test.ts +59 -49
  106. package/src/model/table-presentation.ts +31 -26
  107. package/src/playwright/TableManager.ts +5 -5
  108. package/src/playwright/playwright.config.ts +1 -1
  109. package/src/playwright/smoke.spec.ts +8 -5
  110. package/src/testing/useTestTableModel.ts +44 -37
  111. package/src/translations.ts +3 -3
  112. package/src/types/Table.ts +90 -0
  113. package/src/types/index.ts +1 -1
  114. package/src/util/dynamic-table.ts +39 -27
  115. package/src/util/index.ts +2 -0
  116. package/src/util/query-ast.ts +19 -0
  117. package/src/util/sort.ts +90 -0
  118. package/dist/lib/browser/chunk-4K7JPALT.mjs +0 -66
  119. package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
  120. package/dist/lib/browser/chunk-G7MGPBSR.mjs.map +0 -7
  121. package/dist/lib/node-esm/chunk-LD5JEBA6.mjs.map +0 -7
  122. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
  123. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
  124. package/dist/types/src/model/table-sorting.d.ts +0 -52
  125. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  126. package/dist/types/src/types/schema.d.ts +0 -28
  127. package/dist/types/src/types/schema.d.ts.map +0 -1
  128. package/src/model/table-sorting.ts +0 -225
  129. package/src/types/schema.ts +0 -73
@@ -2,14 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Registry } from '@effect-atom/atom-react';
5
6
  import * as Schema from 'effect/Schema';
6
7
  import { beforeEach, describe, expect, it } from 'vitest';
7
8
 
8
- import { Filter, Query } from '@dxos/echo';
9
- import { TypedObject } from '@dxos/echo/internal';
9
+ import { Filter, Query, Type } from '@dxos/echo';
10
10
  import { createEchoSchema } from '@dxos/echo/testing';
11
- import { live } from '@dxos/live-object';
12
- import { createView } from '@dxos/schema';
11
+ import { ProjectionModel, View, createDirectChangeCallback } from '@dxos/schema';
13
12
 
14
13
  import { Table } from '../types';
15
14
 
@@ -17,48 +16,53 @@ import { TableModel, type TableModelProps } from './table-model';
17
16
  import { TablePresentation } from './table-presentation';
18
17
 
19
18
  describe('TablePresentation', () => {
20
- describe('row reactivity', () => {
19
+ describe('getCells', () => {
20
+ let registry: Registry.Registry;
21
21
  let data: any[];
22
- let updateCount: number;
23
22
  let model: any;
24
23
  let presentation: TablePresentation;
25
24
 
26
25
  beforeEach(async () => {
27
- updateCount = 0;
28
- ({ data } = live({
29
- data: [
30
- { title: 'A', count: 1 },
31
- { title: 'B', count: 2 },
32
- ],
33
- }));
34
-
35
- model = createTableModel({
36
- onCellUpdate: () => updateCount++,
37
- });
38
- model.setRows(data);
26
+ registry = Registry.make();
27
+ data = [
28
+ { id: '1', title: 'A', count: 1 },
29
+ { id: '2', title: 'B', count: 2 },
30
+ ];
31
+
32
+ model = createTableModel(registry, {});
39
33
  await model.open();
40
- presentation = new TablePresentation(model);
34
+ model.setRows(data);
35
+ presentation = new TablePresentation(registry, model);
41
36
  });
42
37
 
43
- it('should update with row-level reactivity', () => {
44
- // Set up visible range to include our test data.
45
- presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
38
+ it('should return cells for the visible range', () => {
39
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
40
+
41
+ // Verify that cells are returned for the range. The exact column indices depend on the projection.
42
+ const cellKeys = Object.keys(cells);
43
+ expect(cellKeys.length).toBeGreaterThan(0);
44
+
45
+ // Check that values from the data are present in the cells.
46
+ const values = Object.values(cells).map((c) => c.value);
47
+ expect(values).toContain('A');
48
+ expect(values).toContain('B');
49
+ });
46
50
 
47
- // Trigger a row update.
51
+ it('should reflect row data changes', () => {
52
+ // Update the row data and refresh.
48
53
  data[0].title = 'New Value';
49
- expect(updateCount).toBe(2);
54
+ model.setRows(data);
50
55
 
51
56
  // Verify the new value through getCells.
52
- const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
53
- expect(cells['0,0']?.value).toBe('New Value');
57
+ const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 2 } }, 'grid');
58
+ const values = Object.values(cells).map((c) => c.value);
59
+ expect(values).toContain('New Value');
54
60
  });
55
61
 
56
- it('should update reactively when adding a new row', () => {
57
- // Set up visible range to include our test data.
58
- presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
59
-
60
- data.push({ title: 'C', count: 3 });
61
- expect(updateCount).toBe(2);
62
+ it('should reflect new rows', () => {
63
+ // Add a new row.
64
+ data.push({ id: '3', title: 'C', count: 3 });
65
+ model.setRows(data);
62
66
 
63
67
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
64
68
 
@@ -67,17 +71,12 @@ describe('TablePresentation', () => {
67
71
  expect(cells['1,2']?.value).toBe('3');
68
72
  });
69
73
 
70
- it('should handle combined operations reactively', () => {
71
- // Set up visible range.
72
- presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
73
-
74
- // Multiple operations.
74
+ it('should handle row modifications', () => {
75
+ // Modify the data.
75
76
  data[0].title = 'Updated A';
76
- data.push({ title: 'C', count: 3, completed: true });
77
+ data.push({ id: '3', title: 'C', count: 3 });
77
78
  data.splice(1, 1);
78
-
79
- // We expect one update per operation affecting visible rows.
80
- expect(updateCount).toBe(4);
79
+ model.setRows(data);
81
80
 
82
81
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
83
82
 
@@ -88,18 +87,29 @@ describe('TablePresentation', () => {
88
87
  });
89
88
  });
90
89
 
91
- class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
90
+ const Test = Schema.Struct({
92
91
  title: Schema.String,
93
92
  count: Schema.Number,
94
- }) {}
95
-
96
- const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
93
+ }).pipe(
94
+ Type.object({
95
+ typename: 'example.com/type/Test',
96
+ version: '0.1.0',
97
+ }),
98
+ );
99
+
100
+ const createTableModel = (registry: Registry.Registry, props: Partial<TableModelProps> = {}): TableModel => {
97
101
  const schema = createEchoSchema(Test);
98
- const table = Table.make();
99
- const view = createView({
102
+ const view = View.make({
100
103
  query: Query.select(Filter.type(schema)),
101
104
  jsonSchema: schema.jsonSchema,
102
- presentation: table,
103
105
  });
104
- return new TableModel({ view, schema: schema.jsonSchema, ...props });
106
+ const object = Table.make({ view });
107
+ const projection = new ProjectionModel({
108
+ registry,
109
+ view,
110
+ baseSchema: schema.jsonSchema,
111
+ change: createDirectChangeCallback(view.projection, schema.jsonSchema),
112
+ });
113
+ projection.normalizeView();
114
+ return new TableModel({ registry, object, projection, ...props });
105
115
  };
@@ -2,11 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { signal } from '@preact/signals-core';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
6
  import * as Predicate from 'effect/Predicate';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
- import { FormatEnum, TypeEnum, getValue } from '@dxos/echo/internal';
9
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
10
10
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
11
  import {
12
12
  type DxGridCellValue,
@@ -15,8 +15,8 @@ import {
15
15
  type DxGridPlaneRange,
16
16
  toPlaneCellIndex,
17
17
  } from '@dxos/react-ui-grid';
18
- import { mx } from '@dxos/react-ui-theme';
19
18
  import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
19
+ import { mx } from '@dxos/ui-theme';
20
20
 
21
21
  import { tableButtons, tableControls } from '../util';
22
22
 
@@ -29,15 +29,20 @@ import { type TableModel, type TableRow } from './table-model';
29
29
  * different grid planes.
30
30
  */
31
31
  export class TablePresentation<T extends TableRow = TableRow> {
32
+ private readonly _registry: Registry.Registry;
33
+ private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
32
34
  private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
33
35
 
34
36
  constructor(
37
+ registry: Registry.Registry,
35
38
  private readonly model: TableModel<T>,
36
- private readonly _visibleRange = signal<DxGridPlaneRange>({
39
+ ) {
40
+ this._registry = registry;
41
+ this._visibleRange = Atom.make<DxGridPlaneRange>({
37
42
  start: { row: 0, col: 0 },
38
43
  end: { row: 0, col: 0 },
39
- }),
40
- ) {}
44
+ });
45
+ }
41
46
 
42
47
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
43
48
  // Clear cache at the start of each render pass
@@ -47,7 +52,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
47
52
 
48
53
  switch (plane) {
49
54
  case 'grid':
50
- this._visibleRange.value = range;
55
+ this._registry.set(this._visibleRange, range);
51
56
  cells = this.getMainGridCells(range);
52
57
  break;
53
58
  case 'frozenRowsStart':
@@ -120,12 +125,12 @@ export class TablePresentation<T extends TableRow = TableRow> {
120
125
  }
121
126
 
122
127
  switch (props.format) {
123
- case FormatEnum.Boolean:
124
- case FormatEnum.SingleSelect:
125
- case FormatEnum.MultiSelect: {
128
+ case Format.TypeFormat.Boolean:
129
+ case Format.TypeFormat.SingleSelect:
130
+ case Format.TypeFormat.MultiSelect: {
126
131
  return '';
127
132
  }
128
- case FormatEnum.Ref: {
133
+ case Format.TypeFormat.Ref: {
129
134
  if (!field.referencePath) {
130
135
  return ''; // TODO(burdon): Show error.
131
136
  }
@@ -201,7 +206,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
201
206
  }
202
207
 
203
208
  // References.
204
- if (props.format === FormatEnum.Ref && props.referenceSchema) {
209
+ if (props.format === Format.TypeFormat.Ref && props.referenceSchema) {
205
210
  const targetObj = getValue(obj, field.path)?.target;
206
211
  if (targetObj) {
207
212
  const dxn = Obj.getDXN(targetObj)?.toString();
@@ -210,7 +215,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
210
215
  }
211
216
 
212
217
  // Booleans.
213
- if (props.format === FormatEnum.Boolean) {
218
+ if (props.format === Format.TypeFormat.Boolean) {
214
219
  const value = getValue(obj, field.path);
215
220
  cell.accessoryHtml = tableControls.switch.render({
216
221
  colIndex,
@@ -221,7 +226,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
221
226
  }
222
227
 
223
228
  // Single-Selects.
224
- if (props.format === FormatEnum.SingleSelect) {
229
+ if (props.format === Format.TypeFormat.SingleSelect) {
225
230
  const value = getValue(obj, field.path);
226
231
  const options = this.model.projection.getFieldProjection(field.id).props.options;
227
232
  if (options) {
@@ -233,7 +238,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
233
238
  }
234
239
 
235
240
  // Multi-Selects.
236
- if (props.format === FormatEnum.MultiSelect) {
241
+ if (props.format === Format.TypeFormat.MultiSelect) {
237
242
  const values = getValue(obj, field.path) as string[] | undefined;
238
243
  const options = this.model.projection.getFieldProjection(field.id).props.options;
239
244
  if (options && values && values.length > 0) {
@@ -260,7 +265,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
260
265
 
261
266
  private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
262
267
  const cells: DxGridPlaneCells = {};
263
- const fields = this.model.projection?.fields ?? [];
268
+ const fields = this.model.projection?.getFields() ?? [];
264
269
 
265
270
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
266
271
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
@@ -269,7 +274,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
269
274
  continue;
270
275
  }
271
276
 
272
- this.createDataCell(cells, this.model.rows.value[row], field, col, row);
277
+ this.createDataCell(cells, this.model.getRows()[row], field, col, row);
273
278
  }
274
279
  }
275
280
 
@@ -278,8 +283,8 @@ export class TablePresentation<T extends TableRow = TableRow> {
278
283
 
279
284
  private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
280
285
  const cells: DxGridPlaneCells = {};
281
- const fields = this.model.projection?.fields ?? [];
282
- const draftRows = this.model.draftRows.value;
286
+ const fields = this.model.projection?.getFields() ?? [];
287
+ const draftRows = this.model.getDraftRows();
283
288
 
284
289
  // Return cells of the CTA row if no draft row is active
285
290
  if (draftRows.length === 0) {
@@ -319,11 +324,11 @@ export class TablePresentation<T extends TableRow = TableRow> {
319
324
 
320
325
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
321
326
  const cells: DxGridPlaneCells = {};
322
- const fields = this.model.projection?.fields ?? [];
327
+ const fields = this.model.projection?.getFields() ?? [];
328
+ const currentSort = this.model.getSorting();
323
329
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
324
330
  const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
325
- const sorting = this.model.sorting?.sorting;
326
- const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
331
+ const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
327
332
 
328
333
  cells[toPlaneCellIndex({ col, row: 0 })] = {
329
334
  // TODO(burdon): Use same logic as form for fallback title.
@@ -389,7 +394,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
389
394
  accessoryHtml: tableControls.checkbox.render({
390
395
  rowIndex: 0,
391
396
  header: true,
392
- checked: this.model.selection.allRowsSeleted.value,
397
+ checked: this.model.selection.allRowsSelected,
393
398
  }),
394
399
  className: '!bg-toolbarSurface',
395
400
  readonly: true,
@@ -403,7 +408,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
403
408
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
404
409
  accessoryHtml: this.model.features.schemaEditable
405
410
  ? tableButtons.addColumn.render({
406
- disabled: (this.model.projection?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
411
+ disabled: (this.model.projection?.getFields()?.length ?? 0) >= VIEW_FIELD_LIMIT,
407
412
  })
408
413
  : undefined,
409
414
  className: '!bg-toolbarSurface',
@@ -415,7 +420,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
415
420
 
416
421
  private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
417
422
  const cells: DxGridPlaneCells = {};
418
- const draftRows = this.model.draftRows.value;
423
+ const draftRows = this.model.getDraftRows();
419
424
 
420
425
  // Return cells of the CTA row if no draft row is active
421
426
  if (draftRows.length === 0) {
@@ -442,7 +447,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
442
447
 
443
448
  private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
444
449
  const cells: DxGridPlaneCells = {};
445
- const draftRows = this.model.draftRows.value;
450
+ const draftRows = this.model.getDraftRows();
446
451
 
447
452
  for (let row = range.start.row; row <= range.end.row; row++) {
448
453
  cells[toPlaneCellIndex({ col: 0, row })] = {
@@ -53,12 +53,12 @@ export class TableManager {
53
53
  await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
54
54
  }
55
55
 
56
- public async addColumn({ label, format }: { label: string; format: 'number' }): Promise<void> {
56
+ public async addColumn({ label, format }: { label: string; format: string }): Promise<void> {
57
57
  await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
58
58
  await this.page.getByRole('combobox').click();
59
- await this.page.getByLabel(`format ${format}`).click();
60
- await this.page.getByPlaceholder('Property label.').click();
61
- await this.page.getByPlaceholder('Property label.').fill(label);
62
- await this.page.getByRole('button', { name: 'button save' }).click();
59
+ await this.page.getByLabel(format).click();
60
+ await this.page.getByPlaceholder('Property label').click();
61
+ await this.page.getByPlaceholder('Property label').fill(label);
62
+ await this.page.getByRole('button', { name: 'Save' }).click();
63
63
  }
64
64
  }
@@ -10,7 +10,7 @@ export default defineConfig({
10
10
  ...e2ePreset(import.meta.dirname),
11
11
  // TODO(wittjosiah): Avoid hard-coding ports.
12
12
  webServer: {
13
- command: 'moon run storybook:serve-e2e -- --port=9004',
13
+ command: 'moon run storybook-react:serve-e2e -- --port=9004',
14
14
  port: 9004,
15
15
  reuseExistingServer: false,
16
16
  },
@@ -71,6 +71,7 @@ test.describe('Table', () => {
71
71
  const table = new TableManager(page);
72
72
 
73
73
  await table.grid.ready();
74
+ await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(1);
74
75
  await table.deleteRow(0);
75
76
  await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
76
77
  await page.close();
@@ -88,7 +89,7 @@ test.describe('Table', () => {
88
89
  // Delete action affects all selected rows.
89
90
  await table.deleteRow(0);
90
91
 
91
- await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
92
+ await expect(page.getByRole('gridcell', { name: 'Aut.' })).toHaveCount(0);
92
93
  await expect(page.getByRole('gridcell', { name: 'Beatae.' })).toHaveCount(0);
93
94
  await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
94
95
  await page.close();
@@ -130,7 +131,7 @@ test.describe('Table', () => {
130
131
  await table.grid.ready();
131
132
  const newColumnLabel = 'TEST LABEL';
132
133
 
133
- await table.addColumn({ label: newColumnLabel, format: 'number' });
134
+ await table.addColumn({ label: newColumnLabel, format: 'Number' });
134
135
 
135
136
  await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
136
137
  await page.close();
@@ -153,13 +154,14 @@ test.describe('Table', () => {
153
154
  // Assert the first two switch checkboxes are checked.
154
155
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
155
156
  await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
156
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sapiente.');
157
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Aut.');
157
158
  await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
158
159
 
159
160
  await page.close();
160
161
  });
161
162
 
162
- test('extant relations work as expected', async ({ browser, browserName }) => {
163
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
164
+ test.skip('extant relations work as expected', async ({ browser, browserName }) => {
163
165
  test.skip(browserName === 'webkit');
164
166
  test.skip(browserName === 'firefox');
165
167
  const { page } = await setupPage(browser, { url: relationsStoryUrl });
@@ -213,7 +215,8 @@ test.describe('Table', () => {
213
215
  await page.close();
214
216
  });
215
217
 
216
- test('new relations work as expected', async ({ browser, browserName }) => {
218
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
219
+ test.skip('new relations work as expected', async ({ browser, browserName }) => {
217
220
  test.skip(browserName === 'webkit');
218
221
  test.skip(browserName === 'firefox');
219
222
  const { page } = await setupPage(browser, { url: relationsStoryUrl });
@@ -2,43 +2,52 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback, useMemo, useRef } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { type RefObject, useCallback, useContext, useMemo, useRef } from 'react';
6
7
 
7
- import { isMutable, toJsonSchema } from '@dxos/echo/internal';
8
- import { useGlobalFilteredObjects } from '@dxos/plugin-search';
8
+ import { type Database, type Type } from '@dxos/echo';
9
+ import { isMutable } from '@dxos/echo/internal';
9
10
  import { faker } from '@dxos/random';
10
- import { useClient } from '@dxos/react-client';
11
11
  import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
12
- import { useClientProvider } from '@dxos/react-client/testing';
13
- import { DataType, ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
12
+ import { useClientStory } from '@dxos/react-client/testing';
13
+ import { useGlobalFilteredObjects } from '@dxos/react-ui-searchlist';
14
+ import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
14
15
 
15
16
  import { type TableController } from '../components';
16
- import { useAddRow, useTableModel } from '../hooks';
17
- import { TablePresentation } from '../model';
17
+ import { useAddRow, useProjectionModel, useTableModel } from '../hooks';
18
+ import { type TableModel, TablePresentation } from '../model';
19
+ import { Table } from '../types';
18
20
 
19
21
  faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
20
22
 
23
+ export type TestTableModel<T extends Type.Obj.Any = Type.Obj.Any> = {
24
+ schema: T | undefined;
25
+ table: Table.Table | undefined;
26
+ projection: ProjectionModel | undefined;
27
+ tableRef: RefObject<TableController | null>;
28
+ model: TableModel | undefined;
29
+ presentation: TablePresentation | undefined;
30
+ db: Database.Database | undefined;
31
+ handleInsertRow: () => void;
32
+ handleSaveView: () => void;
33
+ handleDeleteRows: (rowIndex: number, objects: any[]) => void;
34
+ handleDeleteColumn: (fieldId: string) => void;
35
+ };
36
+
21
37
  /**
22
38
  * Custom hook to create and manage a test table model for storybook demonstrations.
23
39
  * Provides table data, schema, and handlers for table operations.
24
40
  */
25
- export const useTestTableModel = () => {
26
- const client = useClient();
27
- const { space } = useClientProvider();
28
-
29
- const views = useQuery(space, Filter.type(DataType.View));
30
- const view = useMemo(() => views.at(0), [views]);
31
- const typename = view?.query ? getTypenameFromQuery(view.query.ast) : undefined;
32
- const schema = useSchema(client, space, typename);
33
- const jsonSchema = useMemo(() => (schema ? toJsonSchema(schema) : undefined), [schema]);
34
-
35
- const projection = useMemo(() => {
36
- if (schema && view?.projection) {
37
- const projection = new ProjectionModel(toJsonSchema(schema), view.projection);
38
- projection.normalizeView();
39
- return projection;
40
- }
41
- }, [schema, view?.projection]);
41
+ export const useTestTableModel = <T extends Type.Obj.Any = Type.Obj.Any>(): TestTableModel<T> => {
42
+ const registry = useContext(RegistryContext);
43
+ const { space } = useClientStory();
44
+ const db = space?.db;
45
+
46
+ const tables = useQuery(space?.db, Filter.type(Table.Table));
47
+ const table = tables.at(0);
48
+ const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
49
+ const schema = useSchema<T>(space?.db, typename);
50
+ const projection = useProjectionModel(schema, table, registry);
42
51
 
43
52
  const features = useMemo(
44
53
  () => ({
@@ -49,7 +58,7 @@ export const useTestTableModel = () => {
49
58
  [schema],
50
59
  );
51
60
 
52
- const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
61
+ const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
53
62
  const filteredObjects = useGlobalFilteredObjects(objects);
54
63
 
55
64
  const tableRef = useRef<TableController>(null);
@@ -61,15 +70,15 @@ export const useTestTableModel = () => {
61
70
  tableRef.current?.update?.();
62
71
  }, []);
63
72
 
64
- const addRow = useAddRow({ space, schema });
73
+ const addRow = useAddRow({ db, schema });
65
74
 
66
75
  const handleDeleteRows = useCallback(
67
76
  (_: number, objects: any[]) => {
68
77
  for (const object of objects) {
69
- space?.db.remove(object);
78
+ db?.remove(object);
70
79
  }
71
80
  },
72
- [space],
81
+ [db],
73
82
  );
74
83
 
75
84
  const handleDeleteColumn = useCallback(
@@ -82,8 +91,7 @@ export const useTestTableModel = () => {
82
91
  );
83
92
 
84
93
  const model = useTableModel({
85
- view,
86
- schema: jsonSchema,
94
+ object: table,
87
95
  projection,
88
96
  features,
89
97
  rows: filteredObjects,
@@ -97,7 +105,7 @@ export const useTestTableModel = () => {
97
105
  const handleInsertRow = useCallback(() => {
98
106
  const insertResult = model?.insertRow();
99
107
  tableRef.current?.handleInsertRowResult?.(insertResult);
100
- }, [model, tableRef.current]);
108
+ }, [model]);
101
109
 
102
110
  const handleSaveView = useCallback(() => {
103
111
  model?.saveView();
@@ -105,19 +113,18 @@ export const useTestTableModel = () => {
105
113
 
106
114
  const presentation = useMemo(() => {
107
115
  if (model) {
108
- return new TablePresentation(model);
116
+ return new TablePresentation(registry, model);
109
117
  }
110
- }, [model]);
118
+ }, [registry, model]);
111
119
 
112
120
  return {
113
121
  schema,
114
- view,
122
+ table,
115
123
  projection,
116
124
  tableRef,
117
125
  model,
118
126
  presentation,
119
- space,
120
- client,
127
+ db,
121
128
  handleInsertRow,
122
129
  handleSaveView,
123
130
  handleDeleteRows,
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-table';
7
+ export const translationKey = '@dxos/react-ui-table';
8
8
 
9
9
  export const translations = [
10
10
  {
@@ -14,8 +14,8 @@ export const translations = [
14
14
  'create object label': 'Create table',
15
15
  'table name placeholder': 'Table name',
16
16
  'settings title': 'Table settings',
17
- 'table schema label': 'Record type',
18
- 'new schema': 'New record type',
17
+ 'table schema label': 'Type',
18
+ 'new schema': 'New type',
19
19
  'continue label': 'Continue',
20
20
  'create stack section label': 'Create table',
21
21
  'add row label': 'Add row',