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