@dxos/react-ui-table 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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