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