@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.69d29f4

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