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