@dxos/react-ui-table 0.8.4-main.1da679c → 0.8.4-main.21d9917

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 (140) hide show
  1. package/dist/lib/browser/chunk-MNZKOFMA.mjs +1746 -0
  2. package/dist/lib/browser/chunk-MNZKOFMA.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1134 -1150
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +29 -39
  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-2ATAIIAO.mjs +1748 -0
  12. package/dist/lib/node-esm/chunk-2ATAIIAO.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1134 -1150
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +29 -39
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +5 -6
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +6 -20
  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 +77 -10
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/Table.d.ts +11 -8
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +76 -11
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -12
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +7 -7
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +5 -6
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  44. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  45. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  47. package/dist/types/src/components/testing.d.ts +23 -16
  48. package/dist/types/src/components/testing.d.ts.map +1 -1
  49. package/dist/types/src/hooks/index.d.ts +2 -1
  50. package/dist/types/src/hooks/index.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  52. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  54. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -8
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/model/modal-controller.d.ts +5 -8
  58. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  59. package/dist/types/src/model/selection-model.d.ts +17 -8
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +103 -24
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +3 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  66. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  67. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  68. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  69. package/dist/types/src/translations.d.ts +4 -4
  70. package/dist/types/src/translations.d.ts.map +1 -1
  71. package/dist/types/src/types/Table.d.ts +31 -0
  72. package/dist/types/src/types/Table.d.ts.map +1 -0
  73. package/dist/types/src/types/index.d.ts +1 -1
  74. package/dist/types/src/types/index.d.ts.map +1 -1
  75. package/dist/types/src/util/dynamic-table.d.ts +13 -12
  76. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  77. package/dist/types/src/util/index.d.ts +2 -0
  78. package/dist/types/src/util/index.d.ts.map +1 -1
  79. package/dist/types/src/util/query-ast.d.ts +7 -0
  80. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  81. package/dist/types/src/util/schema.d.ts +1 -1
  82. package/dist/types/src/util/schema.d.ts.map +1 -1
  83. package/dist/types/src/util/sort.d.ts +12 -0
  84. package/dist/types/src/util/sort.d.ts.map +1 -0
  85. package/dist/types/tsconfig.tsbuildinfo +1 -1
  86. package/package.json +56 -56
  87. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  88. package/src/components/Table/ColumnSettings.tsx +6 -6
  89. package/src/components/Table/DynamicTable.stories.tsx +18 -18
  90. package/src/components/Table/DynamicTable.tsx +23 -19
  91. package/src/components/Table/Relations.stories.tsx +143 -39
  92. package/src/components/Table/RowActionsMenu.tsx +24 -15
  93. package/src/components/Table/Table.stories.tsx +92 -95
  94. package/src/components/Table/Table.tsx +339 -307
  95. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  96. package/src/components/TableCellEditor/FormCellEditor.tsx +116 -129
  97. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +23 -47
  98. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -122
  99. package/src/components/TableCellEditor/index.ts +1 -0
  100. package/src/components/TableToolbar/TableToolbar.tsx +47 -31
  101. package/src/components/testing.ts +39 -13
  102. package/src/hooks/index.ts +2 -1
  103. package/src/hooks/useAddRow.ts +8 -10
  104. package/src/hooks/useProjectionModel.ts +40 -0
  105. package/src/hooks/useTableModel.ts +35 -33
  106. package/src/model/modal-controller.ts +23 -40
  107. package/src/model/selection-model.ts +75 -44
  108. package/src/model/table-model.test.ts +91 -58
  109. package/src/model/table-model.ts +429 -163
  110. package/src/model/table-presentation.test.ts +64 -57
  111. package/src/model/table-presentation.ts +34 -30
  112. package/src/playwright/TableManager.ts +5 -5
  113. package/src/playwright/playwright.config.ts +1 -1
  114. package/src/playwright/smoke.spec.ts +11 -8
  115. package/src/testing/useTestTableModel.ts +44 -35
  116. package/src/translations.ts +3 -3
  117. package/src/types/Table.ts +90 -0
  118. package/src/types/index.ts +1 -1
  119. package/src/util/dynamic-table.ts +41 -30
  120. package/src/util/index.ts +2 -0
  121. package/src/util/query-ast.ts +19 -0
  122. package/src/util/schema.ts +4 -4
  123. package/src/util/sort.ts +90 -0
  124. package/dist/lib/browser/chunk-63UUF4IG.mjs +0 -66
  125. package/dist/lib/browser/chunk-63UUF4IG.mjs.map +0 -7
  126. package/dist/lib/browser/chunk-BBXDTWCX.mjs +0 -1822
  127. package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-K734DJB7.mjs +0 -67
  129. package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +0 -7
  130. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs +0 -1823
  131. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +0 -7
  132. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  133. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  134. package/dist/types/src/model/table-sorting.d.ts +0 -52
  135. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  136. package/dist/types/src/types/schema.d.ts +0 -28
  137. package/dist/types/src/types/schema.d.ts.map +0 -1
  138. package/src/components/Table/CreateRefPanel.tsx +0 -103
  139. package/src/model/table-sorting.ts +0 -225
  140. package/src/types/schema.ts +0 -73
@@ -2,23 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
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-schema';
8
+ import { type Database, Format, Obj, Order, Query, type QueryAST, Ref } from '@dxos/echo';
9
+ import { type JsonProp, type JsonSchemaType, type Mutable, toEffectSchema } from '@dxos/echo/internal';
10
+ import { getSnapshot } from '@dxos/echo/internal';
11
+ import { getValue, setValue } from '@dxos/effect';
18
12
  import { invariant } from '@dxos/invariant';
19
13
  import { ObjectId } from '@dxos/keys';
20
- import { getSnapshot, isLiveObject } from '@dxos/live-object';
21
- import { fullyQualifiedId } from '@dxos/react-client/echo';
22
14
  import { type Label } from '@dxos/react-ui';
23
15
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
24
16
  import {
@@ -27,21 +19,57 @@ import {
27
19
  type DxGridPlaneRange,
28
20
  type DxGridPosition,
29
21
  } from '@dxos/react-ui-grid';
30
- import {
31
- type DataType,
32
- type FieldSortType,
33
- ProjectionModel,
34
- type PropertyType,
35
- type ValidationError,
36
- validateSchema,
37
- } from '@dxos/schema';
38
-
39
- import { Table } from '../types';
40
- import { touch } from '../util';
22
+ import { type ProjectionModel, type PropertyType, type ValidationError, type View, validateSchema } from '@dxos/schema';
23
+
24
+ import { type Table } from '../types';
25
+ import { extractOrder } from '../util';
26
+ import { compareValues } from '../util/sort';
41
27
  import { extractTagIds } from '../util/tag';
42
28
 
29
+ /**
30
+ * Field sort configuration.
31
+ */
32
+ export type FieldSortType = {
33
+ fieldId: string;
34
+ direction: QueryAST.OrderDirection;
35
+ };
36
+
43
37
  import { type SelectionMode, SelectionModel } from './selection-model';
44
- import { TableSorting } from './table-sorting';
38
+
39
+ /**
40
+ * Callback type for wrapping mutations in Obj.change().
41
+ * Contains separate callbacks for table object and row mutations.
42
+ */
43
+ export type TableChangeCallback<T extends TableRow> = {
44
+ /** Callback to wrap table object mutations. */
45
+ table: (mutate: (mutableTable: Table.Table) => void) => void;
46
+ /** Callback to wrap row mutations. */
47
+ row: (row: T, mutate: (mutableRow: T) => void) => void;
48
+ };
49
+
50
+ /**
51
+ * Creates a change callback for ECHO-backed table and rows.
52
+ * Use this when the table and rows are stored in the ECHO database.
53
+ */
54
+ export const createEchoChangeCallback = <T extends TableRow>(table: Table.Table): TableChangeCallback<T> => ({
55
+ table: (mutate) => Obj.change(table, (mutableTable) => mutate(mutableTable as unknown as Table.Table)),
56
+ row: (row, mutate) => {
57
+ if (Obj.isObject(row)) {
58
+ Obj.change(row, (mutableRow) => mutate(mutableRow as T));
59
+ } else {
60
+ mutate(row);
61
+ }
62
+ },
63
+ });
64
+
65
+ /**
66
+ * Creates a change callback that directly mutates objects without wrapping.
67
+ * Use this for plain JavaScript objects (tests, non-ECHO scenarios).
68
+ */
69
+ export const createDirectChangeCallback = <T extends TableRow>(table: Table.Table): TableChangeCallback<T> => ({
70
+ table: (mutate) => mutate(table),
71
+ row: (row, mutate) => mutate(row),
72
+ });
45
73
 
46
74
  // Domain types for cell classification
47
75
  export type TableCellType = 'standard' | 'draft' | 'header';
@@ -78,11 +106,16 @@ const defaultFeatures: TableFeatures = {
78
106
  export type InsertRowResult = 'draft' | 'final';
79
107
 
80
108
  export type TableModelProps<T extends TableRow = TableRow> = {
81
- view: DataType.View;
82
- schema: JsonSchemaType;
83
- projection?: ProjectionModel;
109
+ registry: Registry.Registry;
110
+ object: Table.Table;
111
+ projection: ProjectionModel;
112
+ db?: Database.Database;
113
+ /**
114
+ * Callbacks to wrap mutations in Obj.change().
115
+ * Use createEchoChangeCallback() for ECHO-backed objects or createDirectChangeCallback() for plain objects.
116
+ */
117
+ change?: TableChangeCallback<T>;
84
118
  features?: Partial<TableFeatures>;
85
- sorting?: FieldSortType[];
86
119
  initialSelection?: string[];
87
120
  pinnedRows?: { top: number[]; bottom: number[] };
88
121
  rowActions?: TableRowAction[];
@@ -96,14 +129,13 @@ export type TableModelProps<T extends TableRow = TableRow> = {
96
129
  };
97
130
 
98
131
  export class TableModel<T extends TableRow = TableRow> extends Resource {
99
- private readonly _view: DataType.View;
132
+ private readonly _registry: Registry.Registry;
133
+ private readonly _object: Table.Table;
100
134
  private readonly _projection: ProjectionModel;
101
- private _table?: Table.Table;
135
+ private readonly _db?: Database.Database;
136
+ private readonly _change: TableChangeCallback<T>;
102
137
 
103
- private readonly _visibleRange = signal<DxGridPlaneRange>({
104
- start: { row: 0, col: 0 },
105
- end: { row: 0, col: 0 },
106
- });
138
+ private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
107
139
 
108
140
  private readonly _onInsertRow?: (data?: any) => InsertRowResult;
109
141
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
@@ -114,20 +146,32 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
114
146
  private readonly _rowActions: TableRowAction[];
115
147
  private readonly _features: TableFeatures;
116
148
 
117
- private readonly _rows = signal<T[]>([]);
118
- private readonly _draftRows = signal<DraftRow<T>[]>([]);
119
- private readonly _sorting: TableSorting<T>;
149
+ private readonly _rows: Atom.Writable<T[]>;
150
+ private readonly _draftRows: Atom.Writable<DraftRow<T>[]>;
151
+ // In-memory sort state - changes are local until saved.
152
+ private readonly _inMemorySort: Atom.Writable<FieldSortType | undefined>;
153
+ // Derived atom for persisted sort from view.query.ast.
154
+ private readonly _persistedSort: Atom.Atom<FieldSortType | undefined>;
155
+ // Derived atom for current sort (in-memory takes precedence over persisted).
156
+ private readonly _sorting: Atom.Atom<FieldSortType | undefined>;
157
+ // Derived atom for sorted rows (used by rows getter and SelectionModel).
158
+ private readonly _sortedRows: Atom.Atom<T[]>;
159
+ // Derived atom for viewDirty state.
160
+ private readonly _viewDirty: Atom.Atom<boolean>;
120
161
 
121
162
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
122
163
  private _selection!: SelectionModel<T>;
123
- private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
164
+ private _columnMeta?: Atom.Atom<DxGridAxisMeta>;
165
+ // Counter atom that increments when cells are updated - allows UI to react to cell changes.
166
+ private readonly _cellUpdateCounter: Atom.Writable<number>;
124
167
 
125
168
  constructor({
126
- view,
127
- schema,
169
+ registry,
170
+ object,
128
171
  projection,
172
+ db,
173
+ change,
129
174
  features = {},
130
- sorting = [],
131
175
  initialSelection = [],
132
176
  pinnedRows = { top: [], bottom: [] },
133
177
  rowActions = [],
@@ -139,8 +183,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
139
183
  onRowAction,
140
184
  }: TableModelProps<T>) {
141
185
  super();
142
- this._view = view;
143
- this._projection = projection ?? new ProjectionModel(schema, view.projection);
186
+ this._registry = registry;
187
+ this._object = object;
188
+ this._projection = projection;
189
+ this._projection.normalizeView();
190
+ this._db = db;
191
+ this._change = change ?? createEchoChangeCallback<T>(object);
144
192
 
145
193
  // TODO(ZaymonFC): Use our more robust config merging module?
146
194
  this._features = { ...defaultFeatures, ...features };
@@ -150,20 +198,96 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
150
198
  'Single selection is not compatible with editable tables.',
151
199
  );
152
200
 
153
- this._sorting = new TableSorting(this._rows, this._view, this._projection);
201
+ // Initialize writable atoms.
202
+ this._visibleRange = Atom.make<DxGridPlaneRange>({
203
+ start: { row: 0, col: 0 },
204
+ end: { row: 0, col: 0 },
205
+ });
206
+ this._rows = Atom.make<T[]>([]);
207
+ this._draftRows = Atom.make<DraftRow<T>[]>([]);
208
+ this._inMemorySort = Atom.make<FieldSortType | undefined>(undefined);
209
+ this._cellUpdateCounter = Atom.make<number>(0);
210
+
211
+ // Create derived atom for persisted sort from view.query.ast.
212
+ this._persistedSort = Atom.make((_get) => {
213
+ // Note: This depends on the view being loaded and the projection being set.
214
+ // The view is loaded in _open().
215
+ const viewSnapshot = Obj.getSnapshot(this.view);
216
+ const ast = viewSnapshot.query.ast;
217
+ const orders = extractOrder(ast);
218
+
219
+ if (orders && orders.length > 0) {
220
+ const firstOrder = orders[0];
221
+ if (firstOrder.kind === 'property') {
222
+ // Find field by property path.
223
+ const field = this._projection.getFields().find((f) => f.path === firstOrder.property);
224
+ if (field) {
225
+ return {
226
+ fieldId: field.id,
227
+ direction: firstOrder.direction,
228
+ };
229
+ }
230
+ }
231
+ }
232
+
233
+ return undefined;
234
+ });
154
235
 
155
- if (sorting.length > 0) {
156
- const [sort] = sorting;
157
- this._sorting.setSort(sort.fieldId, sort.direction);
158
- }
236
+ // Create derived atom for current sort (in-memory takes precedence over persisted).
237
+ this._sorting = Atom.make((get) => {
238
+ const inMemorySort = get(this._inMemorySort);
239
+ return inMemorySort ?? get(this._persistedSort);
240
+ });
241
+
242
+ // Create derived atom for sorted rows (used by rows getter and SelectionModel).
243
+ this._sortedRows = Atom.make((get) => {
244
+ const rows = get(this._rows);
245
+ const sort = get(this._sorting);
246
+
247
+ if (!sort || rows.length === 0) {
248
+ return rows;
249
+ }
250
+
251
+ const field = this._projection.getFields().find((f) => f.id === sort.fieldId);
252
+ if (!field) {
253
+ return rows;
254
+ }
255
+
256
+ // Get field projection to check format.
257
+ const fieldProjection = this._projection.getFieldProjection(field.id);
258
+
259
+ const sorted = [...rows].sort((a, b) => {
260
+ const aValue = getValue(a, field.path);
261
+ const bValue = getValue(b, field.path);
262
+ return compareValues(aValue, bValue, fieldProjection.props.format, sort.direction);
263
+ });
264
+
265
+ return sorted;
266
+ });
159
267
 
160
268
  this._selection = new SelectionModel(
161
- this._sorting.sortedRows,
269
+ this._registry,
270
+ this._sortedRows,
162
271
  this._features.selection.mode ?? 'multiple',
163
272
  initialSelection,
164
273
  () => this._onRowOrderChange?.(),
165
274
  );
166
275
 
276
+ // Create derived atom for viewDirty.
277
+ this._viewDirty = Atom.make((get) => {
278
+ const inMemorySort = get(this._inMemorySort);
279
+ if (!inMemorySort) {
280
+ return false;
281
+ }
282
+
283
+ const persisted = get(this._persistedSort);
284
+ if (!persisted) {
285
+ return true; // In-memory sort but no persisted sort.
286
+ }
287
+
288
+ return inMemorySort.fieldId !== persisted.fieldId || inMemorySort.direction !== persisted.direction;
289
+ });
290
+
167
291
  this._pinnedRows = pinnedRows;
168
292
  this._rowActions = rowActions;
169
293
  this._onInsertRow = onInsertRow;
@@ -175,24 +299,53 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
175
299
  }
176
300
 
177
301
  public get id(): string {
178
- return fullyQualifiedId(this._view);
302
+ return Obj.getDXN(this._object).toString();
179
303
  }
180
304
 
181
- public get view(): DataType.View {
182
- return this._view;
305
+ public get view(): View.View {
306
+ const view = this._object.view.target;
307
+ invariant(view, 'Table model not initialized');
308
+ return view;
183
309
  }
184
310
 
185
311
  public get table(): Table.Table {
186
- invariant(this._table, 'Model not initialized');
187
- return this._table;
312
+ return this._object;
188
313
  }
189
314
 
190
315
  public get projection(): ProjectionModel {
191
316
  return this._projection;
192
317
  }
193
318
 
194
- public get rows(): ReadonlySignal<T[]> {
195
- return this.sorting.sortedRows;
319
+ public get db(): Database.Database | undefined {
320
+ return this._db;
321
+ }
322
+
323
+ /**
324
+ * Atom for reactive access to rows with in-memory sorting applied.
325
+ */
326
+ public get rows(): Atom.Atom<T[]> {
327
+ return this._sortedRows;
328
+ }
329
+
330
+ /**
331
+ * Gets rows with in-memory sorting applied.
332
+ * In-memory sort is local to this instance until saved.
333
+ */
334
+ public getRows(): T[] {
335
+ return this._registry.get(this._sortedRows);
336
+ }
337
+
338
+ /** Atom for reactive access to cell changes. */
339
+ public get cellUpdate(): Atom.Atom<number> {
340
+ return this._cellUpdateCounter;
341
+ }
342
+
343
+ /**
344
+ * Change a row using the configured change callback.
345
+ * Use this instead of directly mutating to ensure consistent mutation handling.
346
+ */
347
+ public changeRow(row: T, mutate: (mutableRow: T) => void): void {
348
+ this._change.row(row, mutate);
196
349
  }
197
350
 
198
351
  public get features(): TableFeatures {
@@ -200,28 +353,30 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
200
353
  }
201
354
 
202
355
  /**
203
- * @reactive
356
+ * Gets the current sort state.
357
+ * Priority: in-memory sort (local changes) > persisted sort (from query AST).
204
358
  */
205
- public get sorting(): TableSorting<T> {
206
- return this._sorting;
359
+ public getSorting(): FieldSortType | undefined {
360
+ return this._registry.get(this._sorting);
207
361
  }
208
362
 
209
363
  /**
210
364
  * Gets the row data at the specified display index.
211
365
  */
212
366
  public getRowAt(displayIndex: number): T | undefined {
213
- if (displayIndex < 0 || displayIndex >= this.sorting.sortedRows.value.length) {
367
+ const sortedRows = this._registry.get(this._sortedRows);
368
+ if (displayIndex < 0 || displayIndex >= sortedRows.length) {
214
369
  return undefined;
215
370
  }
216
371
 
217
- return this.sorting.sortedRows.value[displayIndex];
372
+ return sortedRows[displayIndex];
218
373
  }
219
374
 
220
375
  public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
221
376
  return this._pinnedRows;
222
377
  }
223
378
 
224
- public get columnMeta(): ReadonlySignal<DxGridAxisMeta> {
379
+ public get columnMeta(): Atom.Atom<DxGridAxisMeta> {
225
380
  invariant(this._columnMeta);
226
381
  return this._columnMeta;
227
382
  }
@@ -230,11 +385,6 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
230
385
  return this._selection;
231
386
  }
232
387
 
233
- /** @reactive */
234
- public get isViewDirty(): boolean {
235
- return this.sorting.isDirty;
236
- }
237
-
238
388
  public get rowActions(): TableRowAction[] {
239
389
  return this._rowActions;
240
390
  }
@@ -244,18 +394,19 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
244
394
  //
245
395
 
246
396
  protected override async _open(): Promise<void> {
247
- const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
248
- invariant(Obj.instanceOf(Table.Table, presentation));
249
- this._table = presentation;
250
-
397
+ await this._object.view.load();
251
398
  this.initializeColumnMeta();
252
399
  this.initializeEffects();
253
400
  await this._selection.open(this._ctx);
254
401
  }
255
402
 
256
403
  private initializeColumnMeta(): void {
257
- this._columnMeta = computed(() => {
258
- const fields = this._projection?.fields ?? [];
404
+ invariant(this._object.view.target, 'View must be loaded before initializing column meta');
405
+
406
+ // Derive columnMeta from the projection's fields atom - updates when fields change.
407
+ // The projection.fields atom handles subscriptions to view changes internally.
408
+ this._columnMeta = Atom.make((get) => {
409
+ const fields = get(this._projection.fields);
259
410
  const meta = Object.fromEntries(
260
411
  fields.map((field, index: number) => [index, { size: this.table.sizes[field.path] ?? 256, resizeable: true }]),
261
412
  );
@@ -269,40 +420,51 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
269
420
  }
270
421
 
271
422
  private initializeEffects(): void {
272
- const rowOrderWatcher = effect(() => {
273
- touch(this.sorting.sortedRows.value);
423
+ // Subscribe to rows changes to notify row order changed.
424
+ const rowsUnsubscribe = this._registry.subscribe(this._rows, () => {
274
425
  this._onRowOrderChange?.();
275
426
  });
276
- this._ctx.onDispose(rowOrderWatcher);
277
-
278
- /**
279
- * Creates reactive effects for each row in the visible range.
280
- * Subscribes to changes in the visible range, recreating row effects when it changes.
281
- * When a row's data changes, invokes onCellUpdate to notify subscribers and trigger UI updates.
282
- */
283
- const rowEffectManager = effect(() => {
284
- const { start, end } = touch(this._visibleRange.value);
285
- const rowEffects: ReturnType<typeof effect>[] = [];
286
- for (let row = start.row; row <= end.row; row++) {
287
- rowEffects.push(
288
- effect(() => {
289
- const obj = this.sorting.sortedRows.value[row];
290
- this._projection?.fields.forEach((field) => touch(getValue(obj, field.path)));
291
- this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
292
- }),
293
- );
427
+ this._ctx.onDispose(rowsUnsubscribe);
428
+
429
+ // Track row subscriptions for cleanup.
430
+ const rowSubscriptions = new Map<string, () => void>();
431
+
432
+ // Subscribe to visible range changes to set up row subscriptions.
433
+ const visibleRangeUnsubscribe = this._registry.subscribe(this._visibleRange, () => {
434
+ // Unsubscribe from old row subscriptions.
435
+ for (const [id, unsub] of rowSubscriptions) {
436
+ unsub();
437
+ rowSubscriptions.delete(id);
294
438
  }
295
439
 
296
- return () => rowEffects.forEach((cleanup) => cleanup());
440
+ const { start, end } = this._registry.get(this._visibleRange);
441
+ const sortedRows = this._registry.get(this._sortedRows);
442
+
443
+ // Subscribe to each visible row's changes.
444
+ for (let row = start.row; row <= end.row && row < sortedRows.length; row++) {
445
+ const obj = sortedRows[row];
446
+ if (Obj.isObject(obj) && !rowSubscriptions.has(obj.id)) {
447
+ const unsub = Obj.subscribe(obj, () => {
448
+ // Increment cell update counter to notify UI of changes.
449
+ this._registry.set(this._cellUpdateCounter, this._registry.get(this._cellUpdateCounter) + 1);
450
+ this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
451
+ });
452
+ rowSubscriptions.set(obj.id, unsub);
453
+ }
454
+ }
455
+ });
456
+ this._ctx.onDispose(() => {
457
+ visibleRangeUnsubscribe();
458
+ for (const unsub of rowSubscriptions.values()) {
459
+ unsub();
460
+ }
297
461
  });
298
- this._ctx.onDispose(rowEffectManager);
299
462
 
300
- const draftRowsWatcher = effect(() => {
301
- touch(this._draftRows.value);
302
- // Notify grid that draft rows have changed
463
+ // Subscribe to draft rows changes to notify grid.
464
+ const draftRowsUnsubscribe = this._registry.subscribe(this._draftRows, () => {
303
465
  this._onRowOrderChange?.();
304
466
  });
305
- this._ctx.onDispose(draftRowsWatcher);
467
+ this._ctx.onDispose(draftRowsUnsubscribe);
306
468
  }
307
469
 
308
470
  //
@@ -310,21 +472,25 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
310
472
  //
311
473
 
312
474
  public setRows = (obj: T[]): void => {
313
- this._rows.value = obj;
475
+ this._registry.set(this._rows, obj);
314
476
  };
315
477
 
316
- public getRowCount = (): number => this._rows.value.length;
478
+ public getRowCount = (): number => this._registry.get(this._sortedRows).length;
317
479
 
318
- /**
319
- * @reactive
320
- */
321
- public getDraftRowCount = (): number => this._draftRows.value.length;
480
+ public getDraftRowCount(): number {
481
+ return this._registry.get(this._draftRows).length;
482
+ }
322
483
 
323
- // TODO(ZaymonFC): Return .value instead of exposing the signal.
324
- public get draftRows(): ReadonlySignal<DraftRow<T>[]> {
484
+ /** Atom for reactive access to draft rows. */
485
+ public get draftRows(): Atom.Atom<DraftRow<T>[]> {
325
486
  return this._draftRows;
326
487
  }
327
488
 
489
+ /** Gets the current draft rows. */
490
+ public getDraftRows(): DraftRow<T>[] {
491
+ return this._registry.get(this._draftRows);
492
+ }
493
+
328
494
  /**
329
495
  * Checks if a specific field path has validation errors for a given draft row.
330
496
  * @param draftRowIndex - The index of the draft row to check
@@ -332,20 +498,20 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
332
498
  * @returns true if the field has validation errors, false otherwise
333
499
  */
334
500
  public hasDraftRowValidationError(draftRowIndex: number, fieldPath: string): boolean {
335
- if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
501
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
336
502
  return false;
337
503
  }
338
504
 
339
- const draftRow = this._draftRows.value[draftRowIndex];
505
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
340
506
  const validationErrors = draftRow.validationErrors || [];
341
507
  return validationErrors.some((error) => error.path === fieldPath);
342
508
  }
343
509
 
344
- public getColumnCount = (): number => this._projection?.fields.length ?? 0;
510
+ public getColumnCount = (): number => this._projection?.getFields().length ?? 0;
345
511
 
346
512
  public insertRow = (): InsertRowResult => {
347
513
  const result = this._onInsertRow?.();
348
- if (result === 'draft' && this._draftRows.value.length === 0) {
514
+ if (result === 'draft' && this._registry.get(this._draftRows).length === 0) {
349
515
  this.createDraftRow();
350
516
  }
351
517
  return result ?? 'final';
@@ -366,15 +532,15 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
366
532
  validationErrors,
367
533
  };
368
534
 
369
- this._draftRows.value = [...this._draftRows.value, draftRow];
535
+ this._registry.set(this._draftRows, [...this._registry.get(this._draftRows), draftRow]);
370
536
  }
371
537
 
372
538
  private validateDraftRow(draftRowIndex: number): void {
373
- if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
539
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
374
540
  return;
375
541
  }
376
542
 
377
- const draftRow = this._draftRows.value[draftRowIndex];
543
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
378
544
  const validationErrors = this.validateDraftRowData(draftRow.data);
379
545
 
380
546
  const updatedDraftRow = {
@@ -383,9 +549,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
383
549
  validationErrors,
384
550
  };
385
551
 
386
- const newDraftRows = [...this._draftRows.value];
552
+ const newDraftRows = [...this._registry.get(this._draftRows)];
387
553
  newDraftRows[draftRowIndex] = updatedDraftRow;
388
- this._draftRows.value = newDraftRows;
554
+ this._registry.set(this._draftRows, newDraftRows);
389
555
  }
390
556
 
391
557
  private validateDraftRowData(data: T): ValidationError[] {
@@ -394,11 +560,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
394
560
  }
395
561
 
396
562
  public commitDraftRow = (draftRowIndex: number): boolean => {
397
- if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
563
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
398
564
  return false;
399
565
  }
400
566
 
401
- const draftRow = this._draftRows.value[draftRowIndex];
567
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
402
568
  if (!draftRow.valid) {
403
569
  return false;
404
570
  }
@@ -406,26 +572,28 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
406
572
  const insertRowResult = this._onInsertRow?.(draftRow.data);
407
573
 
408
574
  if (insertRowResult === 'final') {
409
- const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
410
- this._draftRows.value = newDraftRows;
575
+ const newDraftRows = this._registry.get(this._draftRows).filter((_, index) => index !== draftRowIndex);
576
+ this._registry.set(this._draftRows, newDraftRows);
411
577
  }
412
578
 
413
579
  return insertRowResult === 'final';
414
580
  };
415
581
 
416
582
  public deleteRow = (rowIndex: number): void => {
417
- const row = this._sorting.getDataIndex(rowIndex);
418
- const obj = this._rows.value[row];
583
+ const sortedRows = this._registry.get(this._sortedRows);
584
+ const obj = sortedRows[rowIndex];
419
585
  const objectsToDelete = [];
420
586
 
421
- if (this.features.selection.enabled && this._selection.hasSelection.value) {
587
+ if (this.features.selection.enabled && this._selection.hasSelection) {
422
588
  const selectedRows = this._selection.getSelectedRows();
423
589
  objectsToDelete.push(...selectedRows);
424
- } else {
590
+ } else if (obj) {
425
591
  objectsToDelete.push(obj);
426
592
  }
427
593
 
428
- this._onDeleteRows?.(row, objectsToDelete);
594
+ if (objectsToDelete.length > 0) {
595
+ this._onDeleteRows?.(rowIndex, objectsToDelete);
596
+ }
429
597
  };
430
598
 
431
599
  public handleRowAction = (actionId: string, rowIndex: number): void => {
@@ -433,8 +601,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
433
601
  return;
434
602
  }
435
603
 
436
- const row = this._sorting.getDataIndex(rowIndex);
437
- const data = this._rows.value[row];
604
+ const sortedRows = this._registry.get(this._sortedRows);
605
+ const data = sortedRows[rowIndex];
438
606
 
439
607
  if (data) {
440
608
  this._onRowAction(actionId, data);
@@ -443,7 +611,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
443
611
 
444
612
  public getCellData = (cell: DxGridPosition): any => {
445
613
  const { col, row, plane } = cell;
446
- const fields = this._projection?.fields ?? [];
614
+ const fields = this._projection?.getFields() ?? [];
447
615
  if (col < 0 || col >= fields.length) {
448
616
  return undefined;
449
617
  }
@@ -453,15 +621,15 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
453
621
 
454
622
  if (plane === 'frozenRowsEnd') {
455
623
  // Get data from draft row
456
- if (row >= 0 && row < this._draftRows.value.length) {
457
- value = getValue(this._draftRows.value[row].data, field.path);
624
+ if (row >= 0 && row < this._registry.get(this._draftRows).length) {
625
+ value = getValue(this._registry.get(this._draftRows)[row].data, field.path);
458
626
  } else {
459
627
  return undefined;
460
628
  }
461
629
  } else {
462
- // Get data from regular row
463
- const dataIndex = this._sorting.getDataIndex(row);
464
- value = getValue(this._rows.value[dataIndex], field.path);
630
+ // Get data from regular row (use sorted rows for display index)
631
+ const sortedRows = this._registry.get(this._sortedRows);
632
+ value = getValue(sortedRows[row], field.path);
465
633
  }
466
634
 
467
635
  if (value == null) {
@@ -470,7 +638,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
470
638
 
471
639
  const { props } = this._projection.getFieldProjection(field.id);
472
640
  switch (props.format) {
473
- case FormatEnum.Ref: {
641
+ case Format.TypeFormat.Ref: {
474
642
  if (!field.referencePath) {
475
643
  return ''; // TODO(burdon): Show error.
476
644
  }
@@ -479,13 +647,17 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
479
647
  }
480
648
 
481
649
  default: {
482
- return formatForEditing({ type: props.type, format: props.format, value });
650
+ return formatForEditing({
651
+ type: props.type,
652
+ format: props.format,
653
+ value,
654
+ });
483
655
  }
484
656
  }
485
657
  };
486
658
 
487
659
  public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
488
- const fields = this._projection?.fields ?? [];
660
+ const fields = this._projection?.getFields() ?? [];
489
661
  if (col < 0 || col >= fields.length) {
490
662
  return { valid: false, error: 'Invalid column index' };
491
663
  }
@@ -497,12 +669,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
497
669
  const isDraftRow = plane === 'frozenRowsEnd';
498
670
 
499
671
  if (isDraftRow) {
500
- const isInvalidDraftRowIndex = row < 0 || row >= this._draftRows.value.length;
672
+ const isInvalidDraftRowIndex = row < 0 || row >= this._registry.get(this._draftRows).length;
501
673
  if (isInvalidDraftRowIndex) {
502
674
  return { valid: false, error: 'Invalid draft row index' };
503
675
  }
504
676
 
505
- const draftRow = this._draftRows.value[row];
677
+ const draftRow = this._registry.get(this._draftRows)[row];
506
678
  const snapshot = { ...draftRow.data };
507
679
  setValue(snapshot, field.path, transformedValue);
508
680
 
@@ -516,14 +688,13 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
516
688
  }
517
689
  return { valid: true };
518
690
  } else {
519
- const rowIdx = this._sorting.getDataIndex(row);
520
- const currentRow = this._rows.value[rowIdx];
691
+ const currentRow = this._registry.get(this._rows)[row];
521
692
  invariant(currentRow, 'Invalid row index');
522
693
 
523
694
  const snapshot = getSnapshot(currentRow);
524
695
  setValue(snapshot, field.path, transformedValue);
525
696
 
526
- const schema = getSchema(currentRow);
697
+ const schema = Obj.getSchema(currentRow);
527
698
  invariant(schema);
528
699
 
529
700
  const validationResult = validateSchema(schema, snapshot);
@@ -539,7 +710,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
539
710
 
540
711
  public setCellData = (cell: DxGridPosition, value: any): void => {
541
712
  const { col, row, plane } = cell;
542
- const fields = this._projection?.fields ?? [];
713
+ const fields = this._projection?.getFields() ?? [];
543
714
  if (col < 0 || col >= fields.length) {
544
715
  return;
545
716
  }
@@ -549,21 +720,24 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
549
720
  const transformedValue = editorTextToCellValue(props, value);
550
721
 
551
722
  // Special handling for Ref format to preserve existing behavior
552
- if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
723
+ if (props.format === Format.TypeFormat.Ref && !Obj.isObject(value)) {
553
724
  return;
554
725
  }
555
726
 
556
727
  if (plane === 'frozenRowsEnd') {
557
- // Update draft row data
558
- if (row >= 0 && row < this._draftRows.value.length) {
559
- setValue(this._draftRows.value[row].data, field.path, transformedValue);
728
+ // Update draft row data (draft rows are plain objects, not ECHO objects).
729
+ if (row >= 0 && row < this._registry.get(this._draftRows).length) {
730
+ setValue(this._registry.get(this._draftRows)[row].data, field.path, transformedValue);
560
731
  // Re-validate the draft row after the update
561
732
  this.validateDraftRow(row);
562
733
  }
563
734
  } else {
564
- // Update regular row data
565
- const rowIdx = this._sorting.getDataIndex(row);
566
- setValue(this._rows.value[rowIdx], field.path, transformedValue);
735
+ // Update regular row data (use sorted rows for display index)
736
+ const sortedRows = this._registry.get(this._sortedRows);
737
+ const rowData = sortedRows[row];
738
+ this._change.row(rowData, (mutableRow) => {
739
+ setValue(mutableRow, field.path, transformedValue);
740
+ });
567
741
  }
568
742
  };
569
743
 
@@ -573,13 +747,16 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
573
747
  * @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
574
748
  */
575
749
  public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
576
- const dataRow = this._sorting.getDataIndex(row);
577
- const fields = this._projection?.fields ?? [];
750
+ const fields = this._projection?.getFields() ?? [];
578
751
  const field = fields[col];
752
+ const sortedRows = this._registry.get(this._sortedRows);
753
+ const rowData = sortedRows[row];
579
754
 
580
- const value = getValue(this._rows.value[dataRow], field.path);
755
+ const value = getValue(rowData, field.path);
581
756
  const updatedValue = update(value);
582
- setValue(this._rows.value[dataRow], field.path, updatedValue);
757
+ this._change.row(rowData, (mutableRow) => {
758
+ setValue(mutableRow, field.path, updatedValue);
759
+ });
583
760
  }
584
761
 
585
762
  /**
@@ -618,7 +795,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
618
795
  return;
619
796
  }
620
797
 
621
- const field = this._projection?.fields.find((field) => field.id === fieldId);
798
+ const field = this._projection?.getFields().find((field) => field.id === fieldId);
622
799
  if (field && this._onDeleteColumn) {
623
800
  this._onDeleteColumn(field.id);
624
801
  }
@@ -629,12 +806,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
629
806
  //
630
807
 
631
808
  public setColumnWidth(columnIndex: number, width: number): void {
632
- const fields = this._projection?.fields ?? [];
809
+ const fields = this._projection?.getFields() ?? [];
633
810
  if (columnIndex < fields.length) {
634
811
  const newWidth = Math.max(0, width);
635
812
  const field = fields[columnIndex];
636
813
  if (field) {
637
- this.table.sizes[field.path] = newWidth;
814
+ this._change.table((mutableTable) => {
815
+ mutableTable.sizes[field.path] = newWidth;
816
+ });
638
817
  }
639
818
  }
640
819
  }
@@ -654,25 +833,112 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
654
833
  }
655
834
 
656
835
  //
657
- // View operations
836
+ // Sort operations
658
837
  //
659
838
 
839
+ /**
840
+ * Sets the sort order for a field in-memory (local to this instance).
841
+ * Use saveView() to persist the sort to the view query.
842
+ */
843
+ public setSort(fieldId: string, direction: QueryAST.OrderDirection): void {
844
+ const field = this._projection.getFields().find((f) => f.id === fieldId);
845
+ if (!field) {
846
+ return;
847
+ }
848
+
849
+ // Update in-memory sort (local changes).
850
+ this._registry.set(this._inMemorySort, {
851
+ fieldId,
852
+ direction,
853
+ });
854
+ }
855
+
856
+ /**
857
+ * Toggles the sort direction for a field in-memory.
858
+ */
859
+ public toggleSort(fieldId: string): void {
860
+ const currentSort = this.getSorting();
861
+ if (!currentSort || currentSort.fieldId !== fieldId) {
862
+ this.setSort(fieldId, 'asc');
863
+ return;
864
+ }
865
+
866
+ const newDirection = currentSort.direction === 'asc' ? 'desc' : 'asc';
867
+ this.setSort(fieldId, newDirection);
868
+ }
869
+
870
+ /**
871
+ * Clears the in-memory sort order.
872
+ */
873
+ public clearSort(): void {
874
+ this._registry.set(this._inMemorySort, undefined);
875
+ }
876
+
877
+ /**
878
+ * Saves the current in-memory sort to the view query AST.
879
+ * This persists the sort so it becomes the default for all viewers.
880
+ */
660
881
  public saveView(): void {
661
- this._sorting.save();
882
+ const view = this.view;
883
+ const inMemorySort = this._registry.get(this._inMemorySort);
884
+
885
+ // Snapshot view before accessing query.ast
886
+ const viewSnapshot = Obj.getSnapshot(view);
887
+ const currentQuery = Query.fromAst(viewSnapshot.query.ast);
888
+ const baseQuery = this._getBaseQuery(currentQuery);
889
+
890
+ if (inMemorySort) {
891
+ // Find the field to get its path
892
+ const field = this._projection.getFields().find((f) => f.id === inMemorySort.fieldId);
893
+ if (field) {
894
+ // Persist sort to view.query.ast
895
+ const newQuery = baseQuery.orderBy(Order.property<any>(field.path as string, inMemorySort.direction));
896
+ Obj.change(view, (v) => {
897
+ v.query.ast = newQuery.ast as Mutable<typeof newQuery.ast>;
898
+ });
899
+ }
900
+ } else {
901
+ // Clear sort from view.query.ast
902
+ Obj.change(view, (v) => {
903
+ v.query.ast = baseQuery.ast as Mutable<typeof baseQuery.ast>;
904
+ });
905
+ }
906
+
907
+ // Clear in-memory sort since it's now persisted.
908
+ this._registry.set(this._inMemorySort, undefined);
909
+ }
910
+
911
+ /**
912
+ * Extracts the base query without order clauses.
913
+ */
914
+ private _getBaseQuery(query: Query.Any): Query.Any {
915
+ const ast = query.ast;
916
+ // If the query has an order clause, extract the inner query
917
+ if (ast.type === 'order') {
918
+ return Query.fromAst(ast.query);
919
+ }
920
+ return query;
921
+ }
922
+
923
+ /**
924
+ * Checks if the view has unsaved changes (in-memory sort differs from persisted sort).
925
+ */
926
+ public getViewDirty(): boolean {
927
+ return this._registry.get(this._viewDirty);
662
928
  }
663
929
  }
664
930
 
665
931
  const editorTextToCellValue = (props: PropertyType, value: any): any => {
666
932
  switch (props.format) {
667
- case FormatEnum.Ref: {
668
- if (isLiveObject(value)) {
933
+ case Format.TypeFormat.Ref: {
934
+ if (Obj.isObject(value)) {
669
935
  return Ref.make(value);
670
936
  } else {
671
937
  return value;
672
938
  }
673
939
  }
674
940
 
675
- case FormatEnum.SingleSelect: {
941
+ case Format.TypeFormat.SingleSelect: {
676
942
  const ids = extractTagIds(value);
677
943
  if (ids && ids.length > 0) {
678
944
  return ids[0];
@@ -681,7 +947,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
681
947
  }
682
948
  }
683
949
 
684
- case FormatEnum.MultiSelect: {
950
+ case Format.TypeFormat.MultiSelect: {
685
951
  const ids = extractTagIds(value);
686
952
  return ids || value;
687
953
  }