@dxos/react-ui-table 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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