@dxos/react-ui-table 0.8.4-main.5ea62a8 → 0.8.4-main.60689f5b1c

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