@dxos/react-ui-table 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5

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