@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae

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 (158) hide show
  1. package/dist/lib/browser/chunk-DSR2MRZB.mjs +80 -0
  2. package/dist/lib/browser/chunk-DSR2MRZB.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-PTHMYAAM.mjs +1751 -0
  4. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1190 -1139
  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 +31 -38
  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-GI72MQSG.mjs +1753 -0
  12. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-HTZJSULI.mjs +81 -0
  14. package/dist/lib/node-esm/chunk-HTZJSULI.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1190 -1139
  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 +31 -38
  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 +2 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -3
  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 +37 -3
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +17 -13
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +108 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +11 -18
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  46. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +5 -5
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +23 -16
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +10 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -8
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +112 -30
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +5 -2
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +4 -4
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +31 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  75. package/dist/types/src/types/index.d.ts +1 -1
  76. package/dist/types/src/types/index.d.ts.map +1 -1
  77. package/dist/types/src/util/dynamic-table.d.ts +14 -12
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  80. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  81. package/dist/types/src/util/index.d.ts +2 -1
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/query-ast.d.ts +7 -0
  84. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  85. package/dist/types/src/util/schema.d.ts +1 -1
  86. package/dist/types/src/util/schema.d.ts.map +1 -1
  87. package/dist/types/src/util/sort.d.ts +12 -0
  88. package/dist/types/src/util/sort.d.ts.map +1 -0
  89. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  90. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +61 -56
  93. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  94. package/src/components/Table/ColumnSettings.tsx +9 -10
  95. package/src/components/Table/DynamicTable.stories.tsx +28 -24
  96. package/src/components/Table/DynamicTable.tsx +33 -27
  97. package/src/components/Table/Relations.stories.tsx +240 -0
  98. package/src/components/Table/RowActionsMenu.tsx +26 -17
  99. package/src/components/Table/Table.stories.tsx +146 -116
  100. package/src/components/Table/Table.tsx +353 -297
  101. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -6
  102. package/src/components/TableCellEditor/FormCellEditor.tsx +134 -40
  103. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +31 -66
  104. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -238
  105. package/src/components/TableCellEditor/index.ts +1 -0
  106. package/src/components/TableToolbar/TableToolbar.tsx +54 -38
  107. package/src/components/testing.ts +39 -13
  108. package/src/hooks/index.ts +2 -1
  109. package/src/hooks/useAddRow.ts +14 -14
  110. package/src/hooks/useProjectionModel.ts +39 -0
  111. package/src/hooks/useTableModel.ts +36 -34
  112. package/src/model/modal-controller.ts +24 -41
  113. package/src/model/selection-model.ts +75 -43
  114. package/src/model/table-model.test.ts +92 -55
  115. package/src/model/table-model.ts +453 -175
  116. package/src/model/table-presentation.test.ts +70 -58
  117. package/src/model/table-presentation.ts +111 -64
  118. package/src/playwright/TableManager.ts +13 -6
  119. package/src/playwright/playwright.config.ts +1 -1
  120. package/src/playwright/smoke.spec.ts +171 -33
  121. package/src/testing/useTestTableModel.ts +46 -35
  122. package/src/translations.ts +3 -3
  123. package/src/types/Table.ts +95 -0
  124. package/src/types/index.ts +1 -1
  125. package/src/util/dynamic-table.test.ts +70 -0
  126. package/src/util/dynamic-table.ts +46 -33
  127. package/src/util/index.ts +2 -1
  128. package/src/util/query-ast.ts +22 -0
  129. package/src/util/schema.ts +4 -4
  130. package/src/util/sort.ts +90 -0
  131. package/src/util/table-buttons.ts +4 -3
  132. package/src/util/table-controls.ts +6 -3
  133. package/dist/lib/browser/chunk-5XLSHLOG.mjs +0 -1751
  134. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  136. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  138. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  139. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +0 -1752
  140. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  141. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  142. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  143. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  144. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  145. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  146. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  147. package/dist/types/src/model/table-sorting.d.ts +0 -53
  148. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  149. package/dist/types/src/types/schema.d.ts +0 -8
  150. package/dist/types/src/types/schema.d.ts.map +0 -1
  151. package/dist/types/src/util/table.d.ts +0 -11
  152. package/dist/types/src/util/table.d.ts.map +0 -1
  153. package/src/components/Table/CreateRefPanel.tsx +0 -86
  154. package/src/components/Table/relations.stories.tsx +0 -116
  155. package/src/components/TableCellEditor/extension.ts +0 -61
  156. package/src/model/table-sorting.ts +0 -216
  157. package/src/types/schema.ts +0 -16
  158. package/src/util/table.ts +0 -49
@@ -2,40 +2,76 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } 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
- type FieldSortType,
11
- FormatEnum,
12
- getValue,
13
- setValue,
14
- type JsonProp,
15
- getSnapshot,
16
- getSchema,
17
- toEffectSchema,
18
- type JsonSchemaType,
19
- } from '@dxos/echo-schema';
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';
20
13
  import { invariant } from '@dxos/invariant';
21
14
  import { ObjectId } from '@dxos/keys';
22
- import { isLiveObject } from '@dxos/live-object';
23
- import { fullyQualifiedId } from '@dxos/react-client/echo';
15
+ import { type Label } from '@dxos/react-ui';
24
16
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
25
17
  import {
26
18
  type DxGridAxisMeta,
27
- type DxGridPlaneRange,
28
19
  type DxGridPlanePosition,
20
+ type DxGridPlaneRange,
29
21
  type DxGridPosition,
30
22
  } from '@dxos/react-ui-grid';
31
- import { type DataType, ProjectionModel, type PropertyType, validateSchema, type ValidationError } from '@dxos/schema';
23
+ import { type ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
32
24
 
33
- import { type SelectionMode, SelectionModel } from './selection-model';
34
- import { TableSorting } from './table-sorting';
35
- import { TableView } from '../types';
36
- import { touch } from '../util';
25
+ import { type Table } from '../types';
26
+ import { extractOrder } from '../util';
27
+ import { compareValues } from '../util/sort';
37
28
  import { extractTagIds } from '../util/tag';
38
29
 
30
+ /**
31
+ * Field sort configuration.
32
+ */
33
+ export type FieldSortType = {
34
+ fieldId: string;
35
+ direction: QueryAST.OrderDirection;
36
+ };
37
+
38
+ import { type SelectionMode, SelectionModel } from './selection-model';
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
+ });
74
+
39
75
  // Domain types for cell classification
40
76
  export type TableCellType = 'standard' | 'draft' | 'header';
41
77
 
@@ -53,7 +89,7 @@ export type TableRow = Record<JsonProp, any> & { id: string };
53
89
 
54
90
  export type TableRowAction = {
55
91
  id: string;
56
- translationKey: string;
92
+ label: Label;
57
93
  };
58
94
 
59
95
  export type TableFeatures = {
@@ -68,70 +104,92 @@ const defaultFeatures: TableFeatures = {
68
104
  schemaEditable: false,
69
105
  };
70
106
 
107
+ export type InsertRowResult = 'draft' | 'final';
108
+
71
109
  export type TableModelProps<T extends TableRow = TableRow> = {
72
- view: DataType.View;
73
- schema: JsonSchemaType;
74
- projection?: ProjectionModel;
110
+ registry: Registry.Registry;
111
+ object: Table.Table;
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>;
75
119
  features?: Partial<TableFeatures>;
76
- sorting?: FieldSortType[];
77
120
  initialSelection?: string[];
78
121
  pinnedRows?: { top: number[]; bottom: number[] };
79
122
  rowActions?: TableRowAction[];
80
123
  onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
81
- onInsertRow?: (data?: any) => boolean;
124
+ onInsertRow?: (data?: any) => InsertRowResult;
82
125
  onDeleteRows?: (index: number, obj: T[]) => void;
83
- onDeleteColumn?: (fieldId: string) => void;
126
+ onColumnDelete?: (fieldId: string) => void;
84
127
  onCellUpdate?: (cell: DxGridPosition) => void;
85
128
  onRowOrderChange?: () => void;
86
129
  onRowAction?: (actionId: string, data: T) => void;
87
130
  };
88
131
 
89
132
  export class TableModel<T extends TableRow = TableRow> extends Resource {
90
- private readonly _view: DataType.View;
133
+ private readonly _registry: Registry.Registry;
134
+ private readonly _object: Table.Table;
91
135
  private readonly _projection: ProjectionModel;
92
- private _table?: TableView;
136
+ private readonly _db?: Database.Database;
137
+ private readonly _change: TableChangeCallback<T>;
93
138
 
94
- private readonly _visibleRange = signal<DxGridPlaneRange>({
95
- start: { row: 0, col: 0 },
96
- end: { row: 0, col: 0 },
97
- });
139
+ private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
98
140
 
99
- private readonly _onInsertRow?: (data?: any) => boolean;
141
+ private readonly _onInsertRow?: (data?: any) => InsertRowResult;
100
142
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
101
- private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
143
+ private readonly _onColumnDelete?: TableModelProps<T>['onColumnDelete'];
102
144
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
103
145
  private readonly _onRowOrderChange?: TableModelProps<T>['onRowOrderChange'];
104
146
  private readonly _onRowAction?: TableModelProps<T>['onRowAction'];
105
147
  private readonly _rowActions: TableRowAction[];
106
148
  private readonly _features: TableFeatures;
107
149
 
108
- private readonly _rows = signal<T[]>([]);
109
- private readonly _draftRows = signal<DraftRow<T>[]>([]);
110
- 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>;
111
162
 
112
163
  private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
113
164
  private _selection!: SelectionModel<T>;
114
- 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>;
115
168
 
116
169
  constructor({
117
- view,
118
- schema,
170
+ registry,
171
+ object,
119
172
  projection,
173
+ db,
174
+ change,
120
175
  features = {},
121
- sorting = [],
122
176
  initialSelection = [],
123
177
  pinnedRows = { top: [], bottom: [] },
124
178
  rowActions = [],
125
179
  onCellUpdate,
126
- onDeleteColumn,
180
+ onColumnDelete,
127
181
  onDeleteRows,
128
182
  onInsertRow,
129
183
  onRowOrderChange,
130
184
  onRowAction,
131
185
  }: TableModelProps<T>) {
132
186
  super();
133
- this._view = view;
134
- this._projection = projection ?? new ProjectionModel(schema, view.projection);
187
+ this._registry = registry;
188
+ this._object = object;
189
+ this._projection = projection;
190
+ this._projection.normalizeView();
191
+ this._db = db;
192
+ this._change = change ?? createEchoChangeCallback<T>(object);
135
193
 
136
194
  // TODO(ZaymonFC): Use our more robust config merging module?
137
195
  this._features = { ...defaultFeatures, ...features };
@@ -141,49 +199,154 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
141
199
  'Single selection is not compatible with editable tables.',
142
200
  );
143
201
 
144
- this._sorting = new TableSorting(this._rows, this._view, this._projection);
202
+ // Initialize writable atoms.
203
+ this._visibleRange = Atom.make<DxGridPlaneRange>({
204
+ start: { row: 0, col: 0 },
205
+ end: { row: 0, col: 0 },
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
+ }
145
233
 
146
- if (sorting.length > 0) {
147
- const [sort] = sorting;
148
- this._sorting.setSort(sort.fieldId, sort.direction);
149
- }
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
+ });
150
268
 
151
269
  this._selection = new SelectionModel(
152
- this._sorting.sortedRows,
270
+ this._registry,
271
+ this._sortedRows,
153
272
  this._features.selection.mode ?? 'multiple',
154
273
  initialSelection,
155
274
  () => this._onRowOrderChange?.(),
156
275
  );
157
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
+
158
292
  this._pinnedRows = pinnedRows;
159
293
  this._rowActions = rowActions;
160
294
  this._onInsertRow = onInsertRow;
161
295
  this._onDeleteRows = onDeleteRows;
162
- this._onDeleteColumn = onDeleteColumn;
296
+ this._onColumnDelete = onColumnDelete;
163
297
  this._onCellUpdate = onCellUpdate;
164
298
  this._onRowOrderChange = onRowOrderChange;
165
299
  this._onRowAction = onRowAction;
166
300
  }
167
301
 
168
302
  public get id(): string {
169
- return fullyQualifiedId(this._view);
303
+ return Obj.getDXN(this._object).toString();
170
304
  }
171
305
 
172
- public get view(): DataType.View {
173
- return this._view;
306
+ public get view(): View.View {
307
+ const view = this._object.view.target;
308
+ invariant(view, 'Table model not initialized');
309
+ return view;
174
310
  }
175
311
 
176
- public get table(): TableView {
177
- invariant(this._table, 'Model not initialized');
178
- return this._table;
312
+ public get table(): Table.Table {
313
+ return this._object;
179
314
  }
180
315
 
181
316
  public get projection(): ProjectionModel {
182
317
  return this._projection;
183
318
  }
184
319
 
185
- public get rows(): ReadonlySignal<T[]> {
186
- 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);
187
350
  }
188
351
 
189
352
  public get features(): TableFeatures {
@@ -191,28 +354,30 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
191
354
  }
192
355
 
193
356
  /**
194
- * @reactive
357
+ * Gets the current sort state.
358
+ * Priority: in-memory sort (local changes) > persisted sort (from query AST).
195
359
  */
196
- public get sorting(): TableSorting<T> {
197
- return this._sorting;
360
+ public getSorting(): FieldSortType | undefined {
361
+ return this._registry.get(this._sorting);
198
362
  }
199
363
 
200
364
  /**
201
365
  * Gets the row data at the specified display index.
202
366
  */
203
367
  public getRowAt(displayIndex: number): T | undefined {
204
- if (displayIndex < 0 || displayIndex >= this.sorting.sortedRows.value.length) {
368
+ const sortedRows = this._registry.get(this._sortedRows);
369
+ if (displayIndex < 0 || displayIndex >= sortedRows.length) {
205
370
  return undefined;
206
371
  }
207
372
 
208
- return this.sorting.sortedRows.value[displayIndex];
373
+ return sortedRows[displayIndex];
209
374
  }
210
375
 
211
376
  public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
212
377
  return this._pinnedRows;
213
378
  }
214
379
 
215
- public get columnMeta(): ReadonlySignal<DxGridAxisMeta> {
380
+ public get columnMeta(): Atom.Atom<DxGridAxisMeta> {
216
381
  invariant(this._columnMeta);
217
382
  return this._columnMeta;
218
383
  }
@@ -221,11 +386,6 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
221
386
  return this._selection;
222
387
  }
223
388
 
224
- /** @reactive */
225
- public get isViewDirty(): boolean {
226
- return this.sorting.isDirty;
227
- }
228
-
229
389
  public get rowActions(): TableRowAction[] {
230
390
  return this._rowActions;
231
391
  }
@@ -235,18 +395,19 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
235
395
  //
236
396
 
237
397
  protected override async _open(): Promise<void> {
238
- const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
239
- invariant(Obj.instanceOf(TableView, presentation));
240
- this._table = presentation;
241
-
398
+ await this._object.view.load();
242
399
  this.initializeColumnMeta();
243
400
  this.initializeEffects();
244
401
  await this._selection.open(this._ctx);
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
  );
@@ -254,46 +415,57 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
254
415
  return {
255
416
  grid: meta,
256
417
  frozenColsStart: { 0: { size: 30, resizeable: false } },
257
- frozenColsEnd: { 0: { size: 40, resizeable: false } },
418
+ frozenColsEnd: { 0: { size: 32, resizeable: false } },
258
419
  };
259
420
  });
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,22 +499,23 @@ 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
- public insertRow = (): void => {
513
+ public insertRow = (): InsertRowResult => {
338
514
  const result = this._onInsertRow?.();
339
- if (result === false && this._draftRows.value.length === 0) {
515
+ if (result === 'draft' && this._registry.get(this._draftRows).length === 0) {
340
516
  this.createDraftRow();
341
517
  }
518
+ return result ?? 'final';
342
519
  };
343
520
 
344
521
  private createDraftRow(): void {
@@ -356,15 +533,15 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
356
533
  validationErrors,
357
534
  };
358
535
 
359
- this._draftRows.value = [...this._draftRows.value, draftRow];
536
+ this._registry.set(this._draftRows, [...this._registry.get(this._draftRows), draftRow]);
360
537
  }
361
538
 
362
539
  private validateDraftRow(draftRowIndex: number): void {
363
- if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
540
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
364
541
  return;
365
542
  }
366
543
 
367
- const draftRow = this._draftRows.value[draftRowIndex];
544
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
368
545
  const validationErrors = this.validateDraftRowData(draftRow.data);
369
546
 
370
547
  const updatedDraftRow = {
@@ -373,9 +550,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
373
550
  validationErrors,
374
551
  };
375
552
 
376
- const newDraftRows = [...this._draftRows.value];
553
+ const newDraftRows = [...this._registry.get(this._draftRows)];
377
554
  newDraftRows[draftRowIndex] = updatedDraftRow;
378
- this._draftRows.value = newDraftRows;
555
+ this._registry.set(this._draftRows, newDraftRows);
379
556
  }
380
557
 
381
558
  private validateDraftRowData(data: T): ValidationError[] {
@@ -384,38 +561,40 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
384
561
  }
385
562
 
386
563
  public commitDraftRow = (draftRowIndex: number): boolean => {
387
- if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
564
+ if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
388
565
  return false;
389
566
  }
390
567
 
391
- const draftRow = this._draftRows.value[draftRowIndex];
568
+ const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
392
569
  if (!draftRow.valid) {
393
570
  return false;
394
571
  }
395
572
 
396
- const success = this._onInsertRow?.(draftRow.data);
573
+ const insertRowResult = this._onInsertRow?.(draftRow.data);
397
574
 
398
- if (success) {
399
- const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
400
- this._draftRows.value = newDraftRows;
575
+ if (insertRowResult === 'final') {
576
+ const newDraftRows = this._registry.get(this._draftRows).filter((_, index) => index !== draftRowIndex);
577
+ this._registry.set(this._draftRows, newDraftRows);
401
578
  }
402
579
 
403
- return success ?? false;
580
+ return insertRowResult === 'final';
404
581
  };
405
582
 
406
583
  public deleteRow = (rowIndex: number): void => {
407
- const row = this._sorting.getDataIndex(rowIndex);
408
- const obj = this._rows.value[row];
584
+ const sortedRows = this._registry.get(this._sortedRows);
585
+ const obj = sortedRows[rowIndex];
409
586
  const objectsToDelete = [];
410
587
 
411
- if (this.features.selection.enabled && this._selection.hasSelection.value) {
588
+ if (this.features.selection.enabled && this._selection.hasSelection) {
412
589
  const selectedRows = this._selection.getSelectedRows();
413
590
  objectsToDelete.push(...selectedRows);
414
- } else {
591
+ } else if (obj) {
415
592
  objectsToDelete.push(obj);
416
593
  }
417
594
 
418
- this._onDeleteRows?.(row, objectsToDelete);
595
+ if (objectsToDelete.length > 0) {
596
+ this._onDeleteRows?.(rowIndex, objectsToDelete);
597
+ }
419
598
  };
420
599
 
421
600
  public handleRowAction = (actionId: string, rowIndex: number): void => {
@@ -423,8 +602,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
423
602
  return;
424
603
  }
425
604
 
426
- const row = this._sorting.getDataIndex(rowIndex);
427
- const data = this._rows.value[row];
605
+ const sortedRows = this._registry.get(this._sortedRows);
606
+ const data = sortedRows[rowIndex];
428
607
 
429
608
  if (data) {
430
609
  this._onRowAction(actionId, data);
@@ -433,7 +612,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
433
612
 
434
613
  public getCellData = (cell: DxGridPosition): any => {
435
614
  const { col, row, plane } = cell;
436
- const fields = this._projection?.fields ?? [];
615
+ const fields = this._projection?.getFields() ?? [];
437
616
  if (col < 0 || col >= fields.length) {
438
617
  return undefined;
439
618
  }
@@ -443,15 +622,15 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
443
622
 
444
623
  if (plane === 'frozenRowsEnd') {
445
624
  // Get data from draft row
446
- if (row >= 0 && row < this._draftRows.value.length) {
447
- 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);
448
627
  } else {
449
628
  return undefined;
450
629
  }
451
630
  } else {
452
- // Get data from regular row
453
- const dataIndex = this._sorting.getDataIndex(row);
454
- 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);
455
634
  }
456
635
 
457
636
  if (value == null) {
@@ -460,7 +639,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
460
639
 
461
640
  const { props } = this._projection.getFieldProjection(field.id);
462
641
  switch (props.format) {
463
- case FormatEnum.Ref: {
642
+ case Format.TypeFormat.Ref: {
464
643
  if (!field.referencePath) {
465
644
  return ''; // TODO(burdon): Show error.
466
645
  }
@@ -469,13 +648,17 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
469
648
  }
470
649
 
471
650
  default: {
472
- return formatForEditing({ type: props.type, format: props.format, value });
651
+ return formatForEditing({
652
+ type: props.type,
653
+ format: props.format,
654
+ value,
655
+ });
473
656
  }
474
657
  }
475
658
  };
476
659
 
477
660
  public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
478
- const fields = this._projection?.fields ?? [];
661
+ const fields = this._projection?.getFields() ?? [];
479
662
  if (col < 0 || col >= fields.length) {
480
663
  return { valid: false, error: 'Invalid column index' };
481
664
  }
@@ -487,16 +670,17 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
487
670
  const isDraftRow = plane === 'frozenRowsEnd';
488
671
 
489
672
  if (isDraftRow) {
490
- const isInvalidDraftRowIndex = row < 0 || row >= this._draftRows.value.length;
673
+ const isInvalidDraftRowIndex = row < 0 || row >= this._registry.get(this._draftRows).length;
491
674
  if (isInvalidDraftRowIndex) {
492
675
  return { valid: false, error: 'Invalid draft row index' };
493
676
  }
494
677
 
495
- const draftRow = this._draftRows.value[row];
678
+ const draftRow = this._registry.get(this._draftRows)[row];
496
679
  const snapshot = { ...draftRow.data };
497
680
  setValue(snapshot, field.path, transformedValue);
498
681
 
499
682
  const validationErrors = this.validateDraftRowData(snapshot);
683
+ // TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
500
684
  if (validationErrors.length > 0) {
501
685
  const error = validationErrors.find((err) => err.path === field.path);
502
686
  if (error) {
@@ -505,14 +689,13 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
505
689
  }
506
690
  return { valid: true };
507
691
  } else {
508
- const rowIdx = this._sorting.getDataIndex(row);
509
- const currentRow = this._rows.value[rowIdx];
692
+ const currentRow = this._registry.get(this._rows)[row];
510
693
  invariant(currentRow, 'Invalid row index');
511
694
 
512
- const snapshot = getSnapshot(currentRow);
695
+ const snapshot = { ...getSnapshot(currentRow) };
513
696
  setValue(snapshot, field.path, transformedValue);
514
697
 
515
- const schema = getSchema(currentRow);
698
+ const schema = Obj.getSchema(currentRow);
516
699
  invariant(schema);
517
700
 
518
701
  const validationResult = validateSchema(schema, snapshot);
@@ -528,7 +711,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
528
711
 
529
712
  public setCellData = (cell: DxGridPosition, value: any): void => {
530
713
  const { col, row, plane } = cell;
531
- const fields = this._projection?.fields ?? [];
714
+ const fields = this._projection?.getFields() ?? [];
532
715
  if (col < 0 || col >= fields.length) {
533
716
  return;
534
717
  }
@@ -538,21 +721,24 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
538
721
  const transformedValue = editorTextToCellValue(props, value);
539
722
 
540
723
  // Special handling for Ref format to preserve existing behavior
541
- if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
724
+ if (props.format === Format.TypeFormat.Ref && !Obj.isObject(value)) {
542
725
  return;
543
726
  }
544
727
 
545
728
  if (plane === 'frozenRowsEnd') {
546
- // Update draft row data
547
- if (row >= 0 && row < this._draftRows.value.length) {
548
- 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);
549
732
  // Re-validate the draft row after the update
550
733
  this.validateDraftRow(row);
551
734
  }
552
735
  } else {
553
- // Update regular row data
554
- const rowIdx = this._sorting.getDataIndex(row);
555
- 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
+ });
556
742
  }
557
743
  };
558
744
 
@@ -562,13 +748,16 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
562
748
  * @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
563
749
  */
564
750
  public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
565
- const dataRow = this._sorting.getDataIndex(row);
566
- const fields = this._projection?.fields ?? [];
751
+ const fields = this._projection?.getFields() ?? [];
567
752
  const field = fields[col];
753
+ const sortedRows = this._registry.get(this._sortedRows);
754
+ const rowData = sortedRows[row];
568
755
 
569
- const value = getValue(this._rows.value[dataRow], field.path);
756
+ const value = getValue(rowData, field.path);
570
757
  const updatedValue = update(value);
571
- setValue(this._rows.value[dataRow], field.path, updatedValue);
758
+ this._change.row(rowData, (mutableRow) => {
759
+ setValue(mutableRow, field.path, updatedValue);
760
+ });
572
761
  }
573
762
 
574
763
  /**
@@ -607,9 +796,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
607
796
  return;
608
797
  }
609
798
 
610
- const field = this._projection?.fields.find((field) => field.id === fieldId);
611
- if (field && this._onDeleteColumn) {
612
- 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);
613
802
  }
614
803
  }
615
804
 
@@ -618,12 +807,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
618
807
  //
619
808
 
620
809
  public setColumnWidth(columnIndex: number, width: number): void {
621
- const fields = this._projection?.fields ?? [];
810
+ const fields = this._projection?.getFields() ?? [];
622
811
  if (columnIndex < fields.length) {
623
812
  const newWidth = Math.max(0, width);
624
813
  const field = fields[columnIndex];
625
814
  if (field) {
626
- this.table.sizes[field.path] = newWidth;
815
+ this._change.table((mutableTable) => {
816
+ mutableTable.sizes[field.path] = newWidth;
817
+ });
627
818
  }
628
819
  }
629
820
  }
@@ -643,25 +834,112 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
643
834
  }
644
835
 
645
836
  //
646
- // View operations
837
+ // Sort operations
647
838
  //
648
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
+ */
649
882
  public saveView(): void {
650
- 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, (v) => {
898
+ v.query.ast = newQuery.ast as Mutable<typeof newQuery.ast>;
899
+ });
900
+ }
901
+ } else {
902
+ // Clear sort from view.query.ast
903
+ Obj.change(view, (v) => {
904
+ v.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);
651
929
  }
652
930
  }
653
931
 
654
932
  const editorTextToCellValue = (props: PropertyType, value: any): any => {
655
933
  switch (props.format) {
656
- case FormatEnum.Ref: {
657
- if (isLiveObject(value)) {
934
+ case Format.TypeFormat.Ref: {
935
+ if (Obj.isObject(value)) {
658
936
  return Ref.make(value);
659
937
  } else {
660
938
  return value;
661
939
  }
662
940
  }
663
941
 
664
- case FormatEnum.SingleSelect: {
942
+ case Format.TypeFormat.SingleSelect: {
665
943
  const ids = extractTagIds(value);
666
944
  if (ids && ids.length > 0) {
667
945
  return ids[0];
@@ -670,7 +948,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
670
948
  }
671
949
  }
672
950
 
673
- case FormatEnum.MultiSelect: {
951
+ case Format.TypeFormat.MultiSelect: {
674
952
  const ids = extractTagIds(value);
675
953
  return ids || value;
676
954
  }