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