@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.72ec0f3

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 (132) hide show
  1. package/dist/lib/browser/chunk-2PFAPQL4.mjs +66 -0
  2. package/dist/lib/browser/chunk-2PFAPQL4.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-GS2UNGJK.mjs} +493 -469
  4. package/dist/lib/browser/chunk-GS2UNGJK.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +610 -695
  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 +14 -9
  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-6POOFFYF.mjs +67 -0
  12. package/dist/lib/node-esm/chunk-6POOFFYF.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-TRR3C5NX.mjs} +493 -469
  14. package/dist/lib/node-esm/chunk-TRR3C5NX.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +610 -695
  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 +14 -9
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -1
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
  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 +107 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +8 -4
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +42 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  45. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  48. package/dist/types/src/components/testing.d.ts +3 -3
  49. package/dist/types/src/components/testing.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useAddRow.d.ts +3 -2
  51. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useTableModel.d.ts +4 -4
  53. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  54. package/dist/types/src/model/modal-controller.d.ts +0 -7
  55. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  56. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  57. package/dist/types/src/model/table-model.d.ts +11 -9
  58. package/dist/types/src/model/table-model.d.ts.map +1 -1
  59. package/dist/types/src/model/table-presentation.d.ts +2 -1
  60. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  61. package/dist/types/src/model/table-sorting.d.ts +3 -4
  62. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  63. package/dist/types/src/testing/useTestTableModel.d.ts +3 -22
  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/types/index.d.ts +1 -1
  67. package/dist/types/src/types/index.d.ts.map +1 -1
  68. package/dist/types/src/types/schema.d.ts +25 -5
  69. package/dist/types/src/types/schema.d.ts.map +1 -1
  70. package/dist/types/src/util/dynamic-table.d.ts +5 -6
  71. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  72. package/dist/types/src/util/index.d.ts +0 -1
  73. package/dist/types/src/util/index.d.ts.map +1 -1
  74. package/dist/types/src/util/schema.d.ts +1 -1
  75. package/dist/types/src/util/schema.d.ts.map +1 -1
  76. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  77. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  78. package/dist/types/tsconfig.tsbuildinfo +1 -1
  79. package/package.json +61 -59
  80. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  81. package/src/components/Table/ColumnSettings.tsx +1 -1
  82. package/src/components/Table/DynamicTable.stories.tsx +18 -14
  83. package/src/components/Table/DynamicTable.tsx +8 -10
  84. package/src/components/Table/Relations.stories.tsx +242 -0
  85. package/src/components/Table/RowActionsMenu.tsx +23 -15
  86. package/src/components/Table/Table.stories.tsx +72 -60
  87. package/src/components/Table/Table.tsx +140 -115
  88. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  89. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
  90. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +26 -65
  91. package/src/components/TableCellEditor/TableCellEditor.tsx +74 -224
  92. package/src/components/TableCellEditor/index.ts +1 -0
  93. package/src/components/TableToolbar/TableToolbar.tsx +34 -24
  94. package/src/components/testing.ts +2 -2
  95. package/src/hooks/useAddRow.ts +7 -5
  96. package/src/hooks/useTableModel.ts +6 -6
  97. package/src/model/modal-controller.ts +1 -29
  98. package/src/model/selection-model.ts +3 -2
  99. package/src/model/table-model.test.ts +20 -11
  100. package/src/model/table-model.ts +46 -31
  101. package/src/model/table-presentation.test.ts +24 -22
  102. package/src/model/table-presentation.ts +74 -33
  103. package/src/model/table-sorting.ts +17 -8
  104. package/src/playwright/TableManager.ts +1 -1
  105. package/src/playwright/smoke.spec.ts +131 -37
  106. package/src/testing/useTestTableModel.ts +12 -8
  107. package/src/translations.ts +2 -2
  108. package/src/types/index.ts +1 -1
  109. package/src/types/schema.ts +63 -6
  110. package/src/util/dynamic-table.ts +16 -17
  111. package/src/util/index.ts +0 -1
  112. package/src/util/schema.ts +4 -4
  113. package/src/util/table-buttons.ts +4 -3
  114. package/src/util/table-controls.ts +4 -1
  115. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  116. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  117. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  118. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  119. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  120. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  121. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  122. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  123. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  124. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  125. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  126. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  127. package/dist/types/src/util/table.d.ts +0 -11
  128. package/dist/types/src/util/table.d.ts.map +0 -1
  129. package/src/components/Table/CreateRefPanel.tsx +0 -86
  130. package/src/components/Table/relations.stories.tsx +0 -116
  131. package/src/components/TableCellEditor/extension.ts +0 -61
  132. package/src/util/table.ts +0 -49
@@ -3,10 +3,7 @@
3
3
  //
4
4
 
5
5
  import { signal } from '@preact/signals-core';
6
- import { type RefObject, type MutableRefObject } from 'react';
7
-
8
- import { type Live } from '@dxos/live-object';
9
- import { log } from '@dxos/log';
6
+ import { type MutableRefObject, type RefObject } from 'react';
10
7
 
11
8
  export type ColumnSettingsMode = { type: 'create' } | { type: 'edit'; fieldId: string };
12
9
 
@@ -14,12 +11,6 @@ export type ModalState =
14
11
  | { type: 'row'; rowIndex: number }
15
12
  | { type: 'column'; fieldId: string }
16
13
  | { type: 'refPanel'; targetId: string; typename: string }
17
- | {
18
- type: 'createRefPanel';
19
- typename: string;
20
- initialValues?: Record<string, string>;
21
- onCreate?: (obj: Live<any>) => void;
22
- }
23
14
  | { type: 'columnSettings'; mode: ColumnSettingsMode }
24
15
  | { type: 'closed' };
25
16
 
@@ -68,25 +59,6 @@ export class ModalController {
68
59
  }
69
60
  };
70
61
 
71
- public openCreateRef = (
72
- typename: string,
73
- anchorCell: Element | null,
74
- initialValues?: Record<string, string>,
75
- onCreate?: (obj: Live<any>) => void,
76
- ) => {
77
- if (anchorCell) {
78
- this._triggerRef.current = anchorCell as HTMLElement;
79
- this._state.value = {
80
- type: 'createRefPanel',
81
- typename,
82
- initialValues,
83
- onCreate,
84
- };
85
- } else {
86
- log.warn('anchor cell not found while creating new ref');
87
- }
88
- };
89
-
90
62
  public close = () => {
91
63
  this._state.value = { type: 'closed' };
92
64
  };
@@ -2,13 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, type Signal, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, type Signal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
 
9
- import { type TableRow } from './table-model';
10
9
  import { touch } from '../util';
11
10
 
11
+ import { type TableRow } from './table-model';
12
+
12
13
  export type SelectionMode = 'single' | 'multiple';
13
14
 
14
15
  export class SelectionModel<T extends TableRow> extends Resource {
@@ -3,19 +3,20 @@
3
3
  //
4
4
 
5
5
  import { computed } from '@preact/signals-core';
6
- import { Schema } from 'effect';
6
+ import * as Schema from 'effect/Schema';
7
7
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
8
8
 
9
- import { Obj } from '@dxos/echo';
10
- import { TypedObject } from '@dxos/echo-schema';
11
- import { updateCounter } from '@dxos/echo-schema/testing';
9
+ import { Filter, Query } from '@dxos/echo';
10
+ import { TypedObject } from '@dxos/echo/internal';
11
+ import { updateCounter } from '@dxos/echo/testing';
12
+ import { createEchoSchema } from '@dxos/echo/testing';
12
13
  import { registerSignalsRuntime } from '@dxos/echo-signals';
13
14
  import { live } from '@dxos/live-object';
14
- import { createEchoSchema } from '@dxos/live-object/testing';
15
- import { createView } from '@dxos/schema';
15
+ import { View } from '@dxos/schema';
16
+
17
+ import { Table } from '../types';
16
18
 
17
19
  import { TableModel, type TableModelProps } from './table-model';
18
- import { TableView } from '../types';
19
20
 
20
21
  // TODO(burdon): Tests are disabled in project.json since they bring in plugin deps.
21
22
  // Restore once factored out into react-ui-table.
@@ -24,7 +25,7 @@ registerSignalsRuntime();
24
25
 
25
26
  describe('TableModel', () => {
26
27
  let updateCount = 0;
27
- let model: TableModel;
28
+ let model: any;
28
29
 
29
30
  beforeEach(async () => {
30
31
  updateCount = 0;
@@ -32,6 +33,11 @@ describe('TableModel', () => {
32
33
  onCellUpdate: () => updateCount++,
33
34
  });
34
35
  await model.open();
36
+ model.setRows([
37
+ { id: '1', title: 'Test', completed: false },
38
+ { id: '2', title: 'Test 2', completed: true },
39
+ { id: '3', title: 'Test 3', completed: false },
40
+ ]);
35
41
  });
36
42
 
37
43
  afterEach(async () => {
@@ -72,7 +78,6 @@ describe('TableModel', () => {
72
78
  describe('reactivity', () => {
73
79
  it('pure signals should nest', () => {
74
80
  const signal$ = live({ arr: [{ thingInside: 1 }, { thingInside: 2 }] });
75
-
76
81
  const computed$ = computed(() => {
77
82
  return signal$.arr.map((row) =>
78
83
  computed(() => {
@@ -107,7 +112,11 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
107
112
 
108
113
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
109
114
  const schema = createEchoSchema(Test);
110
- const table = Obj.make(TableView, { sizes: {} });
111
- const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
115
+ const table = Table.make();
116
+ const view = View.make({
117
+ query: Query.select(Filter.type(schema)),
118
+ jsonSchema: schema.jsonSchema,
119
+ presentation: table,
120
+ });
112
121
  return new TableModel({ view, schema: schema.jsonSchema, ...props });
113
122
  };
@@ -2,39 +2,45 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
  import { Obj, Ref } from '@dxos/echo';
9
9
  import {
10
- type FieldSortType,
11
10
  FormatEnum,
12
- getValue,
13
- setValue,
14
11
  type JsonProp,
15
- getSnapshot,
12
+ type JsonSchemaType,
16
13
  getSchema,
14
+ getValue,
15
+ setValue,
17
16
  toEffectSchema,
18
- type JsonSchemaType,
19
- } from '@dxos/echo-schema';
17
+ } from '@dxos/echo/internal';
20
18
  import { invariant } from '@dxos/invariant';
21
19
  import { ObjectId } from '@dxos/keys';
22
- import { isLiveObject } from '@dxos/live-object';
23
- import { fullyQualifiedId } from '@dxos/react-client/echo';
20
+ import { getSnapshot, isLiveObject } from '@dxos/live-object';
21
+ import { type Label } from '@dxos/react-ui';
24
22
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
25
23
  import {
26
24
  type DxGridAxisMeta,
27
- type DxGridPlaneRange,
28
25
  type DxGridPlanePosition,
26
+ type DxGridPlaneRange,
29
27
  type DxGridPosition,
30
28
  } from '@dxos/react-ui-grid';
31
- import { type DataType, ProjectionModel, type PropertyType, validateSchema, type ValidationError } from '@dxos/schema';
29
+ import {
30
+ type FieldSortType,
31
+ ProjectionModel,
32
+ type PropertyType,
33
+ type ValidationError,
34
+ type View,
35
+ validateSchema,
36
+ } from '@dxos/schema';
37
+
38
+ import { Table } from '../types';
39
+ import { touch } from '../util';
40
+ import { extractTagIds } from '../util/tag';
32
41
 
33
42
  import { type SelectionMode, SelectionModel } from './selection-model';
34
43
  import { TableSorting } from './table-sorting';
35
- import { TableView } from '../types';
36
- import { touch } from '../util';
37
- import { extractTagIds } from '../util/tag';
38
44
 
39
45
  // Domain types for cell classification
40
46
  export type TableCellType = 'standard' | 'draft' | 'header';
@@ -53,7 +59,7 @@ export type TableRow = Record<JsonProp, any> & { id: string };
53
59
 
54
60
  export type TableRowAction = {
55
61
  id: string;
56
- translationKey: string;
62
+ label: Label;
57
63
  };
58
64
 
59
65
  export type TableFeatures = {
@@ -68,8 +74,10 @@ const defaultFeatures: TableFeatures = {
68
74
  schemaEditable: false,
69
75
  };
70
76
 
77
+ export type InsertRowResult = 'draft' | 'final';
78
+
71
79
  export type TableModelProps<T extends TableRow = TableRow> = {
72
- view: DataType.View;
80
+ view: View.View;
73
81
  schema: JsonSchemaType;
74
82
  projection?: ProjectionModel;
75
83
  features?: Partial<TableFeatures>;
@@ -78,7 +86,7 @@ export type TableModelProps<T extends TableRow = TableRow> = {
78
86
  pinnedRows?: { top: number[]; bottom: number[] };
79
87
  rowActions?: TableRowAction[];
80
88
  onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
81
- onInsertRow?: (data?: any) => boolean;
89
+ onInsertRow?: (data?: any) => InsertRowResult;
82
90
  onDeleteRows?: (index: number, obj: T[]) => void;
83
91
  onDeleteColumn?: (fieldId: string) => void;
84
92
  onCellUpdate?: (cell: DxGridPosition) => void;
@@ -87,16 +95,16 @@ export type TableModelProps<T extends TableRow = TableRow> = {
87
95
  };
88
96
 
89
97
  export class TableModel<T extends TableRow = TableRow> extends Resource {
90
- private readonly _view: DataType.View;
98
+ private readonly _view: View.View;
91
99
  private readonly _projection: ProjectionModel;
92
- private _table?: TableView;
100
+ private _table?: Table.Table;
93
101
 
94
102
  private readonly _visibleRange = signal<DxGridPlaneRange>({
95
103
  start: { row: 0, col: 0 },
96
104
  end: { row: 0, col: 0 },
97
105
  });
98
106
 
99
- private readonly _onInsertRow?: (data?: any) => boolean;
107
+ private readonly _onInsertRow?: (data?: any) => InsertRowResult;
100
108
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
101
109
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
102
110
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
@@ -132,6 +140,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
132
140
  super();
133
141
  this._view = view;
134
142
  this._projection = projection ?? new ProjectionModel(schema, view.projection);
143
+ this._projection.normalizeView();
135
144
 
136
145
  // TODO(ZaymonFC): Use our more robust config merging module?
137
146
  this._features = { ...defaultFeatures, ...features };
@@ -166,14 +175,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
166
175
  }
167
176
 
168
177
  public get id(): string {
169
- return fullyQualifiedId(this._view);
178
+ return Obj.getDXN(this._view).toString();
170
179
  }
171
180
 
172
- public get view(): DataType.View {
181
+ public get view(): View.View {
173
182
  return this._view;
174
183
  }
175
184
 
176
- public get table(): TableView {
185
+ public get table(): Table.Table {
177
186
  invariant(this._table, 'Model not initialized');
178
187
  return this._table;
179
188
  }
@@ -236,7 +245,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
236
245
 
237
246
  protected override async _open(): Promise<void> {
238
247
  const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
239
- invariant(Obj.instanceOf(TableView, presentation));
248
+ invariant(Obj.instanceOf(Table.Table, presentation));
240
249
  this._table = presentation;
241
250
 
242
251
  this.initializeColumnMeta();
@@ -254,7 +263,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
254
263
  return {
255
264
  grid: meta,
256
265
  frozenColsStart: { 0: { size: 30, resizeable: false } },
257
- frozenColsEnd: { 0: { size: 40, resizeable: false } },
266
+ frozenColsEnd: { 0: { size: 32, resizeable: false } },
258
267
  };
259
268
  });
260
269
  }
@@ -334,11 +343,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
334
343
 
335
344
  public getColumnCount = (): number => this._projection?.fields.length ?? 0;
336
345
 
337
- public insertRow = (): void => {
346
+ public insertRow = (): InsertRowResult => {
338
347
  const result = this._onInsertRow?.();
339
- if (result === false && this._draftRows.value.length === 0) {
348
+ if (result === 'draft' && this._draftRows.value.length === 0) {
340
349
  this.createDraftRow();
341
350
  }
351
+ return result ?? 'final';
342
352
  };
343
353
 
344
354
  private createDraftRow(): void {
@@ -393,14 +403,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
393
403
  return false;
394
404
  }
395
405
 
396
- const success = this._onInsertRow?.(draftRow.data);
406
+ const insertRowResult = this._onInsertRow?.(draftRow.data);
397
407
 
398
- if (success) {
408
+ if (insertRowResult === 'final') {
399
409
  const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
400
410
  this._draftRows.value = newDraftRows;
401
411
  }
402
412
 
403
- return success ?? false;
413
+ return insertRowResult === 'final';
404
414
  };
405
415
 
406
416
  public deleteRow = (rowIndex: number): void => {
@@ -469,7 +479,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
469
479
  }
470
480
 
471
481
  default: {
472
- return formatForEditing({ type: props.type, format: props.format, value });
482
+ return formatForEditing({
483
+ type: props.type,
484
+ format: props.format,
485
+ value,
486
+ });
473
487
  }
474
488
  }
475
489
  };
@@ -497,6 +511,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
497
511
  setValue(snapshot, field.path, transformedValue);
498
512
 
499
513
  const validationErrors = this.validateDraftRowData(snapshot);
514
+ // TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
500
515
  if (validationErrors.length > 0) {
501
516
  const error = validationErrors.find((err) => err.path === field.path);
502
517
  if (error) {
@@ -2,18 +2,19 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
6
- import { describe, expect, it, beforeEach } from 'vitest';
5
+ import * as Schema from 'effect/Schema';
6
+ import { beforeEach, describe, expect, it } from 'vitest';
7
7
 
8
- import { Obj } from '@dxos/echo';
9
- import { TypedObject } from '@dxos/echo-schema';
8
+ import { Filter, Query } from '@dxos/echo';
9
+ import { TypedObject } from '@dxos/echo/internal';
10
+ import { createEchoSchema } from '@dxos/echo/testing';
10
11
  import { live } from '@dxos/live-object';
11
- import { createEchoSchema } from '@dxos/live-object/testing';
12
- import { createView } from '@dxos/schema';
12
+ import { View } from '@dxos/schema';
13
+
14
+ import { Table } from '../types';
13
15
 
14
16
  import { TableModel, type TableModelProps } from './table-model';
15
17
  import { TablePresentation } from './table-presentation';
16
- import { TableView } from '../types';
17
18
 
18
19
  describe('TablePresentation', () => {
19
20
  describe('row reactivity', () => {
@@ -26,8 +27,8 @@ describe('TablePresentation', () => {
26
27
  updateCount = 0;
27
28
  ({ data } = live({
28
29
  data: [
29
- { col1: 'A', col2: 1, col3: true },
30
- { col1: 'B', col2: 2, col3: false },
30
+ { title: 'A', count: 1 },
31
+ { title: 'B', count: 2 },
31
32
  ],
32
33
  }));
33
34
 
@@ -44,8 +45,8 @@ describe('TablePresentation', () => {
44
45
  presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
45
46
 
46
47
  // Trigger a row update.
47
- data[0].col1 = 'New Value';
48
- expect(updateCount).toBe(1);
48
+ data[0].title = 'New Value';
49
+ expect(updateCount).toBe(2);
49
50
 
50
51
  // Verify the new value through getCells.
51
52
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
@@ -56,15 +57,14 @@ describe('TablePresentation', () => {
56
57
  // Set up visible range to include our test data.
57
58
  presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
58
59
 
59
- data.push({ col1: 'C', col2: 3, col3: true });
60
- expect(updateCount).toBe(1);
60
+ data.push({ title: 'C', count: 3 });
61
+ expect(updateCount).toBe(2);
61
62
 
62
63
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
63
64
 
64
65
  // Verify the new row's data.
65
66
  expect(cells['0,2']?.value).toBe('C');
66
67
  expect(cells['1,2']?.value).toBe('3');
67
- expect(cells['2,2']?.value).toBe('true');
68
68
  });
69
69
 
70
70
  it('should handle combined operations reactively', () => {
@@ -72,32 +72,34 @@ describe('TablePresentation', () => {
72
72
  presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
73
73
 
74
74
  // Multiple operations.
75
- data[0].col1 = 'Updated A';
76
- data.push({ col1: 'C', col2: 3, col3: true });
75
+ data[0].title = 'Updated A';
76
+ data.push({ title: 'C', count: 3, completed: true });
77
77
  data.splice(1, 1);
78
78
 
79
79
  // We expect one update per operation affecting visible rows.
80
- expect(updateCount).toBe(3);
80
+ expect(updateCount).toBe(4);
81
81
 
82
82
  const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
83
83
 
84
84
  // Verify final state.
85
85
  expect(cells['0,0']?.value).toBe('Updated A');
86
86
  expect(cells['0,1']?.value).toBe('C');
87
- expect(cells['1,1']?.value).toBe('3');
88
- expect(cells['2,1']?.value).toBe('true');
89
87
  });
90
88
  });
91
89
  });
92
90
 
93
91
  class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
94
92
  title: Schema.String,
95
- completed: Schema.Boolean,
93
+ count: Schema.Number,
96
94
  }) {}
97
95
 
98
96
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
99
97
  const schema = createEchoSchema(Test);
100
- const table = Obj.make(TableView, { sizes: {} });
101
- const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
98
+ const table = Table.make();
99
+ const view = View.make({
100
+ query: Query.select(Filter.type(schema)),
101
+ jsonSchema: schema.jsonSchema,
102
+ presentation: table,
103
+ });
102
104
  return new TableModel({ view, schema: schema.jsonSchema, ...props });
103
105
  };
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import { signal } from '@preact/signals-core';
6
- import { isNotNullable } from 'effect/Predicate';
6
+ import * as Predicate from 'effect/Predicate';
7
7
 
8
8
  import { Obj } from '@dxos/echo';
9
- import { FormatEnum, getValue, TypeEnum } from '@dxos/echo-schema';
9
+ import { FormatEnum, TypeEnum, getValue } from '@dxos/echo/internal';
10
10
  import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
11
11
  import {
12
12
  type DxGridCellValue,
@@ -16,12 +16,13 @@ import {
16
16
  toPlaneCellIndex,
17
17
  } from '@dxos/react-ui-grid';
18
18
  import { mx } from '@dxos/react-ui-theme';
19
- import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
19
+ import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
20
20
 
21
- import { type SelectionMode } from './selection-model';
22
- import { type TableRow, type TableModel } from './table-model';
23
21
  import { tableButtons, tableControls } from '../util';
24
22
 
23
+ import { type SelectionMode } from './selection-model';
24
+ import { type TableModel, type TableRow } from './table-model';
25
+
25
26
  /**
26
27
  * Presentation layer for a table component, handling cell rendering and grid display logic.
27
28
  * Manages rendering of table cells, headers, selection columns, and action columns across
@@ -67,6 +68,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
67
68
  case 'frozenRowsEnd':
68
69
  cells = this.getDraftRowCells(range);
69
70
  break;
71
+ case 'fixedEndStart':
72
+ cells = this.getDraftIconCells(range);
73
+ break;
70
74
  case 'fixedEndEnd':
71
75
  cells = this.getDraftActionCells(range);
72
76
  break;
@@ -137,20 +141,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
137
141
  };
138
142
 
139
143
  const classes = [];
140
- const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
144
+ const formatClasses = cellClassesForFieldType(props);
145
+
141
146
  if (formatClasses) {
142
147
  classes.push(formatClasses);
143
148
  }
149
+
144
150
  const rowSelectionClasses = cellClassesForRowSelection(
145
151
  this.model.selection.isObjectSelected(obj),
146
152
  this.model.selection.selectionMode,
147
153
  );
154
+
148
155
  if (rowSelectionClasses) {
149
156
  classes.push(rowSelectionClasses);
150
157
  }
151
- if (classes.length > 0) {
152
- cell.className = mx(classes.flat());
153
- }
158
+
159
+ cell.className = mx(classes);
154
160
 
155
161
  // Arrays.
156
162
  if (props.type === TypeEnum.Array) {
@@ -184,7 +190,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
184
190
 
185
191
  const tags = targetArray
186
192
  .map(getLabel)
187
- .filter(isNotNullable)
193
+ .filter(Predicate.isNotNullable)
188
194
  .map((title) => {
189
195
  return `<span class="dx-tag" data-hue="neutral">${title}</span>`;
190
196
  })
@@ -199,7 +205,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
199
205
  const targetObj = getValue(obj, field.path)?.target;
200
206
  if (targetObj) {
201
207
  const dxn = Obj.getDXN(targetObj)?.toString();
202
- cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-1"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
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>`;
203
209
  }
204
210
  }
205
211
 
@@ -275,23 +281,34 @@ export class TablePresentation<T extends TableRow = TableRow> {
275
281
  const fields = this.model.projection?.fields ?? [];
276
282
  const draftRows = this.model.draftRows.value;
277
283
 
278
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
279
- const draftRow = draftRows[row];
284
+ // Return cells of the CTA row if no draft row is active
285
+ if (draftRows.length === 0) {
280
286
  for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
281
- const field = fields[col];
282
- if (!field) {
283
- continue;
284
- }
287
+ cells[toPlaneCellIndex({ col, row: 0 })] = {
288
+ value: '',
289
+ readonly: true,
290
+ className: 'dx-grid__row--cta__cell',
291
+ };
292
+ }
293
+ } else {
294
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
295
+ const draftRow = draftRows[row];
296
+ for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
297
+ const cellIndex = toPlaneCellIndex({ col, row });
298
+ const field = fields[col];
299
+ if (!field) {
300
+ continue;
301
+ }
285
302
 
286
- this.createDataCell(cells, draftRow.data, field, col, row);
303
+ this.createDataCell(cells, draftRow.data, field, col, row);
287
304
 
288
- if (this.model.hasDraftRowValidationError(row, field.path)) {
289
- const cellIndex = toPlaneCellIndex({ col, row });
290
- const cellValue = cells[cellIndex];
291
- if (cellValue) {
292
- const existingClasses = cellValue.className || '';
293
- const draftClasses = 'dx-grid__cell--flagged';
294
- cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
305
+ if (this.model.hasDraftRowValidationError(row, field.path)) {
306
+ const cellValue = cells[cellIndex];
307
+ if (cellValue) {
308
+ const existingClasses = cellValue.className || '';
309
+ const draftClasses = 'dx-grid__cell--flagged';
310
+ cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
311
+ }
295
312
  }
296
313
  }
297
314
  }
@@ -310,14 +327,14 @@ export class TablePresentation<T extends TableRow = TableRow> {
310
327
 
311
328
  cells[toPlaneCellIndex({ col, row: 0 })] = {
312
329
  // TODO(burdon): Use same logic as form for fallback title.
313
- value: props.title ?? field.path,
314
- readonly: true,
330
+ value: '',
315
331
  resizeHandle: 'col',
316
332
  accessoryHtml: `
333
+ <span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
317
334
  ${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
318
335
  ${tableButtons.columnSettings.render({ fieldId: field.id })}
319
336
  `,
320
- className: '!bg-toolbarSurface',
337
+ className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
321
338
  };
322
339
  }
323
340
 
@@ -400,14 +417,39 @@ export class TablePresentation<T extends TableRow = TableRow> {
400
417
  const cells: DxGridPlaneCells = {};
401
418
  const draftRows = this.model.draftRows.value;
402
419
 
403
- for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
404
- const draftRow = draftRows[row];
405
- const disabled = !draftRow.valid;
420
+ // Return cells of the CTA row if no draft row is active
421
+ if (draftRows.length === 0) {
422
+ cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
423
+ value: '',
424
+ className: 'dx-grid__row--cta__cell',
425
+ readonly: true,
426
+ };
427
+ } else {
428
+ for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
429
+ const draftRow = draftRows[row];
430
+ const disabled = !draftRow.valid;
431
+
432
+ cells[toPlaneCellIndex({ col: 0, row })] = {
433
+ value: '',
434
+ readonly: true,
435
+ accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
436
+ };
437
+ }
438
+ }
439
+
440
+ return cells;
441
+ }
442
+
443
+ private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
444
+ const cells: DxGridPlaneCells = {};
445
+ const draftRows = this.model.draftRows.value;
406
446
 
447
+ for (let row = range.start.row; row <= range.end.row; row++) {
407
448
  cells[toPlaneCellIndex({ col: 0, row })] = {
408
449
  value: '',
409
450
  readonly: true,
410
- accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
451
+ accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
452
+ className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
411
453
  };
412
454
  }
413
455
 
@@ -426,7 +468,6 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
426
468
 
427
469
  switch (selectionMode) {
428
470
  case 'single':
429
- // TODO(ZaymonFC): @thure, do we need a grid version of 'currentRelated'?
430
471
  return ['!bg-currentRelated dx-grid__cell--no-focus-unfurl hover:bg-hoverSurface !cursor-pointer'];
431
472
  case 'multiple':
432
473
  return ['!bg-gridCellSelected'];