@dxos/react-ui-table 0.8.4-main.ead640a → 0.8.4-main.effb148878

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