@dxos/react-ui-table 0.8.4-main.21d9917 → 0.8.4-main.2244d791bb

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 (45) hide show
  1. package/dist/lib/browser/{chunk-MNZKOFMA.mjs → chunk-GTGYBRJA.mjs} +12 -10
  2. package/dist/lib/browser/{chunk-MNZKOFMA.mjs.map → chunk-GTGYBRJA.mjs.map} +3 -3
  3. package/dist/lib/browser/index.mjs +4 -4
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/testing/index.mjs +2 -2
  7. package/dist/lib/browser/testing/index.mjs.map +2 -2
  8. package/dist/lib/node-esm/{chunk-2ATAIIAO.mjs → chunk-DPNSZHU5.mjs} +12 -10
  9. package/dist/lib/node-esm/{chunk-2ATAIIAO.mjs.map → chunk-DPNSZHU5.mjs.map} +3 -3
  10. package/dist/lib/node-esm/index.mjs +4 -4
  11. package/dist/lib/node-esm/index.mjs.map +3 -3
  12. package/dist/lib/node-esm/meta.json +1 -1
  13. package/dist/lib/node-esm/testing/index.mjs +2 -2
  14. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  15. package/dist/types/src/components/Table/DynamicTable.d.ts +3 -2
  16. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  17. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  19. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  20. package/dist/types/src/hooks/useTableModel.d.ts +1 -1
  21. package/dist/types/src/model/table-model.d.ts +3 -3
  22. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  23. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  24. package/dist/types/src/util/dynamic-table.d.ts +4 -3
  25. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  26. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  27. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  28. package/dist/types/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +41 -41
  30. package/src/components/Table/DynamicTable.stories.tsx +5 -4
  31. package/src/components/Table/DynamicTable.tsx +3 -2
  32. package/src/components/Table/Relations.stories.tsx +1 -1
  33. package/src/components/Table/Table.stories.tsx +20 -7
  34. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +1 -1
  35. package/src/hooks/useProjectionModel.ts +4 -5
  36. package/src/hooks/useTableModel.ts +1 -1
  37. package/src/model/table-model.test.ts +1 -1
  38. package/src/model/table-model.ts +7 -7
  39. package/src/model/table-presentation.test.ts +1 -1
  40. package/src/model/table-presentation.ts +1 -1
  41. package/src/playwright/TableManager.ts +10 -3
  42. package/src/playwright/smoke.spec.ts +45 -2
  43. package/src/testing/useTestTableModel.ts +1 -1
  44. package/src/util/dynamic-table.test.ts +70 -0
  45. package/src/util/dynamic-table.ts +7 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-table",
3
- "version": "0.8.4-main.21d9917",
3
+ "version": "0.8.4-main.2244d791bb",
4
4
  "description": "A table component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -60,7 +60,7 @@
60
60
  "@codemirror/autocomplete": "^6.19.0",
61
61
  "@codemirror/state": "^6.5.2",
62
62
  "@codemirror/view": "^6.38.4",
63
- "@effect-atom/atom-react": "^0.4.6",
63
+ "@effect-atom/atom-react": "^0.5.0",
64
64
  "@fluentui/react-tabster": "9.26.11",
65
65
  "@radix-ui/react-context": "1.1.1",
66
66
  "@radix-ui/react-primitive": "2.0.2",
@@ -68,58 +68,58 @@
68
68
  "@radix-ui/react-use-controllable-state": "1.1.0",
69
69
  "date-fns": "^3.3.1",
70
70
  "lodash.orderby": "^4.6.0",
71
- "@dxos/context": "0.8.4-main.21d9917",
72
- "@dxos/debug": "0.8.4-main.21d9917",
73
- "@dxos/async": "0.8.4-main.21d9917",
74
- "@dxos/echo-atom": "0.8.4-main.21d9917",
75
- "@dxos/echo": "0.8.4-main.21d9917",
76
- "@dxos/effect": "0.8.4-main.21d9917",
77
- "@dxos/invariant": "0.8.4-main.21d9917",
78
- "@dxos/keys": "0.8.4-main.21d9917",
79
- "@dxos/lit-grid": "0.8.4-main.21d9917",
80
- "@dxos/random": "0.8.4-main.21d9917",
81
- "@dxos/log": "0.8.4-main.21d9917",
82
- "@dxos/react-client": "0.8.4-main.21d9917",
83
- "@dxos/react-ui-attention": "0.8.4-main.21d9917",
84
- "@dxos/react-ui-components": "0.8.4-main.21d9917",
85
- "@dxos/react-ui-form": "0.8.4-main.21d9917",
86
- "@dxos/react-ui-grid": "0.8.4-main.21d9917",
87
- "@dxos/react-ui-menu": "0.8.4-main.21d9917",
88
- "@dxos/react-ui-stack": "0.8.4-main.21d9917",
89
- "@dxos/schema": "0.8.4-main.21d9917",
90
- "@dxos/react-ui-syntax-highlighter": "0.8.4-main.21d9917",
91
- "@dxos/types": "0.8.4-main.21d9917",
92
- "@dxos/react-ui-searchlist": "0.8.4-main.21d9917",
93
- "@dxos/util": "0.8.4-main.21d9917",
94
- "@dxos/ui-types": "0.8.4-main.21d9917",
95
- "@dxos/react-ui-editor": "0.8.4-main.21d9917"
71
+ "@dxos/async": "0.8.4-main.2244d791bb",
72
+ "@dxos/debug": "0.8.4-main.2244d791bb",
73
+ "@dxos/context": "0.8.4-main.2244d791bb",
74
+ "@dxos/echo": "0.8.4-main.2244d791bb",
75
+ "@dxos/echo-atom": "0.8.4-main.2244d791bb",
76
+ "@dxos/effect": "0.8.4-main.2244d791bb",
77
+ "@dxos/invariant": "0.8.4-main.2244d791bb",
78
+ "@dxos/keys": "0.8.4-main.2244d791bb",
79
+ "@dxos/lit-grid": "0.8.4-main.2244d791bb",
80
+ "@dxos/log": "0.8.4-main.2244d791bb",
81
+ "@dxos/random": "0.8.4-main.2244d791bb",
82
+ "@dxos/react-client": "0.8.4-main.2244d791bb",
83
+ "@dxos/react-ui-attention": "0.8.4-main.2244d791bb",
84
+ "@dxos/react-ui-editor": "0.8.4-main.2244d791bb",
85
+ "@dxos/react-ui-form": "0.8.4-main.2244d791bb",
86
+ "@dxos/react-ui-grid": "0.8.4-main.2244d791bb",
87
+ "@dxos/react-ui-searchlist": "0.8.4-main.2244d791bb",
88
+ "@dxos/react-ui-menu": "0.8.4-main.2244d791bb",
89
+ "@dxos/react-ui-components": "0.8.4-main.2244d791bb",
90
+ "@dxos/react-ui-stack": "0.8.4-main.2244d791bb",
91
+ "@dxos/react-ui-syntax-highlighter": "0.8.4-main.2244d791bb",
92
+ "@dxos/types": "0.8.4-main.2244d791bb",
93
+ "@dxos/schema": "0.8.4-main.2244d791bb",
94
+ "@dxos/ui-types": "0.8.4-main.2244d791bb",
95
+ "@dxos/util": "0.8.4-main.2244d791bb"
96
96
  },
97
97
  "devDependencies": {
98
- "@effect/platform": "0.93.6",
98
+ "@effect/platform": "0.94.4",
99
99
  "@types/lodash.orderby": "^4.6.9",
100
100
  "@types/react": "~19.2.7",
101
101
  "@types/react-dom": "~19.2.3",
102
- "effect": "3.19.11",
102
+ "effect": "3.19.16",
103
103
  "fast-check": "^3.19.0",
104
104
  "react": "~19.2.3",
105
105
  "react-dom": "~19.2.3",
106
106
  "vite": "7.1.9",
107
- "@dxos/lit-ui": "0.8.4-main.21d9917",
108
- "@dxos/random": "0.8.4-main.21d9917",
109
- "@dxos/react-client": "0.8.4-main.21d9917",
110
- "@dxos/react-ui": "0.8.4-main.21d9917",
111
- "@dxos/storybook-utils": "0.8.4-main.21d9917",
112
- "@dxos/ui-theme": "0.8.4-main.21d9917",
113
- "@dxos/test-utils": "0.8.4-main.21d9917"
107
+ "@dxos/lit-ui": "0.8.4-main.2244d791bb",
108
+ "@dxos/random": "0.8.4-main.2244d791bb",
109
+ "@dxos/react-client": "0.8.4-main.2244d791bb",
110
+ "@dxos/react-ui": "0.8.4-main.2244d791bb",
111
+ "@dxos/storybook-utils": "0.8.4-main.2244d791bb",
112
+ "@dxos/test-utils": "0.8.4-main.2244d791bb",
113
+ "@dxos/ui-theme": "0.8.4-main.2244d791bb"
114
114
  },
115
115
  "peerDependencies": {
116
- "@effect-atom/atom-react": "^0.4.6",
117
- "@effect/platform": "0.93.6",
118
- "effect": "3.19.11",
116
+ "@effect-atom/atom-react": "^0.5.0",
117
+ "@effect/platform": "0.94.4",
118
+ "effect": "3.19.16",
119
119
  "react": "~19.2.3",
120
120
  "react-dom": "~19.2.3",
121
- "@dxos/ui-theme": "0.8.4-main.21d9917",
122
- "@dxos/react-ui": "0.8.4-main.21d9917"
121
+ "@dxos/react-ui": "0.8.4-main.2244d791bb",
122
+ "@dxos/ui-theme": "0.8.4-main.2244d791bb"
123
123
  },
124
124
  "publishConfig": {
125
125
  "access": "public"
@@ -3,10 +3,11 @@
3
3
  //
4
4
 
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import type * as Types from 'effect/Types';
6
7
  import React, { useMemo, useState } from 'react';
7
8
 
8
- import { Obj } from '@dxos/echo';
9
- import { Format, type JsonSchemaType } from '@dxos/echo/internal';
9
+ import { type JsonSchema, Obj } from '@dxos/echo';
10
+ import { Format } from '@dxos/echo/internal';
10
11
  import { faker } from '@dxos/random';
11
12
  import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
12
13
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
@@ -58,7 +59,7 @@ const DynamicTableStory = () => {
58
59
  const meta = {
59
60
  title: 'ui/react-ui-table/DynamicTable',
60
61
  component: DynamicTable,
61
- decorators: [withTheme, withRegistry],
62
+ decorators: [withTheme(), withRegistry],
62
63
  parameters: {
63
64
  layout: 'fullscreen',
64
65
  translations,
@@ -101,7 +102,7 @@ export const WithClickToSelect: StoryObj = {
101
102
 
102
103
  export const WithJsonSchema: StoryObj = {
103
104
  render: () => {
104
- const schema = useMemo<JsonSchemaType>(
105
+ const schema = useMemo<Types.DeepMutable<JsonSchema.JsonSchema>>(
105
106
  () => ({
106
107
  type: 'object',
107
108
  properties: {
@@ -3,10 +3,11 @@
3
3
  //
4
4
 
5
5
  import { RegistryContext } from '@effect-atom/atom-react';
6
+ import type * as Types from 'effect/Types';
6
7
  import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
7
8
 
8
9
  import { type Type } from '@dxos/echo';
9
- import { type JsonSchemaType } from '@dxos/echo/internal';
10
+ import { type JsonSchema } from '@dxos/echo';
10
11
  import { type ThemedClassName, useDefaultValue } from '@dxos/react-ui';
11
12
  import { type ProjectionModel } from '@dxos/schema';
12
13
  import { mx } from '@dxos/ui-theme';
@@ -23,7 +24,7 @@ export type DynamicTableProps<T extends Type.Entity.Any = Type.Entity.Any> = The
23
24
  name?: string; // TODO(burdon): Remove?
24
25
  rows: any[];
25
26
  properties?: TablePropertyDefinition[];
26
- jsonSchema?: JsonSchemaType;
27
+ jsonSchema?: Types.DeepMutable<JsonSchema.JsonSchema>;
27
28
  features?: Partial<TableFeatures>;
28
29
  rowActions?: TableRowAction[];
29
30
  onRowClick?: (row: any) => void;
@@ -125,7 +125,7 @@ const meta = {
125
125
  title: 'ui/react-ui-table/Relations',
126
126
  render: DefaultStory,
127
127
  decorators: [
128
- withTheme,
128
+ withTheme(),
129
129
  withRegistry,
130
130
  // TODO(thure): Shouldn't `layout: 'fullscreen'` below make this unnecessary?
131
131
  withLayout({ classNames: 'fixed inset-0' }),
@@ -12,6 +12,7 @@ import { invariant } from '@dxos/invariant';
12
12
  import { faker } from '@dxos/random';
13
13
  import { PublicKey } from '@dxos/react-client';
14
14
  import { withClientProvider } from '@dxos/react-client/testing';
15
+ import { ScrollArea } from '@dxos/react-ui';
15
16
  import { withTheme } from '@dxos/react-ui/testing';
16
17
  import { ViewEditor, translations as formTranslations } from '@dxos/react-ui-form';
17
18
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
@@ -103,6 +104,15 @@ const DefaultStory = () => {
103
104
  const { db, schema, table, tableRef, model, presentation, handleInsertRow, handleSaveView, handleDeleteColumn } =
104
105
  useTestTableModel();
105
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
+
106
116
  if (!schema || !table?.view.target) {
107
117
  return <div />;
108
118
  }
@@ -117,16 +127,19 @@ const DefaultStory = () => {
117
127
  schema={schema}
118
128
  model={model}
119
129
  presentation={presentation}
130
+ onRowClick={handleRowClick}
120
131
  ignoreAttention
121
132
  />
122
133
  </TableComponent.Root>
123
134
  </div>
124
- <div className='flex flex-col bs-full border-l border-separator overflow-y-auto'>
125
- <StoryViewEditor view={table.view.target} schema={schema} db={db} handleDeleteColumn={handleDeleteColumn} />
126
- <SyntaxHighlighter language='json' className='text-xs'>
127
- {JSON.stringify({ view: table.view.target, schema }, null, 2)}
128
- </SyntaxHighlighter>
129
- </div>
135
+ <ScrollArea.Root orientation='vertical' classNames='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>
130
143
  </div>
131
144
  );
132
145
  };
@@ -143,7 +156,7 @@ const meta = {
143
156
  title: 'ui/react-ui-table/Table',
144
157
  render: DefaultStory,
145
158
  decorators: [
146
- withTheme,
159
+ withTheme(),
147
160
  withRegistry,
148
161
  withClientProvider({
149
162
  types: [View.View, Table.Table],
@@ -45,7 +45,7 @@ const meta = {
45
45
  component: DefaultStory,
46
46
  render: DefaultStory,
47
47
  decorators: [
48
- withTheme,
48
+ withTheme(),
49
49
  withLayout({ layout: 'column' }),
50
50
  withClientProvider({
51
51
  types: [View.View, Task.Task, Table.Table],
@@ -7,7 +7,7 @@ import { useState } from 'react';
7
7
 
8
8
  import { Type } from '@dxos/echo';
9
9
  import { useAsyncEffect } from '@dxos/react-ui';
10
- import { ProjectionModel, createDirectChangeCallback, createEchoChangeCallback } from '@dxos/schema';
10
+ import { ProjectionModel, createEchoChangeCallback } from '@dxos/schema';
11
11
 
12
12
  import { type Table } from '../types';
13
13
 
@@ -25,10 +25,9 @@ export const useProjectionModel = <S extends Type.Entity.Any>(
25
25
  // For immutable schemas, create a snapshot.
26
26
  const jsonSchema = Type.isMutable(schema) ? schema.jsonSchema : Type.toJsonSchema(schema);
27
27
 
28
- // Use createEchoChangeCallback for mutable schemas (EchoSchema), otherwise use direct mutation.
29
- const change = Type.isMutable(schema)
30
- ? createEchoChangeCallback(view, schema)
31
- : createDirectChangeCallback(view.projection, jsonSchema);
28
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
29
+ // Pass schema only when mutable to allow schema mutations.
30
+ const change = createEchoChangeCallback(view, Type.isMutable(schema) ? schema : undefined);
32
31
 
33
32
  const projection = new ProjectionModel({ registry, view, baseSchema: jsonSchema, change });
34
33
  projection.normalizeView();
@@ -29,7 +29,7 @@ export type UseTableModelProps<T extends TableRow = TableRow> = {
29
29
  onRowAction?: (actionId: string, data: T) => void;
30
30
  } & Pick<
31
31
  TableModelProps<T>,
32
- 'features' | 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChange'
32
+ 'features' | 'onInsertRow' | 'onDeleteRows' | 'onColumnDelete' | 'onCellUpdate' | 'onRowOrderChange'
33
33
  >;
34
34
 
35
35
  export const useTableModel = <T extends TableRow = TableRow>({
@@ -137,7 +137,7 @@ const createTableModel = (registry: Registry.Registry, props: Partial<TableModel
137
137
  registry,
138
138
  view,
139
139
  baseSchema: schema.jsonSchema,
140
- change: createDirectChangeCallback(view.projection, schema.jsonSchema),
140
+ change: createDirectChangeCallback(view.projection, Type.toJsonSchema(schema)),
141
141
  });
142
142
  projection.normalizeView();
143
143
  return new TableModel({
@@ -122,7 +122,7 @@ export type TableModelProps<T extends TableRow = TableRow> = {
122
122
  onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
123
123
  onInsertRow?: (data?: any) => InsertRowResult;
124
124
  onDeleteRows?: (index: number, obj: T[]) => void;
125
- onDeleteColumn?: (fieldId: string) => void;
125
+ onColumnDelete?: (fieldId: string) => void;
126
126
  onCellUpdate?: (cell: DxGridPosition) => void;
127
127
  onRowOrderChange?: () => void;
128
128
  onRowAction?: (actionId: string, data: T) => void;
@@ -139,7 +139,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
139
139
 
140
140
  private readonly _onInsertRow?: (data?: any) => InsertRowResult;
141
141
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
142
- private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
142
+ private readonly _onColumnDelete?: TableModelProps<T>['onColumnDelete'];
143
143
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
144
144
  private readonly _onRowOrderChange?: TableModelProps<T>['onRowOrderChange'];
145
145
  private readonly _onRowAction?: TableModelProps<T>['onRowAction'];
@@ -176,7 +176,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
176
176
  pinnedRows = { top: [], bottom: [] },
177
177
  rowActions = [],
178
178
  onCellUpdate,
179
- onDeleteColumn,
179
+ onColumnDelete,
180
180
  onDeleteRows,
181
181
  onInsertRow,
182
182
  onRowOrderChange,
@@ -292,7 +292,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
292
292
  this._rowActions = rowActions;
293
293
  this._onInsertRow = onInsertRow;
294
294
  this._onDeleteRows = onDeleteRows;
295
- this._onDeleteColumn = onDeleteColumn;
295
+ this._onColumnDelete = onColumnDelete;
296
296
  this._onCellUpdate = onCellUpdate;
297
297
  this._onRowOrderChange = onRowOrderChange;
298
298
  this._onRowAction = onRowAction;
@@ -691,7 +691,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
691
691
  const currentRow = this._registry.get(this._rows)[row];
692
692
  invariant(currentRow, 'Invalid row index');
693
693
 
694
- const snapshot = getSnapshot(currentRow);
694
+ const snapshot = { ...getSnapshot(currentRow) };
695
695
  setValue(snapshot, field.path, transformedValue);
696
696
 
697
697
  const schema = Obj.getSchema(currentRow);
@@ -796,8 +796,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
796
796
  }
797
797
 
798
798
  const field = this._projection?.getFields().find((field) => field.id === fieldId);
799
- if (field && this._onDeleteColumn) {
800
- this._onDeleteColumn(field.id);
799
+ if (field && this._onColumnDelete) {
800
+ this._onColumnDelete(field.id);
801
801
  }
802
802
  }
803
803
 
@@ -108,7 +108,7 @@ const createTableModel = (registry: Registry.Registry, props: Partial<TableModel
108
108
  registry,
109
109
  view,
110
110
  baseSchema: schema.jsonSchema,
111
- change: createDirectChangeCallback(view.projection, schema.jsonSchema),
111
+ change: createDirectChangeCallback(view.projection, Type.toJsonSchema(schema)),
112
112
  });
113
113
  projection.normalizeView();
114
114
  return new TableModel({ registry, object, projection, ...props });
@@ -210,7 +210,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
210
210
  const targetObj = getValue(obj, field.path)?.target;
211
211
  if (targetObj) {
212
212
  const dxn = Obj.getDXN(targetObj)?.toString();
213
- 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>`;
213
+ cell.accessoryHtml = `<div role="none" class="absolute inline-end-0 inset-block-0 p-[--dx-grid-cell-content-padding-block]"><dx-anchor dxn=${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
214
  }
215
215
  }
216
216
 
@@ -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,10 +54,16 @@ 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();
63
+ if (format) {
64
+ await this.page.getByRole('combobox').click();
65
+ await this.page.getByLabel(format).click();
66
+ }
60
67
  await this.page.getByPlaceholder('Property label').click();
61
68
  await this.page.getByPlaceholder('Property label').fill(label);
62
69
  await this.page.getByRole('button', { name: 'Save' }).click();
@@ -121,8 +121,22 @@ test.describe('Table', () => {
121
121
  await page.close();
122
122
  });
123
123
 
124
- // Rest of add column test remains the same as it's a more complex flow.
125
- 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 }) => {
126
140
  test.skip(browserName === 'webkit');
127
141
  test.skip(browserName === 'firefox');
128
142
  const { page } = await setupPage(browser, { url: storyUrl });
@@ -160,6 +174,35 @@ test.describe('Table', () => {
160
174
  await page.close();
161
175
  });
162
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
+
163
206
  // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
164
207
  test.skip('extant relations work as expected', async ({ browser, browserName }) => {
165
208
  test.skip(browserName === 'webkit');
@@ -97,7 +97,7 @@ export const useTestTableModel = <T extends Type.Obj.Any = Type.Obj.Any>(): Test
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
  });
@@ -0,0 +1,70 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Registry } from '@effect-atom/atom-react';
6
+ import { describe, expect, test } from 'vitest';
7
+
8
+ import { Format } from '@dxos/echo';
9
+
10
+ import { type TablePropertyDefinition, getBaseSchema, makeDynamicTable } from './dynamic-table';
11
+
12
+ describe('makeDynamicTable', () => {
13
+ /**
14
+ * Base case: plain jsonSchema (not from Echo / Type.toJsonSchema). Does not exercise the path
15
+ * where projection or schema are reactive, so this does not reproduce the Obj.change regression.
16
+ * Confirms makeDynamicTable + setProperties (title) works with plain objects.
17
+ */
18
+ test('makeDynamicTable with plain jsonSchema and properties with title does not throw', () => {
19
+ const registry = Registry.make();
20
+ const jsonSchema = {
21
+ type: 'object' as const,
22
+ properties: {
23
+ id: { type: 'string' as const },
24
+ name: { type: 'string' as const },
25
+ },
26
+ required: ['id'],
27
+ };
28
+ const properties: TablePropertyDefinition[] = [
29
+ { name: 'id', format: Format.TypeFormat.String },
30
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
31
+ ];
32
+
33
+ expect(() => {
34
+ const { projection, object } = makeDynamicTable({
35
+ registry,
36
+ jsonSchema,
37
+ properties,
38
+ });
39
+ expect(projection).toBeDefined();
40
+ expect(object).toBeDefined();
41
+ expect(projection.getFields().length).toBeGreaterThan(0);
42
+ }).not.toThrow();
43
+ });
44
+
45
+ /**
46
+ * Regression test for the path where jsonSchema comes from getBaseSchema(typename, properties).
47
+ * That path uses Echo (View.make, Type.toJsonSchema); mutations must run inside Obj.change and on a
48
+ * cloned schema. This test ensures that flow does not throw.
49
+ */
50
+ test('makeDynamicTable with jsonSchema from getBaseSchema(typename, properties) and properties with title does not throw', () => {
51
+ const registry = Registry.make();
52
+ const typename = 'example.com/type/DynamicTableTest';
53
+ const properties: TablePropertyDefinition[] = [
54
+ { name: 'id', format: Format.TypeFormat.String },
55
+ { name: 'name', format: Format.TypeFormat.String, title: 'Name' },
56
+ ];
57
+ const { jsonSchema } = getBaseSchema({ typename, properties });
58
+
59
+ expect(() => {
60
+ const { projection, object } = makeDynamicTable({
61
+ registry,
62
+ jsonSchema,
63
+ properties,
64
+ });
65
+ expect(projection).toBeDefined();
66
+ expect(object).toBeDefined();
67
+ expect(projection.getFields().length).toBeGreaterThan(0);
68
+ }).not.toThrow();
69
+ });
70
+ });
@@ -3,13 +3,14 @@
3
3
  //
4
4
 
5
5
  import { type Registry } from '@effect-atom/atom-react';
6
+ import type * as Types from 'effect/Types';
6
7
 
7
8
  import { Filter, type JsonSchema, Obj, Order, Query, type QueryAST, Ref, Type } from '@dxos/echo';
8
9
  import {
9
10
  ProjectionModel,
10
11
  type SchemaPropertyDefinition,
11
12
  View,
12
- createDirectChangeCallback,
13
+ createEchoChangeCallback,
13
14
  getSchemaFromPropertyDefinitions,
14
15
  } from '@dxos/schema';
15
16
 
@@ -37,11 +38,11 @@ export const getBaseSchema = ({
37
38
  schema?: Type.Entity.Any;
38
39
  typename?: string;
39
40
  properties?: TablePropertyDefinition[];
40
- jsonSchema?: JsonSchema.JsonSchema;
41
- }): { typename: string; jsonSchema: JsonSchema.JsonSchema } => {
41
+ jsonSchema?: Types.DeepMutable<JsonSchema.JsonSchema>;
42
+ }): { typename: string; jsonSchema: Types.DeepMutable<JsonSchema.JsonSchema> } => {
42
43
  if (typename && properties) {
43
44
  const schema = getSchemaFromPropertyDefinitions(typename, properties);
44
- return { typename: schema.typename, jsonSchema: schema.jsonSchema };
45
+ return { typename: schema.typename, jsonSchema: Type.toJsonSchema(schema) };
45
46
  } else if (schema) {
46
47
  return { typename: Type.getTypename(schema)!, jsonSchema: Type.toJsonSchema(schema) };
47
48
  } else if (typename && jsonSchema) {
@@ -57,7 +58,7 @@ export const makeDynamicTable = ({
57
58
  properties,
58
59
  }: {
59
60
  registry: Registry.Registry;
60
- jsonSchema: JsonSchema.JsonSchema;
61
+ jsonSchema: Types.DeepMutable<JsonSchema.JsonSchema>;
61
62
  properties?: TablePropertyDefinition[];
62
63
  }): { projection: ProjectionModel; object: Table.Table } => {
63
64
  const view = View.make({
@@ -67,12 +68,11 @@ export const makeDynamicTable = ({
67
68
  });
68
69
  const object = Obj.make(Table.Table, { view: Ref.make(view), sizes: {} });
69
70
 
70
- // Use direct change callback for in-memory objects (non-ECHO backed).
71
71
  const projection = new ProjectionModel({
72
72
  registry,
73
73
  view,
74
74
  baseSchema: jsonSchema,
75
- change: createDirectChangeCallback(view.projection, jsonSchema),
75
+ change: createEchoChangeCallback(view, jsonSchema),
76
76
  });
77
77
  projection.normalizeView();
78
78
  if (properties && projection.getFields()) {