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