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