@dxos/react-ui-table 0.8.4-main.7ace549 → 0.8.4-main.8360d9e660
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.
- package/dist/lib/browser/{chunk-5XWSB3JP.mjs → chunk-BF54SSNL.mjs} +27 -28
- package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
- package/dist/lib/browser/{chunk-KNC7XYT6.mjs → chunk-PTHMYAAM.mjs} +620 -652
- package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1154 -1036
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +20 -19
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-CWSGS3Y2.mjs → chunk-GI72MQSG.mjs} +620 -651
- package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-MKTMOY46.mjs → chunk-SIRDIPHZ.mjs} +27 -28
- package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1154 -1036
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +20 -19
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +2 -16
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +6 -3
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +11 -11
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +6 -3
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +2 -2
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +3 -3
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +22 -13
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +5 -6
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +2 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +6 -5
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +5 -1
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +17 -8
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +100 -18
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +3 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts +2 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +5 -8
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +13 -15
- package/dist/types/src/types/Table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +12 -10
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
- package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts +7 -0
- package/dist/types/src/util/query-ast.d.ts.map +1 -0
- package/dist/types/src/util/sort.d.ts +12 -0
- package/dist/types/src/util/sort.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +54 -55
- package/src/components/Table/ColumnActionsMenu.tsx +10 -6
- package/src/components/Table/ColumnSettings.tsx +8 -9
- package/src/components/Table/DynamicTable.stories.tsx +11 -11
- package/src/components/Table/DynamicTable.tsx +30 -30
- package/src/components/Table/Relations.stories.tsx +34 -31
- package/src/components/Table/RowActionsMenu.tsx +3 -2
- package/src/components/Table/Table.stories.tsx +80 -67
- package/src/components/Table/Table.tsx +324 -299
- package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
- package/src/components/TableCellEditor/FormCellEditor.tsx +118 -135
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +10 -7
- package/src/components/TableCellEditor/TableCellEditor.tsx +27 -27
- package/src/components/TableToolbar/TableToolbar.tsx +24 -18
- package/src/components/testing.ts +37 -11
- package/src/hooks/useAddRow.ts +8 -10
- package/src/hooks/useProjectionModel.ts +29 -9
- package/src/hooks/useTableModel.ts +27 -23
- package/src/model/modal-controller.ts +23 -12
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +81 -53
- package/src/model/table-model.ts +410 -138
- package/src/model/table-presentation.test.ts +58 -48
- package/src/model/table-presentation.ts +34 -28
- package/src/playwright/TableManager.ts +12 -5
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +48 -4
- package/src/testing/useTestTableModel.ts +24 -25
- package/src/translations.ts +1 -1
- package/src/types/Table.ts +28 -38
- package/src/util/dynamic-table.test.ts +70 -0
- package/src/util/dynamic-table.ts +36 -19
- package/src/util/index.ts +2 -0
- package/src/util/query-ast.ts +22 -0
- package/src/util/sort.ts +90 -0
- package/src/util/table-buttons.ts +1 -1
- package/src/util/table-controls.ts +2 -2
- package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +0 -7
- package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +0 -7
- package/dist/types/src/model/table-sorting.d.ts +0 -56
- package/dist/types/src/model/table-sorting.d.ts.map +0 -1
- package/src/model/table-sorting.ts +0 -234
package/src/model/table-model.ts
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom-react';
|
|
6
6
|
|
|
7
7
|
import { Resource } from '@dxos/context';
|
|
8
|
-
import { Format, Obj, Ref } from '@dxos/echo';
|
|
9
|
-
import { type
|
|
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';
|
|
10
13
|
import { invariant } from '@dxos/invariant';
|
|
11
14
|
import { ObjectId } from '@dxos/keys';
|
|
12
|
-
import { getSnapshot, isLiveObject } from '@dxos/live-object';
|
|
13
15
|
import { type Label } from '@dxos/react-ui';
|
|
14
16
|
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
15
17
|
import {
|
|
@@ -18,21 +20,57 @@ import {
|
|
|
18
20
|
type DxGridPlaneRange,
|
|
19
21
|
type DxGridPosition,
|
|
20
22
|
} from '@dxos/react-ui-grid';
|
|
21
|
-
import {
|
|
22
|
-
type FieldSortType,
|
|
23
|
-
type ProjectionModel,
|
|
24
|
-
type PropertyType,
|
|
25
|
-
type ValidationError,
|
|
26
|
-
type View,
|
|
27
|
-
validateSchema,
|
|
28
|
-
} from '@dxos/schema';
|
|
23
|
+
import { type ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
|
|
29
24
|
|
|
30
25
|
import { type Table } from '../types';
|
|
31
|
-
import {
|
|
26
|
+
import { extractOrder } from '../util';
|
|
27
|
+
import { compareValues } from '../util/sort';
|
|
32
28
|
import { extractTagIds } from '../util/tag';
|
|
33
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Field sort configuration.
|
|
32
|
+
*/
|
|
33
|
+
export type FieldSortType = {
|
|
34
|
+
fieldId: string;
|
|
35
|
+
direction: QueryAST.OrderDirection;
|
|
36
|
+
};
|
|
37
|
+
|
|
34
38
|
import { type SelectionMode, SelectionModel } from './selection-model';
|
|
35
|
-
|
|
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, (mutableTable) => mutate(mutableTable as unknown as Table.Table)),
|
|
57
|
+
row: (row, mutate) => {
|
|
58
|
+
if (Obj.isObject(row)) {
|
|
59
|
+
Obj.change(row, (mutableRow) => mutate(mutableRow 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
|
+
});
|
|
36
74
|
|
|
37
75
|
// Domain types for cell classification
|
|
38
76
|
export type TableCellType = 'standard' | 'draft' | 'header';
|
|
@@ -69,67 +107,89 @@ const defaultFeatures: TableFeatures = {
|
|
|
69
107
|
export type InsertRowResult = 'draft' | 'final';
|
|
70
108
|
|
|
71
109
|
export type TableModelProps<T extends TableRow = TableRow> = {
|
|
110
|
+
registry: Registry.Registry;
|
|
72
111
|
object: Table.Table;
|
|
73
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>;
|
|
74
119
|
features?: Partial<TableFeatures>;
|
|
75
|
-
sorting?: FieldSortType[];
|
|
76
120
|
initialSelection?: string[];
|
|
77
121
|
pinnedRows?: { top: number[]; bottom: number[] };
|
|
78
122
|
rowActions?: TableRowAction[];
|
|
79
123
|
onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
|
|
80
124
|
onInsertRow?: (data?: any) => InsertRowResult;
|
|
81
125
|
onDeleteRows?: (index: number, obj: T[]) => void;
|
|
82
|
-
|
|
126
|
+
onColumnDelete?: (fieldId: string) => void;
|
|
83
127
|
onCellUpdate?: (cell: DxGridPosition) => void;
|
|
84
128
|
onRowOrderChange?: () => void;
|
|
85
129
|
onRowAction?: (actionId: string, data: T) => void;
|
|
86
130
|
};
|
|
87
131
|
|
|
88
132
|
export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
133
|
+
private readonly _registry: Registry.Registry;
|
|
89
134
|
private readonly _object: Table.Table;
|
|
90
135
|
private readonly _projection: ProjectionModel;
|
|
136
|
+
private readonly _db?: Database.Database;
|
|
137
|
+
private readonly _change: TableChangeCallback<T>;
|
|
91
138
|
|
|
92
|
-
private readonly _visibleRange
|
|
93
|
-
start: { row: 0, col: 0 },
|
|
94
|
-
end: { row: 0, col: 0 },
|
|
95
|
-
});
|
|
139
|
+
private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
|
|
96
140
|
|
|
97
141
|
private readonly _onInsertRow?: (data?: any) => InsertRowResult;
|
|
98
142
|
private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
|
|
99
|
-
private readonly
|
|
143
|
+
private readonly _onColumnDelete?: TableModelProps<T>['onColumnDelete'];
|
|
100
144
|
private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
|
|
101
145
|
private readonly _onRowOrderChange?: TableModelProps<T>['onRowOrderChange'];
|
|
102
146
|
private readonly _onRowAction?: TableModelProps<T>['onRowAction'];
|
|
103
147
|
private readonly _rowActions: TableRowAction[];
|
|
104
148
|
private readonly _features: TableFeatures;
|
|
105
149
|
|
|
106
|
-
private readonly _rows
|
|
107
|
-
private readonly _draftRows
|
|
108
|
-
|
|
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>;
|
|
109
162
|
|
|
110
163
|
private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
|
|
111
164
|
private _selection!: SelectionModel<T>;
|
|
112
|
-
private _columnMeta?:
|
|
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>;
|
|
113
168
|
|
|
114
169
|
constructor({
|
|
170
|
+
registry,
|
|
115
171
|
object,
|
|
116
172
|
projection,
|
|
173
|
+
db,
|
|
174
|
+
change,
|
|
117
175
|
features = {},
|
|
118
|
-
sorting = [],
|
|
119
176
|
initialSelection = [],
|
|
120
177
|
pinnedRows = { top: [], bottom: [] },
|
|
121
178
|
rowActions = [],
|
|
122
179
|
onCellUpdate,
|
|
123
|
-
|
|
180
|
+
onColumnDelete,
|
|
124
181
|
onDeleteRows,
|
|
125
182
|
onInsertRow,
|
|
126
183
|
onRowOrderChange,
|
|
127
184
|
onRowAction,
|
|
128
185
|
}: TableModelProps<T>) {
|
|
129
186
|
super();
|
|
187
|
+
this._registry = registry;
|
|
130
188
|
this._object = object;
|
|
131
189
|
this._projection = projection;
|
|
132
190
|
this._projection.normalizeView();
|
|
191
|
+
this._db = db;
|
|
192
|
+
this._change = change ?? createEchoChangeCallback<T>(object);
|
|
133
193
|
|
|
134
194
|
// TODO(ZaymonFC): Use our more robust config merging module?
|
|
135
195
|
this._features = { ...defaultFeatures, ...features };
|
|
@@ -139,29 +199,101 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
139
199
|
'Single selection is not compatible with editable tables.',
|
|
140
200
|
);
|
|
141
201
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
202
|
+
// Initialize writable atoms.
|
|
203
|
+
this._visibleRange = Atom.make<DxGridPlaneRange>({
|
|
204
|
+
start: { row: 0, col: 0 },
|
|
205
|
+
end: { row: 0, col: 0 },
|
|
146
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
|
+
}
|
|
147
233
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
});
|
|
152
268
|
|
|
153
269
|
this._selection = new SelectionModel(
|
|
154
|
-
this.
|
|
270
|
+
this._registry,
|
|
271
|
+
this._sortedRows,
|
|
155
272
|
this._features.selection.mode ?? 'multiple',
|
|
156
273
|
initialSelection,
|
|
157
274
|
() => this._onRowOrderChange?.(),
|
|
158
275
|
);
|
|
159
276
|
|
|
277
|
+
// Create derived atom for viewDirty.
|
|
278
|
+
this._viewDirty = Atom.make((get) => {
|
|
279
|
+
const inMemorySort = get(this._inMemorySort);
|
|
280
|
+
if (!inMemorySort) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const persisted = get(this._persistedSort);
|
|
285
|
+
if (!persisted) {
|
|
286
|
+
return true; // In-memory sort but no persisted sort.
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return inMemorySort.fieldId !== persisted.fieldId || inMemorySort.direction !== persisted.direction;
|
|
290
|
+
});
|
|
291
|
+
|
|
160
292
|
this._pinnedRows = pinnedRows;
|
|
161
293
|
this._rowActions = rowActions;
|
|
162
294
|
this._onInsertRow = onInsertRow;
|
|
163
295
|
this._onDeleteRows = onDeleteRows;
|
|
164
|
-
this.
|
|
296
|
+
this._onColumnDelete = onColumnDelete;
|
|
165
297
|
this._onCellUpdate = onCellUpdate;
|
|
166
298
|
this._onRowOrderChange = onRowOrderChange;
|
|
167
299
|
this._onRowAction = onRowAction;
|
|
@@ -185,8 +317,36 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
185
317
|
return this._projection;
|
|
186
318
|
}
|
|
187
319
|
|
|
188
|
-
public get
|
|
189
|
-
return this.
|
|
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);
|
|
190
350
|
}
|
|
191
351
|
|
|
192
352
|
public get features(): TableFeatures {
|
|
@@ -194,28 +354,30 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
194
354
|
}
|
|
195
355
|
|
|
196
356
|
/**
|
|
197
|
-
*
|
|
357
|
+
* Gets the current sort state.
|
|
358
|
+
* Priority: in-memory sort (local changes) > persisted sort (from query AST).
|
|
198
359
|
*/
|
|
199
|
-
public
|
|
200
|
-
return this._sorting;
|
|
360
|
+
public getSorting(): FieldSortType | undefined {
|
|
361
|
+
return this._registry.get(this._sorting);
|
|
201
362
|
}
|
|
202
363
|
|
|
203
364
|
/**
|
|
204
365
|
* Gets the row data at the specified display index.
|
|
205
366
|
*/
|
|
206
367
|
public getRowAt(displayIndex: number): T | undefined {
|
|
207
|
-
|
|
368
|
+
const sortedRows = this._registry.get(this._sortedRows);
|
|
369
|
+
if (displayIndex < 0 || displayIndex >= sortedRows.length) {
|
|
208
370
|
return undefined;
|
|
209
371
|
}
|
|
210
372
|
|
|
211
|
-
return
|
|
373
|
+
return sortedRows[displayIndex];
|
|
212
374
|
}
|
|
213
375
|
|
|
214
376
|
public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
|
|
215
377
|
return this._pinnedRows;
|
|
216
378
|
}
|
|
217
379
|
|
|
218
|
-
public get columnMeta():
|
|
380
|
+
public get columnMeta(): Atom.Atom<DxGridAxisMeta> {
|
|
219
381
|
invariant(this._columnMeta);
|
|
220
382
|
return this._columnMeta;
|
|
221
383
|
}
|
|
@@ -224,11 +386,6 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
224
386
|
return this._selection;
|
|
225
387
|
}
|
|
226
388
|
|
|
227
|
-
/** @reactive */
|
|
228
|
-
public get isViewDirty(): boolean {
|
|
229
|
-
return this.sorting.isDirty;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
389
|
public get rowActions(): TableRowAction[] {
|
|
233
390
|
return this._rowActions;
|
|
234
391
|
}
|
|
@@ -245,8 +402,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
245
402
|
}
|
|
246
403
|
|
|
247
404
|
private initializeColumnMeta(): void {
|
|
248
|
-
this.
|
|
249
|
-
|
|
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);
|
|
250
411
|
const meta = Object.fromEntries(
|
|
251
412
|
fields.map((field, index: number) => [index, { size: this.table.sizes[field.path] ?? 256, resizeable: true }]),
|
|
252
413
|
);
|
|
@@ -260,40 +421,51 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
260
421
|
}
|
|
261
422
|
|
|
262
423
|
private initializeEffects(): void {
|
|
263
|
-
|
|
264
|
-
|
|
424
|
+
// Subscribe to rows changes to notify row order changed.
|
|
425
|
+
const rowsUnsubscribe = this._registry.subscribe(this._rows, () => {
|
|
265
426
|
this._onRowOrderChange?.();
|
|
266
427
|
});
|
|
267
|
-
this._ctx.onDispose(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
rowEffects.push(
|
|
279
|
-
effect(() => {
|
|
280
|
-
const obj = this.sorting.sortedRows.value[row];
|
|
281
|
-
this._projection?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
282
|
-
this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
|
|
283
|
-
}),
|
|
284
|
-
);
|
|
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);
|
|
285
439
|
}
|
|
286
440
|
|
|
287
|
-
|
|
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
|
+
}
|
|
288
462
|
});
|
|
289
|
-
this._ctx.onDispose(rowEffectManager);
|
|
290
463
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// Notify grid that draft rows have changed
|
|
464
|
+
// Subscribe to draft rows changes to notify grid.
|
|
465
|
+
const draftRowsUnsubscribe = this._registry.subscribe(this._draftRows, () => {
|
|
294
466
|
this._onRowOrderChange?.();
|
|
295
467
|
});
|
|
296
|
-
this._ctx.onDispose(
|
|
468
|
+
this._ctx.onDispose(draftRowsUnsubscribe);
|
|
297
469
|
}
|
|
298
470
|
|
|
299
471
|
//
|
|
@@ -301,21 +473,25 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
301
473
|
//
|
|
302
474
|
|
|
303
475
|
public setRows = (obj: T[]): void => {
|
|
304
|
-
this._rows
|
|
476
|
+
this._registry.set(this._rows, obj);
|
|
305
477
|
};
|
|
306
478
|
|
|
307
|
-
public getRowCount = (): number => this.
|
|
479
|
+
public getRowCount = (): number => this._registry.get(this._sortedRows).length;
|
|
308
480
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
public getDraftRowCount = (): number => this._draftRows.value.length;
|
|
481
|
+
public getDraftRowCount(): number {
|
|
482
|
+
return this._registry.get(this._draftRows).length;
|
|
483
|
+
}
|
|
313
484
|
|
|
314
|
-
|
|
315
|
-
public get draftRows():
|
|
485
|
+
/** Atom for reactive access to draft rows. */
|
|
486
|
+
public get draftRows(): Atom.Atom<DraftRow<T>[]> {
|
|
316
487
|
return this._draftRows;
|
|
317
488
|
}
|
|
318
489
|
|
|
490
|
+
/** Gets the current draft rows. */
|
|
491
|
+
public getDraftRows(): DraftRow<T>[] {
|
|
492
|
+
return this._registry.get(this._draftRows);
|
|
493
|
+
}
|
|
494
|
+
|
|
319
495
|
/**
|
|
320
496
|
* Checks if a specific field path has validation errors for a given draft row.
|
|
321
497
|
* @param draftRowIndex - The index of the draft row to check
|
|
@@ -323,20 +499,20 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
323
499
|
* @returns true if the field has validation errors, false otherwise
|
|
324
500
|
*/
|
|
325
501
|
public hasDraftRowValidationError(draftRowIndex: number, fieldPath: string): boolean {
|
|
326
|
-
if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.
|
|
502
|
+
if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
|
|
327
503
|
return false;
|
|
328
504
|
}
|
|
329
505
|
|
|
330
|
-
const draftRow = this._draftRows
|
|
506
|
+
const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
|
|
331
507
|
const validationErrors = draftRow.validationErrors || [];
|
|
332
508
|
return validationErrors.some((error) => error.path === fieldPath);
|
|
333
509
|
}
|
|
334
510
|
|
|
335
|
-
public getColumnCount = (): number => this._projection?.
|
|
511
|
+
public getColumnCount = (): number => this._projection?.getFields().length ?? 0;
|
|
336
512
|
|
|
337
513
|
public insertRow = (): InsertRowResult => {
|
|
338
514
|
const result = this._onInsertRow?.();
|
|
339
|
-
if (result === 'draft' && this._draftRows.
|
|
515
|
+
if (result === 'draft' && this._registry.get(this._draftRows).length === 0) {
|
|
340
516
|
this.createDraftRow();
|
|
341
517
|
}
|
|
342
518
|
return result ?? 'final';
|
|
@@ -357,15 +533,15 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
357
533
|
validationErrors,
|
|
358
534
|
};
|
|
359
535
|
|
|
360
|
-
this._draftRows
|
|
536
|
+
this._registry.set(this._draftRows, [...this._registry.get(this._draftRows), draftRow]);
|
|
361
537
|
}
|
|
362
538
|
|
|
363
539
|
private validateDraftRow(draftRowIndex: number): void {
|
|
364
|
-
if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.
|
|
540
|
+
if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
|
|
365
541
|
return;
|
|
366
542
|
}
|
|
367
543
|
|
|
368
|
-
const draftRow = this._draftRows
|
|
544
|
+
const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
|
|
369
545
|
const validationErrors = this.validateDraftRowData(draftRow.data);
|
|
370
546
|
|
|
371
547
|
const updatedDraftRow = {
|
|
@@ -374,9 +550,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
374
550
|
validationErrors,
|
|
375
551
|
};
|
|
376
552
|
|
|
377
|
-
const newDraftRows = [...this._draftRows
|
|
553
|
+
const newDraftRows = [...this._registry.get(this._draftRows)];
|
|
378
554
|
newDraftRows[draftRowIndex] = updatedDraftRow;
|
|
379
|
-
this._draftRows
|
|
555
|
+
this._registry.set(this._draftRows, newDraftRows);
|
|
380
556
|
}
|
|
381
557
|
|
|
382
558
|
private validateDraftRowData(data: T): ValidationError[] {
|
|
@@ -385,11 +561,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
385
561
|
}
|
|
386
562
|
|
|
387
563
|
public commitDraftRow = (draftRowIndex: number): boolean => {
|
|
388
|
-
if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.
|
|
564
|
+
if (draftRowIndex < 0 || draftRowIndex >= this._registry.get(this._draftRows).length) {
|
|
389
565
|
return false;
|
|
390
566
|
}
|
|
391
567
|
|
|
392
|
-
const draftRow = this._draftRows
|
|
568
|
+
const draftRow = this._registry.get(this._draftRows)[draftRowIndex];
|
|
393
569
|
if (!draftRow.valid) {
|
|
394
570
|
return false;
|
|
395
571
|
}
|
|
@@ -397,26 +573,28 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
397
573
|
const insertRowResult = this._onInsertRow?.(draftRow.data);
|
|
398
574
|
|
|
399
575
|
if (insertRowResult === 'final') {
|
|
400
|
-
const newDraftRows = this._draftRows.
|
|
401
|
-
this._draftRows
|
|
576
|
+
const newDraftRows = this._registry.get(this._draftRows).filter((_, index) => index !== draftRowIndex);
|
|
577
|
+
this._registry.set(this._draftRows, newDraftRows);
|
|
402
578
|
}
|
|
403
579
|
|
|
404
580
|
return insertRowResult === 'final';
|
|
405
581
|
};
|
|
406
582
|
|
|
407
583
|
public deleteRow = (rowIndex: number): void => {
|
|
408
|
-
const
|
|
409
|
-
const obj =
|
|
584
|
+
const sortedRows = this._registry.get(this._sortedRows);
|
|
585
|
+
const obj = sortedRows[rowIndex];
|
|
410
586
|
const objectsToDelete = [];
|
|
411
587
|
|
|
412
|
-
if (this.features.selection.enabled && this._selection.hasSelection
|
|
588
|
+
if (this.features.selection.enabled && this._selection.hasSelection) {
|
|
413
589
|
const selectedRows = this._selection.getSelectedRows();
|
|
414
590
|
objectsToDelete.push(...selectedRows);
|
|
415
|
-
} else {
|
|
591
|
+
} else if (obj) {
|
|
416
592
|
objectsToDelete.push(obj);
|
|
417
593
|
}
|
|
418
594
|
|
|
419
|
-
|
|
595
|
+
if (objectsToDelete.length > 0) {
|
|
596
|
+
this._onDeleteRows?.(rowIndex, objectsToDelete);
|
|
597
|
+
}
|
|
420
598
|
};
|
|
421
599
|
|
|
422
600
|
public handleRowAction = (actionId: string, rowIndex: number): void => {
|
|
@@ -424,8 +602,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
424
602
|
return;
|
|
425
603
|
}
|
|
426
604
|
|
|
427
|
-
const
|
|
428
|
-
const data =
|
|
605
|
+
const sortedRows = this._registry.get(this._sortedRows);
|
|
606
|
+
const data = sortedRows[rowIndex];
|
|
429
607
|
|
|
430
608
|
if (data) {
|
|
431
609
|
this._onRowAction(actionId, data);
|
|
@@ -434,7 +612,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
434
612
|
|
|
435
613
|
public getCellData = (cell: DxGridPosition): any => {
|
|
436
614
|
const { col, row, plane } = cell;
|
|
437
|
-
const fields = this._projection?.
|
|
615
|
+
const fields = this._projection?.getFields() ?? [];
|
|
438
616
|
if (col < 0 || col >= fields.length) {
|
|
439
617
|
return undefined;
|
|
440
618
|
}
|
|
@@ -444,15 +622,15 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
444
622
|
|
|
445
623
|
if (plane === 'frozenRowsEnd') {
|
|
446
624
|
// Get data from draft row
|
|
447
|
-
if (row >= 0 && row < this._draftRows.
|
|
448
|
-
value = getValue(this._draftRows
|
|
625
|
+
if (row >= 0 && row < this._registry.get(this._draftRows).length) {
|
|
626
|
+
value = getValue(this._registry.get(this._draftRows)[row].data, field.path);
|
|
449
627
|
} else {
|
|
450
628
|
return undefined;
|
|
451
629
|
}
|
|
452
630
|
} else {
|
|
453
|
-
// Get data from regular row
|
|
454
|
-
const
|
|
455
|
-
value = getValue(
|
|
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);
|
|
456
634
|
}
|
|
457
635
|
|
|
458
636
|
if (value == null) {
|
|
@@ -480,7 +658,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
480
658
|
};
|
|
481
659
|
|
|
482
660
|
public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
|
|
483
|
-
const fields = this._projection?.
|
|
661
|
+
const fields = this._projection?.getFields() ?? [];
|
|
484
662
|
if (col < 0 || col >= fields.length) {
|
|
485
663
|
return { valid: false, error: 'Invalid column index' };
|
|
486
664
|
}
|
|
@@ -492,12 +670,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
492
670
|
const isDraftRow = plane === 'frozenRowsEnd';
|
|
493
671
|
|
|
494
672
|
if (isDraftRow) {
|
|
495
|
-
const isInvalidDraftRowIndex = row < 0 || row >= this._draftRows.
|
|
673
|
+
const isInvalidDraftRowIndex = row < 0 || row >= this._registry.get(this._draftRows).length;
|
|
496
674
|
if (isInvalidDraftRowIndex) {
|
|
497
675
|
return { valid: false, error: 'Invalid draft row index' };
|
|
498
676
|
}
|
|
499
677
|
|
|
500
|
-
const draftRow = this._draftRows
|
|
678
|
+
const draftRow = this._registry.get(this._draftRows)[row];
|
|
501
679
|
const snapshot = { ...draftRow.data };
|
|
502
680
|
setValue(snapshot, field.path, transformedValue);
|
|
503
681
|
|
|
@@ -511,11 +689,10 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
511
689
|
}
|
|
512
690
|
return { valid: true };
|
|
513
691
|
} else {
|
|
514
|
-
const
|
|
515
|
-
const currentRow = this._rows.value[rowIdx];
|
|
692
|
+
const currentRow = this._registry.get(this._rows)[row];
|
|
516
693
|
invariant(currentRow, 'Invalid row index');
|
|
517
694
|
|
|
518
|
-
const snapshot = getSnapshot(currentRow);
|
|
695
|
+
const snapshot = { ...getSnapshot(currentRow) };
|
|
519
696
|
setValue(snapshot, field.path, transformedValue);
|
|
520
697
|
|
|
521
698
|
const schema = Obj.getSchema(currentRow);
|
|
@@ -534,7 +711,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
534
711
|
|
|
535
712
|
public setCellData = (cell: DxGridPosition, value: any): void => {
|
|
536
713
|
const { col, row, plane } = cell;
|
|
537
|
-
const fields = this._projection?.
|
|
714
|
+
const fields = this._projection?.getFields() ?? [];
|
|
538
715
|
if (col < 0 || col >= fields.length) {
|
|
539
716
|
return;
|
|
540
717
|
}
|
|
@@ -544,21 +721,24 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
544
721
|
const transformedValue = editorTextToCellValue(props, value);
|
|
545
722
|
|
|
546
723
|
// Special handling for Ref format to preserve existing behavior
|
|
547
|
-
if (props.format === Format.TypeFormat.Ref && !
|
|
724
|
+
if (props.format === Format.TypeFormat.Ref && !Obj.isObject(value)) {
|
|
548
725
|
return;
|
|
549
726
|
}
|
|
550
727
|
|
|
551
728
|
if (plane === 'frozenRowsEnd') {
|
|
552
|
-
// Update draft row data
|
|
553
|
-
if (row >= 0 && row < this._draftRows.
|
|
554
|
-
setValue(this._draftRows
|
|
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);
|
|
555
732
|
// Re-validate the draft row after the update
|
|
556
733
|
this.validateDraftRow(row);
|
|
557
734
|
}
|
|
558
735
|
} else {
|
|
559
|
-
// Update regular row data
|
|
560
|
-
const
|
|
561
|
-
|
|
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
|
+
});
|
|
562
742
|
}
|
|
563
743
|
};
|
|
564
744
|
|
|
@@ -568,13 +748,16 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
568
748
|
* @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
|
|
569
749
|
*/
|
|
570
750
|
public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
|
|
571
|
-
const
|
|
572
|
-
const fields = this._projection?.fields ?? [];
|
|
751
|
+
const fields = this._projection?.getFields() ?? [];
|
|
573
752
|
const field = fields[col];
|
|
753
|
+
const sortedRows = this._registry.get(this._sortedRows);
|
|
754
|
+
const rowData = sortedRows[row];
|
|
574
755
|
|
|
575
|
-
const value = getValue(
|
|
756
|
+
const value = getValue(rowData, field.path);
|
|
576
757
|
const updatedValue = update(value);
|
|
577
|
-
|
|
758
|
+
this._change.row(rowData, (mutableRow) => {
|
|
759
|
+
setValue(mutableRow, field.path, updatedValue);
|
|
760
|
+
});
|
|
578
761
|
}
|
|
579
762
|
|
|
580
763
|
/**
|
|
@@ -613,9 +796,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
613
796
|
return;
|
|
614
797
|
}
|
|
615
798
|
|
|
616
|
-
const field = this._projection?.
|
|
617
|
-
if (field && this.
|
|
618
|
-
this.
|
|
799
|
+
const field = this._projection?.getFields().find((field) => field.id === fieldId);
|
|
800
|
+
if (field && this._onColumnDelete) {
|
|
801
|
+
this._onColumnDelete(field.id);
|
|
619
802
|
}
|
|
620
803
|
}
|
|
621
804
|
|
|
@@ -624,12 +807,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
624
807
|
//
|
|
625
808
|
|
|
626
809
|
public setColumnWidth(columnIndex: number, width: number): void {
|
|
627
|
-
const fields = this._projection?.
|
|
810
|
+
const fields = this._projection?.getFields() ?? [];
|
|
628
811
|
if (columnIndex < fields.length) {
|
|
629
812
|
const newWidth = Math.max(0, width);
|
|
630
813
|
const field = fields[columnIndex];
|
|
631
814
|
if (field) {
|
|
632
|
-
this.table
|
|
815
|
+
this._change.table((mutableTable) => {
|
|
816
|
+
mutableTable.sizes[field.path] = newWidth;
|
|
817
|
+
});
|
|
633
818
|
}
|
|
634
819
|
}
|
|
635
820
|
}
|
|
@@ -649,18 +834,105 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
649
834
|
}
|
|
650
835
|
|
|
651
836
|
//
|
|
652
|
-
//
|
|
837
|
+
// Sort operations
|
|
653
838
|
//
|
|
654
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
|
+
*/
|
|
655
882
|
public saveView(): void {
|
|
656
|
-
this.
|
|
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, (v) => {
|
|
898
|
+
v.query.ast = newQuery.ast as Mutable<typeof newQuery.ast>;
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
// Clear sort from view.query.ast
|
|
903
|
+
Obj.change(view, (v) => {
|
|
904
|
+
v.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);
|
|
657
929
|
}
|
|
658
930
|
}
|
|
659
931
|
|
|
660
932
|
const editorTextToCellValue = (props: PropertyType, value: any): any => {
|
|
661
933
|
switch (props.format) {
|
|
662
934
|
case Format.TypeFormat.Ref: {
|
|
663
|
-
if (
|
|
935
|
+
if (Obj.isObject(value)) {
|
|
664
936
|
return Ref.make(value);
|
|
665
937
|
} else {
|
|
666
938
|
return value;
|