@dxos/react-ui-table 0.8.4-main.7ace549 → 0.8.4-main.8360d9e660

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 (123) hide show
  1. package/dist/lib/browser/{chunk-5XWSB3JP.mjs → chunk-BF54SSNL.mjs} +27 -28
  2. package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-KNC7XYT6.mjs → chunk-PTHMYAAM.mjs} +620 -652
  4. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1154 -1036
  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 -19
  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-CWSGS3Y2.mjs → chunk-GI72MQSG.mjs} +620 -651
  12. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-MKTMOY46.mjs → chunk-SIRDIPHZ.mjs} +27 -28
  14. package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1154 -1036
  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 -19
  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 +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 +7 -7
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +2 -16
  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 +6 -3
  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 -11
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +6 -3
  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 +2 -2
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +3 -3
  44. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  45. package/dist/types/src/components/testing.d.ts +22 -13
  46. package/dist/types/src/components/testing.d.ts.map +1 -1
  47. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  48. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useProjectionModel.d.ts +2 -1
  50. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useTableModel.d.ts +6 -5
  52. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  53. package/dist/types/src/model/modal-controller.d.ts +5 -1
  54. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  55. package/dist/types/src/model/selection-model.d.ts +17 -8
  56. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  57. package/dist/types/src/model/table-model.d.ts +100 -18
  58. package/dist/types/src/model/table-model.d.ts.map +1 -1
  59. package/dist/types/src/model/table-presentation.d.ts +3 -1
  60. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  61. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  62. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  63. package/dist/types/src/testing/useTestTableModel.d.ts +5 -8
  64. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  65. package/dist/types/src/translations.d.ts +2 -2
  66. package/dist/types/src/translations.d.ts.map +1 -1
  67. package/dist/types/src/types/Table.d.ts +13 -15
  68. package/dist/types/src/types/Table.d.ts.map +1 -1
  69. package/dist/types/src/util/dynamic-table.d.ts +12 -10
  70. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  71. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  72. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  73. package/dist/types/src/util/index.d.ts +2 -0
  74. package/dist/types/src/util/index.d.ts.map +1 -1
  75. package/dist/types/src/util/query-ast.d.ts +7 -0
  76. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  77. package/dist/types/src/util/sort.d.ts +12 -0
  78. package/dist/types/src/util/sort.d.ts.map +1 -0
  79. package/dist/types/tsconfig.tsbuildinfo +1 -1
  80. package/package.json +54 -55
  81. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  82. package/src/components/Table/ColumnSettings.tsx +8 -9
  83. package/src/components/Table/DynamicTable.stories.tsx +11 -11
  84. package/src/components/Table/DynamicTable.tsx +30 -30
  85. package/src/components/Table/Relations.stories.tsx +34 -31
  86. package/src/components/Table/RowActionsMenu.tsx +3 -2
  87. package/src/components/Table/Table.stories.tsx +80 -67
  88. package/src/components/Table/Table.tsx +324 -299
  89. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
  90. package/src/components/TableCellEditor/FormCellEditor.tsx +118 -135
  91. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +10 -7
  92. package/src/components/TableCellEditor/TableCellEditor.tsx +27 -27
  93. package/src/components/TableToolbar/TableToolbar.tsx +24 -18
  94. package/src/components/testing.ts +37 -11
  95. package/src/hooks/useAddRow.ts +8 -10
  96. package/src/hooks/useProjectionModel.ts +29 -9
  97. package/src/hooks/useTableModel.ts +27 -23
  98. package/src/model/modal-controller.ts +23 -12
  99. package/src/model/selection-model.ts +75 -44
  100. package/src/model/table-model.test.ts +81 -53
  101. package/src/model/table-model.ts +410 -138
  102. package/src/model/table-presentation.test.ts +58 -48
  103. package/src/model/table-presentation.ts +34 -28
  104. package/src/playwright/TableManager.ts +12 -5
  105. package/src/playwright/playwright.config.ts +1 -1
  106. package/src/playwright/smoke.spec.ts +48 -4
  107. package/src/testing/useTestTableModel.ts +24 -25
  108. package/src/translations.ts +1 -1
  109. package/src/types/Table.ts +28 -38
  110. package/src/util/dynamic-table.test.ts +70 -0
  111. package/src/util/dynamic-table.ts +36 -19
  112. package/src/util/index.ts +2 -0
  113. package/src/util/query-ast.ts +22 -0
  114. package/src/util/sort.ts +90 -0
  115. package/src/util/table-buttons.ts +1 -1
  116. package/src/util/table-controls.ts +2 -2
  117. package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +0 -7
  118. package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +0 -7
  119. package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +0 -7
  120. package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +0 -7
  121. package/dist/types/src/model/table-sorting.d.ts +0 -56
  122. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  123. package/src/model/table-sorting.ts +0 -234
@@ -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, JsonSchema, Query, Type } from '@dxos/echo';
10
10
  import { createEchoSchema } from '@dxos/echo/testing';
11
- import { live } from '@dxos/live-object';
12
- import { ProjectionModel, View } from '@dxos/schema';
11
+ import { ProjectionModel, ViewModel, 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: 'com.example.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 view = View.make({
102
+ const view = ViewModel.make({
99
103
  query: Query.select(Filter.type(schema)),
100
104
  jsonSchema: schema.jsonSchema,
101
105
  });
102
106
  const object = Table.make({ view });
103
- const projection = new ProjectionModel(schema.jsonSchema, view.projection);
104
- return new TableModel({ object, projection, ...props });
107
+ const projection = new ProjectionModel({
108
+ registry,
109
+ view,
110
+ baseSchema: schema.jsonSchema,
111
+ change: createDirectChangeCallback(view.projection, JsonSchema.toJsonSchema(schema)),
112
+ });
113
+ projection.normalizeView();
114
+ return new TableModel({ registry, object, projection, ...props });
105
115
  };
@@ -2,10 +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 { type View } from '@dxos/echo';
9
10
  import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
10
11
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
12
  import {
@@ -15,8 +16,8 @@ import {
15
16
  type DxGridPlaneRange,
16
17
  toPlaneCellIndex,
17
18
  } from '@dxos/react-ui-grid';
18
- import { mx } from '@dxos/react-ui-theme';
19
- import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
19
+ import { VIEW_FIELD_LIMIT } from '@dxos/schema';
20
+ import { mx } from '@dxos/ui-theme';
20
21
 
21
22
  import { tableButtons, tableControls } from '../util';
22
23
 
@@ -29,15 +30,20 @@ import { type TableModel, type TableRow } from './table-model';
29
30
  * different grid planes.
30
31
  */
31
32
  export class TablePresentation<T extends TableRow = TableRow> {
33
+ private readonly _registry: Registry.Registry;
34
+ private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
32
35
  private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
33
36
 
34
37
  constructor(
38
+ registry: Registry.Registry,
35
39
  private readonly model: TableModel<T>,
36
- private readonly _visibleRange = signal<DxGridPlaneRange>({
40
+ ) {
41
+ this._registry = registry;
42
+ this._visibleRange = Atom.make<DxGridPlaneRange>({
37
43
  start: { row: 0, col: 0 },
38
44
  end: { row: 0, col: 0 },
39
- }),
40
- ) {}
45
+ });
46
+ }
41
47
 
42
48
  public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
43
49
  // Clear cache at the start of each render pass
@@ -47,7 +53,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
47
53
 
48
54
  switch (plane) {
49
55
  case 'grid':
50
- this._visibleRange.value = range;
56
+ this._registry.set(this._visibleRange, range);
51
57
  cells = this.getMainGridCells(range);
52
58
  break;
53
59
  case 'frozenRowsStart':
@@ -102,7 +108,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
102
108
  private createDataCell(
103
109
  cells: DxGridPlaneCells,
104
110
  obj: T,
105
- field: FieldType,
111
+ field: View.FieldType,
106
112
  colIndex: number,
107
113
  displayIndex: number,
108
114
  ): void {
@@ -205,7 +211,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
205
211
  const targetObj = getValue(obj, field.path)?.target;
206
212
  if (targetObj) {
207
213
  const dxn = Obj.getDXN(targetObj)?.toString();
208
- cell.accessoryHtml = `<div role="none" class="absolute inline-end-0 inset-block-0 p-[--dx-grid-cell-content-padding-block]"><dx-anchor refId=${dxn} class="dx-button is-6 aspect-square min-bs-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
214
+ cell.accessoryHtml = `<div role="none" class="absolute end-0 inset-y-0 p-(--dx-grid-cell-content-padding-block)"><dx-anchor dxn=${dxn} class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
209
215
  }
210
216
  }
211
217
 
@@ -260,7 +266,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
260
266
 
261
267
  private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
262
268
  const cells: DxGridPlaneCells = {};
263
- const fields = this.model.projection?.fields ?? [];
269
+ const fields = this.model.projection?.getFields() ?? [];
264
270
 
265
271
  for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
266
272
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
@@ -269,7 +275,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
269
275
  continue;
270
276
  }
271
277
 
272
- this.createDataCell(cells, this.model.rows.value[row], field, col, row);
278
+ this.createDataCell(cells, this.model.getRows()[row], field, col, row);
273
279
  }
274
280
  }
275
281
 
@@ -278,8 +284,8 @@ export class TablePresentation<T extends TableRow = TableRow> {
278
284
 
279
285
  private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
280
286
  const cells: DxGridPlaneCells = {};
281
- const fields = this.model.projection?.fields ?? [];
282
- const draftRows = this.model.draftRows.value;
287
+ const fields = this.model.projection?.getFields() ?? [];
288
+ const draftRows = this.model.getDraftRows();
283
289
 
284
290
  // Return cells of the CTA row if no draft row is active
285
291
  if (draftRows.length === 0) {
@@ -319,22 +325,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
319
325
 
320
326
  private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
321
327
  const cells: DxGridPlaneCells = {};
322
- const fields = this.model.projection?.fields ?? [];
328
+ const fields = this.model.projection?.getFields() ?? [];
329
+ const currentSort = this.model.getSorting();
323
330
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
324
331
  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;
332
+ const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
327
333
 
328
334
  cells[toPlaneCellIndex({ col, row: 0 })] = {
329
335
  // TODO(burdon): Use same logic as form for fallback title.
330
336
  value: '',
331
337
  resizeHandle: 'col',
332
338
  accessoryHtml: `
333
- <span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
339
+ <span class="grow min-w-0 truncate">${props.title ?? field.path}</span>
334
340
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
335
341
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
336
342
  `,
337
- className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
343
+ className: '!bg-toolbar-surface text-description! [&>div]:flex [&>div]:items-stretch',
338
344
  };
339
345
  }
340
346
 
@@ -377,7 +383,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
377
383
  if (!this.model.features.selection.enabled || this.model.selection.selectionMode === 'single') {
378
384
  return {
379
385
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
380
- className: '!bg-toolbarSurface',
386
+ className: '!bg-toolbar-surface',
381
387
  readonly: true,
382
388
  value: '',
383
389
  },
@@ -389,9 +395,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
389
395
  accessoryHtml: tableControls.checkbox.render({
390
396
  rowIndex: 0,
391
397
  header: true,
392
- checked: this.model.selection.allRowsSeleted.value,
398
+ checked: this.model.selection.allRowsSelected,
393
399
  }),
394
- className: '!bg-toolbarSurface',
400
+ className: '!bg-toolbar-surface',
395
401
  readonly: true,
396
402
  value: '',
397
403
  },
@@ -403,10 +409,10 @@ export class TablePresentation<T extends TableRow = TableRow> {
403
409
  [toPlaneCellIndex({ col: 0, row: 0 })]: {
404
410
  accessoryHtml: this.model.features.schemaEditable
405
411
  ? tableButtons.addColumn.render({
406
- disabled: (this.model.projection?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
412
+ disabled: (this.model.projection?.getFields()?.length ?? 0) >= VIEW_FIELD_LIMIT,
407
413
  })
408
414
  : undefined,
409
- className: '!bg-toolbarSurface',
415
+ className: '!bg-toolbar-surface',
410
416
  readonly: true,
411
417
  value: '',
412
418
  },
@@ -415,7 +421,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
415
421
 
416
422
  private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
417
423
  const cells: DxGridPlaneCells = {};
418
- const draftRows = this.model.draftRows.value;
424
+ const draftRows = this.model.getDraftRows();
419
425
 
420
426
  // Return cells of the CTA row if no draft row is active
421
427
  if (draftRows.length === 0) {
@@ -442,7 +448,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
442
448
 
443
449
  private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
444
450
  const cells: DxGridPlaneCells = {};
445
- const draftRows = this.model.draftRows.value;
451
+ const draftRows = this.model.getDraftRows();
446
452
 
447
453
  for (let row = range.start.row; row <= range.end.row; row++) {
448
454
  cells[toPlaneCellIndex({ col: 0, row })] = {
@@ -460,7 +466,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
460
466
  export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
461
467
  if (!selected) {
462
468
  if (selectionMode === 'single') {
463
- return ['!cursor-pointer'];
469
+ return ['cursor-pointer!'];
464
470
  } else {
465
471
  return undefined;
466
472
  }
@@ -468,8 +474,8 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
468
474
 
469
475
  switch (selectionMode) {
470
476
  case 'single':
471
- return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
477
+ return ['dx-grid__cell--no-focus-unfurl !bg-current-surface hover:bg-hover-surface cursor-pointer!'];
472
478
  case 'multiple':
473
- return ['!bg-gridCellSelected'];
479
+ return ['!bg-grid-cell-selected'];
474
480
  }
475
481
  };
@@ -17,6 +17,7 @@ const TABLE_SELECTORS = {
17
17
  },
18
18
  columnDelete: 'column-delete',
19
19
  rowDelete: 'row-menu-delete',
20
+ addRow: 'table.toolbar.add-row',
20
21
  } as const;
21
22
 
22
23
  export class TableManager {
@@ -53,12 +54,18 @@ export class TableManager {
53
54
  await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
54
55
  }
55
56
 
56
- public async addColumn({ label, format }: { label: string; format: string }): Promise<void> {
57
+ public async addRow(): Promise<void> {
58
+ await this.page.getByTestId(TABLE_SELECTORS.addRow).click();
59
+ }
60
+
61
+ public async addColumn({ label, format }: { label: string; format?: string }): Promise<void> {
57
62
  await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
58
- await this.page.getByRole('combobox').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);
63
+ if (format) {
64
+ await this.page.getByRole('combobox').click();
65
+ await this.page.getByLabel(format).click();
66
+ }
67
+ await this.page.getByPlaceholder('Property label').click();
68
+ await this.page.getByPlaceholder('Property label').fill(label);
62
69
  await this.page.getByRole('button', { name: 'Save' }).click();
63
70
  }
64
71
  }
@@ -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: 'pnpm storybook dev --ci --quiet --port=9004 --config-dir=.storybook',
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();
@@ -120,8 +121,22 @@ test.describe('Table', () => {
120
121
  await page.close();
121
122
  });
122
123
 
123
- // Rest of add column test remains the same as it's a more complex flow.
124
- test('add column', async ({ browser, browserName }) => {
124
+ test('add column without changing format', async ({ browser, browserName }) => {
125
+ test.skip(browserName === 'webkit');
126
+ test.skip(browserName === 'firefox');
127
+ const { page } = await setupPage(browser, { url: storyUrl });
128
+ const table = new TableManager(page);
129
+
130
+ await table.grid.ready();
131
+ const newColumnLabel = 'DEFAULT FORMAT COL';
132
+
133
+ await table.addColumn({ label: newColumnLabel });
134
+
135
+ await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
136
+ await page.close();
137
+ });
138
+
139
+ test('add column with format', async ({ browser, browserName }) => {
125
140
  test.skip(browserName === 'webkit');
126
141
  test.skip(browserName === 'firefox');
127
142
  const { page } = await setupPage(browser, { url: storyUrl });
@@ -153,12 +168,41 @@ test.describe('Table', () => {
153
168
  // Assert the first two switch checkboxes are checked.
154
169
  await expect(page.getByTestId('table-switch').first()).toBeChecked();
155
170
  await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
156
- await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Sapiente.');
171
+ await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Aut.');
157
172
  await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
158
173
 
159
174
  await page.close();
160
175
  });
161
176
 
177
+ test('add row and edit cell', async ({ browser, browserName }) => {
178
+ test.skip(browserName === 'webkit');
179
+ test.skip(browserName === 'firefox');
180
+ const { page } = await setupPage(browser, { url: storyUrl });
181
+ const table = new TableManager(page);
182
+
183
+ await table.grid.ready();
184
+
185
+ // Click the CTA row at the bottom to add a new row.
186
+ await table.grid.cell(0, 0, 'frozenRowsEnd').click();
187
+
188
+ // All schema fields are optional so the row is added directly to the grid (not as a draft).
189
+ const newCell = table.grid.cell(0, 10, 'grid');
190
+ await newCell.waitFor({ state: 'visible' });
191
+
192
+ // Focus should already be on the new row; engage edit mode.
193
+ await page.keyboard.press('Enter');
194
+ await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
195
+
196
+ // Type text and confirm.
197
+ const cellContent = 'New row content';
198
+ await page.keyboard.type(cellContent);
199
+ await page.keyboard.press('Enter');
200
+
201
+ // Assert the cell content is saved.
202
+ await expect(newCell).toHaveText(cellContent);
203
+ await page.close();
204
+ });
205
+
162
206
  // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
163
207
  test.skip('extant relations work as expected', async ({ browser, browserName }) => {
164
208
  test.skip(browserName === 'webkit');
@@ -2,15 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import type * as Schema from 'effect/Schema';
6
- import { type RefObject, useCallback, useMemo, useRef } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { type RefObject, useCallback, useContext, useMemo, useRef } from 'react';
7
7
 
8
+ import { type Database, type Type } from '@dxos/echo';
8
9
  import { isMutable } from '@dxos/echo/internal';
9
- import { useGlobalFilteredObjects } from '@dxos/plugin-search';
10
10
  import { faker } from '@dxos/random';
11
- import { type Client, useClient } from '@dxos/react-client';
12
- import { Filter, type Space, useQuery, useSchema } from '@dxos/react-client/echo';
13
- import { useClientProvider } from '@dxos/react-client/testing';
11
+ import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
12
+ import { useClientStory } from '@dxos/react-client/testing';
13
+ import { useGlobalFilteredObjects } from '@dxos/react-ui-searchlist';
14
14
  import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
15
15
 
16
16
  import { type TableController } from '../components';
@@ -20,15 +20,14 @@ import { Table } from '../types';
20
20
 
21
21
  faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
22
22
 
23
- export type TestTableModel = {
24
- schema: Schema.Schema.AnyNoContext | undefined;
23
+ export type TestTableModel<T extends Type.AnyObj = Type.AnyObj> = {
24
+ schema: T | undefined;
25
25
  table: Table.Table | undefined;
26
26
  projection: ProjectionModel | undefined;
27
27
  tableRef: RefObject<TableController | null>;
28
28
  model: TableModel | undefined;
29
29
  presentation: TablePresentation | undefined;
30
- space: Space | undefined;
31
- client: Client | undefined;
30
+ db: Database.Database | undefined;
32
31
  handleInsertRow: () => void;
33
32
  handleSaveView: () => void;
34
33
  handleDeleteRows: (rowIndex: number, objects: any[]) => void;
@@ -39,15 +38,16 @@ export type TestTableModel = {
39
38
  * Custom hook to create and manage a test table model for storybook demonstrations.
40
39
  * Provides table data, schema, and handlers for table operations.
41
40
  */
42
- export const useTestTableModel = (): TestTableModel => {
43
- const client = useClient();
44
- const { space } = useClientProvider();
41
+ export const useTestTableModel = <T extends Type.AnyObj = Type.AnyObj>(): TestTableModel<T> => {
42
+ const registry = useContext(RegistryContext);
43
+ const { space } = useClientStory();
44
+ const db = space?.db;
45
45
 
46
- const tables = useQuery(space, Filter.type(Table.Table));
46
+ const tables = useQuery(space?.db, Filter.type(Table.Table));
47
47
  const table = tables.at(0);
48
48
  const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
49
- const schema = useSchema(client, space, typename);
50
- const projection = useProjectionModel(schema, table);
49
+ const schema = useSchema<T>(space?.db, typename);
50
+ const projection = useProjectionModel(schema, table, registry);
51
51
 
52
52
  const features = useMemo(
53
53
  () => ({
@@ -58,7 +58,7 @@ export const useTestTableModel = (): TestTableModel => {
58
58
  [schema],
59
59
  );
60
60
 
61
- const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
61
+ const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
62
62
  const filteredObjects = useGlobalFilteredObjects(objects);
63
63
 
64
64
  const tableRef = useRef<TableController>(null);
@@ -70,15 +70,15 @@ export const useTestTableModel = (): TestTableModel => {
70
70
  tableRef.current?.update?.();
71
71
  }, []);
72
72
 
73
- const addRow = useAddRow({ space, schema });
73
+ const addRow = useAddRow({ db, schema });
74
74
 
75
75
  const handleDeleteRows = useCallback(
76
76
  (_: number, objects: any[]) => {
77
77
  for (const object of objects) {
78
- space?.db.remove(object);
78
+ db?.remove(object);
79
79
  }
80
80
  },
81
- [space],
81
+ [db],
82
82
  );
83
83
 
84
84
  const handleDeleteColumn = useCallback(
@@ -97,7 +97,7 @@ export const useTestTableModel = (): TestTableModel => {
97
97
  rows: filteredObjects,
98
98
  onInsertRow: addRow,
99
99
  onDeleteRows: handleDeleteRows,
100
- onDeleteColumn: handleDeleteColumn,
100
+ onColumnDelete: handleDeleteColumn,
101
101
  onCellUpdate: handleCellUpdate,
102
102
  onRowOrderChange: handleRowOrderChange,
103
103
  });
@@ -113,9 +113,9 @@ export const useTestTableModel = (): TestTableModel => {
113
113
 
114
114
  const presentation = useMemo(() => {
115
115
  if (model) {
116
- return new TablePresentation(model);
116
+ return new TablePresentation(registry, model);
117
117
  }
118
- }, [model]);
118
+ }, [registry, model]);
119
119
 
120
120
  return {
121
121
  schema,
@@ -124,8 +124,7 @@ export const useTestTableModel = (): TestTableModel => {
124
124
  tableRef,
125
125
  model,
126
126
  presentation,
127
- space,
128
- client,
127
+ db,
129
128
  handleInsertRow,
130
129
  handleSaveView,
131
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
  {