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