@dxos/react-ui-table 0.8.4-main.70d3990 → 0.8.4-main.74a063c4e0

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