@dxos/react-ui-table 0.8.4-main.5ad4a44 → 0.8.4-main.66e292d

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 (92) hide show
  1. package/dist/lib/browser/chunk-5XWSB3JP.mjs +71 -0
  2. package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-OZTXLZGM.mjs → chunk-KNC7XYT6.mjs} +521 -542
  4. package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +75 -59
  6. package/dist/lib/browser/index.mjs.map +3 -3
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +14 -27
  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-S56Z4V4X.mjs → chunk-CWSGS3Y2.mjs} +521 -542
  12. package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-MKTMOY46.mjs +72 -0
  14. package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +75 -59
  16. package/dist/lib/node-esm/index.mjs.map +3 -3
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +14 -27
  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/DynamicTable.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +2 -2
  23. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/Relations.stories.d.ts +7 -7
  25. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  26. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/Table.stories.d.ts +67 -4
  28. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +2 -2
  31. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
  33. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  34. package/dist/types/src/components/testing.d.ts +0 -2
  35. package/dist/types/src/components/testing.d.ts.map +1 -1
  36. package/dist/types/src/hooks/index.d.ts +2 -1
  37. package/dist/types/src/hooks/index.d.ts.map +1 -1
  38. package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
  39. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  40. package/dist/types/src/hooks/useTableModel.d.ts +4 -5
  41. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  42. package/dist/types/src/model/table-model.d.ts +7 -9
  43. package/dist/types/src/model/table-model.d.ts.map +1 -1
  44. package/dist/types/src/model/table-sorting.d.ts +8 -4
  45. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  46. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  47. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  48. package/dist/types/src/testing/useTestTableModel.d.ts +21 -15
  49. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  50. package/dist/types/src/translations.d.ts +2 -2
  51. package/dist/types/src/types/Table.d.ts +28 -0
  52. package/dist/types/src/types/Table.d.ts.map +1 -0
  53. package/dist/types/src/types/index.d.ts +1 -1
  54. package/dist/types/src/types/index.d.ts.map +1 -1
  55. package/dist/types/src/util/dynamic-table.d.ts +7 -6
  56. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  57. package/dist/types/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +41 -40
  59. package/src/components/Table/DynamicTable.stories.tsx +7 -7
  60. package/src/components/Table/DynamicTable.tsx +12 -4
  61. package/src/components/Table/Relations.stories.tsx +24 -35
  62. package/src/components/Table/RowActionsMenu.tsx +21 -13
  63. package/src/components/Table/Table.stories.tsx +58 -53
  64. package/src/components/TableCellEditor/FormCellEditor.tsx +11 -11
  65. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +11 -9
  66. package/src/components/TableCellEditor/TableCellEditor.tsx +4 -4
  67. package/src/components/TableToolbar/TableToolbar.tsx +32 -22
  68. package/src/components/testing.ts +3 -3
  69. package/src/hooks/index.ts +2 -1
  70. package/src/hooks/useProjectionModel.ts +19 -0
  71. package/src/hooks/useTableModel.ts +11 -13
  72. package/src/model/table-model.test.ts +5 -5
  73. package/src/model/table-model.ts +34 -40
  74. package/src/model/table-presentation.test.ts +5 -5
  75. package/src/model/table-presentation.ts +9 -9
  76. package/src/model/table-sorting.ts +28 -19
  77. package/src/playwright/TableManager.ts +3 -3
  78. package/src/playwright/smoke.spec.ts +8 -6
  79. package/src/testing/useTestTableModel.ts +32 -24
  80. package/src/translations.ts +2 -2
  81. package/src/types/Table.ts +89 -0
  82. package/src/types/index.ts +1 -1
  83. package/src/util/dynamic-table.ts +12 -16
  84. package/dist/lib/browser/chunk-4K7JPALT.mjs +0 -66
  85. package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
  86. package/dist/lib/browser/chunk-OZTXLZGM.mjs.map +0 -7
  87. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
  88. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
  89. package/dist/lib/node-esm/chunk-S56Z4V4X.mjs.map +0 -7
  90. package/dist/types/src/types/schema.d.ts +0 -28
  91. package/dist/types/src/types/schema.d.ts.map +0 -1
  92. package/src/types/schema.ts +0 -73
@@ -5,20 +5,11 @@
5
5
  import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
- import { Obj, Ref } from '@dxos/echo';
9
- import {
10
- FormatEnum,
11
- type JsonProp,
12
- type JsonSchemaType,
13
- getSchema,
14
- getValue,
15
- setValue,
16
- toEffectSchema,
17
- } from '@dxos/echo/internal';
8
+ import { Format, Obj, Ref } from '@dxos/echo';
9
+ import { type JsonProp, type JsonSchemaType, getValue, setValue, toEffectSchema } from '@dxos/echo/internal';
18
10
  import { invariant } from '@dxos/invariant';
19
11
  import { ObjectId } from '@dxos/keys';
20
12
  import { getSnapshot, isLiveObject } from '@dxos/live-object';
21
- import { fullyQualifiedId } from '@dxos/react-client/echo';
22
13
  import { type Label } from '@dxos/react-ui';
23
14
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
24
15
  import {
@@ -28,15 +19,15 @@ import {
28
19
  type DxGridPosition,
29
20
  } from '@dxos/react-ui-grid';
30
21
  import {
31
- type DataType,
32
22
  type FieldSortType,
33
- ProjectionModel,
23
+ type ProjectionModel,
34
24
  type PropertyType,
35
25
  type ValidationError,
26
+ type View,
36
27
  validateSchema,
37
28
  } from '@dxos/schema';
38
29
 
39
- import { Table } from '../types';
30
+ import { type Table } from '../types';
40
31
  import { touch } from '../util';
41
32
  import { extractTagIds } from '../util/tag';
42
33
 
@@ -78,9 +69,8 @@ const defaultFeatures: TableFeatures = {
78
69
  export type InsertRowResult = 'draft' | 'final';
79
70
 
80
71
  export type TableModelProps<T extends TableRow = TableRow> = {
81
- view: DataType.View;
82
- schema: JsonSchemaType;
83
- projection?: ProjectionModel;
72
+ object: Table.Table;
73
+ projection: ProjectionModel;
84
74
  features?: Partial<TableFeatures>;
85
75
  sorting?: FieldSortType[];
86
76
  initialSelection?: string[];
@@ -96,9 +86,8 @@ export type TableModelProps<T extends TableRow = TableRow> = {
96
86
  };
97
87
 
98
88
  export class TableModel<T extends TableRow = TableRow> extends Resource {
99
- private readonly _view: DataType.View;
89
+ private readonly _object: Table.Table;
100
90
  private readonly _projection: ProjectionModel;
101
- private _table?: Table.Table;
102
91
 
103
92
  private readonly _visibleRange = signal<DxGridPlaneRange>({
104
93
  start: { row: 0, col: 0 },
@@ -123,8 +112,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
123
112
  private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
124
113
 
125
114
  constructor({
126
- view,
127
- schema,
115
+ object,
128
116
  projection,
129
117
  features = {},
130
118
  sorting = [],
@@ -139,8 +127,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
139
127
  onRowAction,
140
128
  }: TableModelProps<T>) {
141
129
  super();
142
- this._view = view;
143
- this._projection = projection ?? new ProjectionModel(schema, view.projection);
130
+ this._object = object;
131
+ this._projection = projection;
144
132
  this._projection.normalizeView();
145
133
 
146
134
  // TODO(ZaymonFC): Use our more robust config merging module?
@@ -151,7 +139,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
151
139
  'Single selection is not compatible with editable tables.',
152
140
  );
153
141
 
154
- this._sorting = new TableSorting(this._rows, this._view, this._projection);
142
+ this._sorting = new TableSorting({
143
+ rows: this._rows,
144
+ getView: () => this.view,
145
+ projection: this._projection,
146
+ });
155
147
 
156
148
  if (sorting.length > 0) {
157
149
  const [sort] = sorting;
@@ -176,16 +168,17 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
176
168
  }
177
169
 
178
170
  public get id(): string {
179
- return fullyQualifiedId(this._view);
171
+ return Obj.getDXN(this._object).toString();
180
172
  }
181
173
 
182
- public get view(): DataType.View {
183
- return this._view;
174
+ public get view(): View.View {
175
+ const view = this._object.view.target;
176
+ invariant(view, 'Table model not initialized');
177
+ return view;
184
178
  }
185
179
 
186
180
  public get table(): Table.Table {
187
- invariant(this._table, 'Model not initialized');
188
- return this._table;
181
+ return this._object;
189
182
  }
190
183
 
191
184
  public get projection(): ProjectionModel {
@@ -245,10 +238,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
245
238
  //
246
239
 
247
240
  protected override async _open(): Promise<void> {
248
- const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
249
- invariant(Obj.instanceOf(Table.Table, presentation));
250
- this._table = presentation;
251
-
241
+ await this._object.view.load();
252
242
  this.initializeColumnMeta();
253
243
  this.initializeEffects();
254
244
  await this._selection.open(this._ctx);
@@ -471,7 +461,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
471
461
 
472
462
  const { props } = this._projection.getFieldProjection(field.id);
473
463
  switch (props.format) {
474
- case FormatEnum.Ref: {
464
+ case Format.TypeFormat.Ref: {
475
465
  if (!field.referencePath) {
476
466
  return ''; // TODO(burdon): Show error.
477
467
  }
@@ -480,7 +470,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
480
470
  }
481
471
 
482
472
  default: {
483
- return formatForEditing({ type: props.type, format: props.format, value });
473
+ return formatForEditing({
474
+ type: props.type,
475
+ format: props.format,
476
+ value,
477
+ });
484
478
  }
485
479
  }
486
480
  };
@@ -524,7 +518,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
524
518
  const snapshot = getSnapshot(currentRow);
525
519
  setValue(snapshot, field.path, transformedValue);
526
520
 
527
- const schema = getSchema(currentRow);
521
+ const schema = Obj.getSchema(currentRow);
528
522
  invariant(schema);
529
523
 
530
524
  const validationResult = validateSchema(schema, snapshot);
@@ -550,7 +544,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
550
544
  const transformedValue = editorTextToCellValue(props, value);
551
545
 
552
546
  // Special handling for Ref format to preserve existing behavior
553
- if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
547
+ if (props.format === Format.TypeFormat.Ref && !isLiveObject(value)) {
554
548
  return;
555
549
  }
556
550
 
@@ -665,7 +659,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
665
659
 
666
660
  const editorTextToCellValue = (props: PropertyType, value: any): any => {
667
661
  switch (props.format) {
668
- case FormatEnum.Ref: {
662
+ case Format.TypeFormat.Ref: {
669
663
  if (isLiveObject(value)) {
670
664
  return Ref.make(value);
671
665
  } else {
@@ -673,7 +667,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
673
667
  }
674
668
  }
675
669
 
676
- case FormatEnum.SingleSelect: {
670
+ case Format.TypeFormat.SingleSelect: {
677
671
  const ids = extractTagIds(value);
678
672
  if (ids && ids.length > 0) {
679
673
  return ids[0];
@@ -682,7 +676,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
682
676
  }
683
677
  }
684
678
 
685
- case FormatEnum.MultiSelect: {
679
+ case Format.TypeFormat.MultiSelect: {
686
680
  const ids = extractTagIds(value);
687
681
  return ids || value;
688
682
  }
@@ -9,7 +9,7 @@ import { Filter, Query } from '@dxos/echo';
9
9
  import { TypedObject } from '@dxos/echo/internal';
10
10
  import { createEchoSchema } from '@dxos/echo/testing';
11
11
  import { live } from '@dxos/live-object';
12
- import { createView } from '@dxos/schema';
12
+ import { ProjectionModel, View } from '@dxos/schema';
13
13
 
14
14
  import { Table } from '../types';
15
15
 
@@ -95,11 +95,11 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
95
95
 
96
96
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
97
97
  const schema = createEchoSchema(Test);
98
- const table = Table.make();
99
- const view = createView({
98
+ const view = View.make({
100
99
  query: Query.select(Filter.type(schema)),
101
100
  jsonSchema: schema.jsonSchema,
102
- presentation: table,
103
101
  });
104
- return new TableModel({ view, schema: schema.jsonSchema, ...props });
102
+ const object = Table.make({ view });
103
+ const projection = new ProjectionModel(schema.jsonSchema, view.projection);
104
+ return new TableModel({ object, projection, ...props });
105
105
  };
@@ -6,7 +6,7 @@ import { signal } from '@preact/signals-core';
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,
@@ -120,12 +120,12 @@ export class TablePresentation<T extends TableRow = TableRow> {
120
120
  }
121
121
 
122
122
  switch (props.format) {
123
- case FormatEnum.Boolean:
124
- case FormatEnum.SingleSelect:
125
- case FormatEnum.MultiSelect: {
123
+ case Format.TypeFormat.Boolean:
124
+ case Format.TypeFormat.SingleSelect:
125
+ case Format.TypeFormat.MultiSelect: {
126
126
  return '';
127
127
  }
128
- case FormatEnum.Ref: {
128
+ case Format.TypeFormat.Ref: {
129
129
  if (!field.referencePath) {
130
130
  return ''; // TODO(burdon): Show error.
131
131
  }
@@ -201,7 +201,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
201
201
  }
202
202
 
203
203
  // References.
204
- if (props.format === FormatEnum.Ref && props.referenceSchema) {
204
+ if (props.format === Format.TypeFormat.Ref && props.referenceSchema) {
205
205
  const targetObj = getValue(obj, field.path)?.target;
206
206
  if (targetObj) {
207
207
  const dxn = Obj.getDXN(targetObj)?.toString();
@@ -210,7 +210,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
210
210
  }
211
211
 
212
212
  // Booleans.
213
- if (props.format === FormatEnum.Boolean) {
213
+ if (props.format === Format.TypeFormat.Boolean) {
214
214
  const value = getValue(obj, field.path);
215
215
  cell.accessoryHtml = tableControls.switch.render({
216
216
  colIndex,
@@ -221,7 +221,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
221
221
  }
222
222
 
223
223
  // Single-Selects.
224
- if (props.format === FormatEnum.SingleSelect) {
224
+ if (props.format === Format.TypeFormat.SingleSelect) {
225
225
  const value = getValue(obj, field.path);
226
226
  const options = this.model.projection.getFieldProjection(field.id).props.options;
227
227
  if (options) {
@@ -233,7 +233,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
233
233
  }
234
234
 
235
235
  // Multi-Selects.
236
- if (props.format === FormatEnum.MultiSelect) {
236
+ if (props.format === Format.TypeFormat.MultiSelect) {
237
237
  const values = getValue(obj, field.path) as string[] | undefined;
238
238
  const options = this.model.projection.getFieldProjection(field.id).props.options;
239
239
  if (options && values && values.length > 0) {
@@ -5,15 +5,15 @@
5
5
  import { type ReadonlySignal, type Signal, computed, signal } from '@preact/signals-core';
6
6
  import orderBy from 'lodash.orderby';
7
7
 
8
- import { FormatEnum, TypeEnum, getValue } from '@dxos/echo/internal';
8
+ import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
9
9
  import { formatForDisplay } from '@dxos/react-ui-form';
10
- import type {
11
- DataType,
12
- FieldSortType,
13
- FieldType,
14
- ProjectionModel,
15
- PropertyType,
16
- SortDirectionType,
10
+ import {
11
+ type FieldSortType,
12
+ type FieldType,
13
+ type ProjectionModel,
14
+ type PropertyType,
15
+ type SortDirectionType,
16
+ type View,
17
17
  } from '@dxos/schema';
18
18
 
19
19
  import { type TableRow } from './table-model';
@@ -52,19 +52,27 @@ type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } |
52
52
  export class TableSorting<T extends TableRow> {
53
53
  private readonly _displayToDataIndex = new Map<number, number>();
54
54
  private readonly _rows: Signal<T[]>;
55
+ private readonly _getView: () => View.View;
56
+ private readonly _projection: ProjectionModel;
55
57
  private readonly _localSort = signal<LocalSort>(undefined);
56
58
  private readonly _isDirty: ReadonlySignal<boolean>;
57
59
  public readonly sortedRows: ReadonlySignal<T[]>;
58
60
 
59
- constructor(
60
- rows: Signal<T[]>,
61
- private readonly _view: DataType.View,
62
- private readonly _projection: ProjectionModel,
63
- ) {
61
+ constructor({
62
+ rows,
63
+ projection,
64
+ getView,
65
+ }: {
66
+ rows: Signal<T[]>;
67
+ getView: () => View.View;
68
+ projection: ProjectionModel;
69
+ }) {
64
70
  this._rows = rows;
71
+ this._getView = getView;
72
+ this._projection = projection;
65
73
  this._isDirty = computed(() => {
66
74
  const local = this._localSort.value;
67
- const viewSort = this._view.sort?.[0];
75
+ const viewSort = this._getView().sort?.[0];
68
76
  if (local?.type === 'cleared') {
69
77
  return viewSort !== undefined;
70
78
  }
@@ -92,7 +100,7 @@ export class TableSorting<T extends TableRow> {
92
100
  return local.sort;
93
101
  }
94
102
 
95
- return this._view.sort?.[0];
103
+ return this._getView().sort?.[0];
96
104
  }
97
105
 
98
106
  /**
@@ -137,10 +145,11 @@ export class TableSorting<T extends TableRow> {
137
145
 
138
146
  public save(): void {
139
147
  if (this._localSort.value !== undefined) {
148
+ const view = this._getView();
140
149
  if (this._localSort.value.type === 'active') {
141
- this._view.sort = [this._localSort.value.sort];
150
+ view.sort = [this._localSort.value.sort];
142
151
  } else {
143
- this._view.sort = [];
152
+ view.sort = [];
144
153
  }
145
154
  this._localSort.value = undefined;
146
155
  }
@@ -202,7 +211,7 @@ export class TableSorting<T extends TableRow> {
202
211
  * For single select fields, the sort value is determined by the index of the option in the
203
212
  * options array. This ensures sort order matches the configured option order.
204
213
  */
205
- if (format === FormatEnum.SingleSelect) {
214
+ if (format === Format.TypeFormat.SingleSelect) {
206
215
  const options = props.options;
207
216
  if (!options) {
208
217
  return undefined;
@@ -215,7 +224,7 @@ export class TableSorting<T extends TableRow> {
215
224
  // TODO(ZaymonFC): Should we format this returned value for display based on the referenced prop format?
216
225
  // Right now we're going to be sorting based on the raw values.
217
226
  // Maybe we need to recurse this function with the referenced prop format and field?
218
- if (format === FormatEnum.Ref && field.referencePath) {
227
+ if (format === Format.TypeFormat.Ref && field.referencePath) {
219
228
  const refValue = getValue(value.target, field.referencePath);
220
229
  return refValue;
221
230
  }
@@ -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();
59
+ await this.page.getByLabel(format).click();
60
60
  await this.page.getByPlaceholder('Property label.').click();
61
61
  await this.page.getByPlaceholder('Property label.').fill(label);
62
- await this.page.getByRole('button', { name: 'button save' }).click();
62
+ await this.page.getByRole('button', { name: 'Save' }).click();
63
63
  }
64
64
  }
@@ -130,7 +130,7 @@ test.describe('Table', () => {
130
130
  await table.grid.ready();
131
131
  const newColumnLabel = 'TEST LABEL';
132
132
 
133
- await table.addColumn({ label: newColumnLabel, format: 'number' });
133
+ await table.addColumn({ label: newColumnLabel, format: 'Number' });
134
134
 
135
135
  await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
136
136
  await page.close();
@@ -159,7 +159,8 @@ test.describe('Table', () => {
159
159
  await page.close();
160
160
  });
161
161
 
162
- test('extant relations work as expected', async ({ browser, browserName }) => {
162
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
163
+ test.skip('extant relations work as expected', async ({ browser, browserName }) => {
163
164
  test.skip(browserName === 'webkit');
164
165
  test.skip(browserName === 'firefox');
165
166
  const { page } = await setupPage(browser, { url: relationsStoryUrl });
@@ -187,7 +188,7 @@ test.describe('Table', () => {
187
188
  // Click on the combobox to open options
188
189
  await page.getByRole('combobox').click();
189
190
  await page.pause();
190
- await page.getByPlaceholder('Search...').focus();
191
+ await page.getByPlaceholder('Search…').focus();
191
192
 
192
193
  // Type the first few letters of an org name.
193
194
  const orgName =
@@ -213,7 +214,8 @@ test.describe('Table', () => {
213
214
  await page.close();
214
215
  });
215
216
 
216
- test('new relations work as expected', async ({ browser, browserName }) => {
217
+ // TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
218
+ test.skip('new relations work as expected', async ({ browser, browserName }) => {
217
219
  test.skip(browserName === 'webkit');
218
220
  test.skip(browserName === 'firefox');
219
221
  const { page } = await setupPage(browser, { url: relationsStoryUrl });
@@ -240,7 +242,7 @@ test.describe('Table', () => {
240
242
 
241
243
  // Click on the combobox to open options
242
244
  await page.getByRole('combobox').click();
243
- await page.getByPlaceholder('Search...').focus();
245
+ await page.getByPlaceholder('Search…').focus();
244
246
 
245
247
  // Type the first few letters of an org name.
246
248
  const orgName = 'Sally';
@@ -254,7 +256,7 @@ test.describe('Table', () => {
254
256
  await page.pause();
255
257
 
256
258
  // Click the save button in the popover
257
- await page.getByTestId('save-button').click();
259
+ await page.getByTestId('create-referenced-object-form').getByTestId('save-button').click();
258
260
 
259
261
  // Assert that the cell element has the org name
260
262
  await expect(targetCell).toHaveText(orgName);
@@ -2,43 +2,52 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { useCallback, useMemo, useRef } from 'react';
5
+ import type * as Schema from 'effect/Schema';
6
+ import { type RefObject, useCallback, useMemo, useRef } from 'react';
6
7
 
7
- import { isMutable, toJsonSchema } from '@dxos/echo/internal';
8
+ import { isMutable } from '@dxos/echo/internal';
8
9
  import { useGlobalFilteredObjects } from '@dxos/plugin-search';
9
10
  import { faker } from '@dxos/random';
10
- import { useClient } from '@dxos/react-client';
11
- import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
11
+ import { type Client, useClient } from '@dxos/react-client';
12
+ import { Filter, type Space, useQuery, useSchema } from '@dxos/react-client/echo';
12
13
  import { useClientProvider } from '@dxos/react-client/testing';
13
- import { DataType, ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
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 = {
24
+ schema: Schema.Schema.AnyNoContext | undefined;
25
+ table: Table.Table | undefined;
26
+ projection: ProjectionModel | undefined;
27
+ tableRef: RefObject<TableController | null>;
28
+ model: TableModel | undefined;
29
+ presentation: TablePresentation | undefined;
30
+ space: Space | undefined;
31
+ client: Client | undefined;
32
+ handleInsertRow: () => void;
33
+ handleSaveView: () => void;
34
+ handleDeleteRows: (rowIndex: number, objects: any[]) => void;
35
+ handleDeleteColumn: (fieldId: string) => void;
36
+ };
37
+
21
38
  /**
22
39
  * Custom hook to create and manage a test table model for storybook demonstrations.
23
40
  * Provides table data, schema, and handlers for table operations.
24
41
  */
25
- export const useTestTableModel = () => {
42
+ export const useTestTableModel = (): TestTableModel => {
26
43
  const client = useClient();
27
44
  const { space } = useClientProvider();
28
45
 
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;
46
+ const tables = useQuery(space, Filter.type(Table.Table));
47
+ const table = tables.at(0);
48
+ const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
32
49
  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]);
50
+ const projection = useProjectionModel(schema, table);
42
51
 
43
52
  const features = useMemo(
44
53
  () => ({
@@ -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();
@@ -111,7 +119,7 @@ export const useTestTableModel = () => {
111
119
 
112
120
  return {
113
121
  schema,
114
- view,
122
+ table,
115
123
  projection,
116
124
  tableRef,
117
125
  model,
@@ -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',
@@ -0,0 +1,89 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Match from 'effect/Match';
6
+ import * as Schema from 'effect/Schema';
7
+
8
+ import { Obj, Ref, Type } from '@dxos/echo';
9
+ import { FormInputAnnotation, type JsonPath, type JsonSchemaType, LabelAnnotation } from '@dxos/echo/internal';
10
+ import { type SimpleType } from '@dxos/effect';
11
+ import { View, ViewAnnotation, getSchemaProperties } from '@dxos/schema';
12
+
13
+ const TableSchema = Schema.Struct({
14
+ name: Schema.String.pipe(Schema.optional),
15
+
16
+ view: Type.Ref(View.View).pipe(FormInputAnnotation.set(false)),
17
+
18
+ sizes: Schema.Record({
19
+ // TODO(wittjosiah): Should be JsonPath.
20
+ key: Schema.String,
21
+ value: Schema.Number,
22
+ }).pipe(Schema.mutable, FormInputAnnotation.set(false)),
23
+ }).pipe(
24
+ Type.Obj({
25
+ typename: 'dxos.org/type/Table',
26
+ version: '0.2.0',
27
+ }),
28
+ LabelAnnotation.set(['name']),
29
+ ViewAnnotation.set(true),
30
+ );
31
+
32
+ export interface Table extends Schema.Schema.Type<typeof TableSchema> {}
33
+ export interface TableEncoded extends Schema.Schema.Encoded<typeof TableSchema> {}
34
+ export const Table: Schema.Schema<Table, TableEncoded> = TableSchema;
35
+
36
+ type MakeProps = Omit<Partial<Obj.MakeProps<typeof Table>>, 'view'> & {
37
+ view: View.View;
38
+ /** Required to auto-size columns. */
39
+ jsonSchema?: JsonSchemaType;
40
+ };
41
+
42
+ /**
43
+ * Make a table as a view of a data set.
44
+ */
45
+ export const make = ({ name, sizes = {}, view, jsonSchema }: MakeProps): Table => {
46
+ const table = Obj.make(Table, { name, view: Ref.make(view), sizes });
47
+
48
+ // Preset sizes.
49
+ if (jsonSchema) {
50
+ const schema = Type.toEffectSchema(jsonSchema);
51
+ const properties = getSchemaProperties(schema.ast, {});
52
+ for (const property of properties) {
53
+ if (sizes?.[property.name]) {
54
+ table.sizes[property.name] = sizes[property.name];
55
+ continue;
56
+ }
57
+
58
+ Match.type<SimpleType>().pipe(
59
+ Match.when('boolean', () => {
60
+ table.sizes[property.name as JsonPath] = 100;
61
+ }),
62
+ Match.when('number', () => {
63
+ table.sizes[property.name as JsonPath] = 100;
64
+ }),
65
+ Match.orElse(() => {
66
+ // Noop.
67
+ }),
68
+ )(property.type);
69
+ }
70
+ }
71
+
72
+ return table;
73
+ };
74
+
75
+ //
76
+ // V1
77
+ //
78
+
79
+ export const TableV1 = Schema.Struct({
80
+ sizes: Schema.Record({
81
+ key: Schema.String,
82
+ value: Schema.Number,
83
+ }).pipe(Schema.mutable),
84
+ }).pipe(
85
+ Type.Obj({
86
+ typename: 'dxos.org/type/Table',
87
+ version: '0.1.0',
88
+ }),
89
+ );
@@ -2,4 +2,4 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * as Table from './schema';
5
+ export * as Table from './Table';