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