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