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