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