@dxos/react-ui-table 0.8.3-staging.0fa589b → 0.8.4-main.28f8d3d
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-L2LSB6KV.mjs +18 -0
- package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
- package/dist/lib/browser/chunk-ZAOSIG3L.mjs +1814 -0
- package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +728 -2104
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +123 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/browser/types/index.mjs +3 -5
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +20 -0
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +1815 -0
- package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +728 -2104
- 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 +124 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/types/index.mjs +3 -5
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
- 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/CreateRefPanel.d.ts +1 -1
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/{relations.stories.d.ts → Relations.stories.d.ts} +2 -2
- 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 +1 -0
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- 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 +2 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +3 -40
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +1 -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/useTableModel.d.ts +8 -7
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +2 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +58 -21
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +10 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/model/table-sorting.d.ts +4 -4
- package/dist/types/src/model/table-sorting.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 +41 -0
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +5 -6
- package/dist/types/src/translations.d.ts.map +1 -1
- 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/types/schema.d.ts +8 -0
- package/dist/types/src/types/schema.d.ts.map +1 -0
- package/dist/types/src/util/dynamic-table.d.ts +3 -4
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts +15 -1
- package/dist/types/src/util/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/src/util/table.d.ts +9 -11
- package/dist/types/src/util/table.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +52 -40
- package/src/components/Table/ColumnActionsMenu.tsx +1 -1
- package/src/components/Table/ColumnSettings.tsx +15 -11
- package/src/components/Table/CreateRefPanel.tsx +6 -4
- package/src/components/Table/DynamicTable.stories.tsx +4 -3
- package/src/components/Table/DynamicTable.tsx +17 -10
- package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +37 -37
- package/src/components/Table/RowActionsMenu.tsx +3 -3
- package/src/components/Table/Table.stories.tsx +109 -148
- package/src/components/Table/Table.tsx +95 -66
- package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
- package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +27 -30
- package/src/components/TableCellEditor/TableCellEditor.tsx +88 -34
- package/src/components/TableCellEditor/extension.ts +2 -2
- package/src/components/TableToolbar/TableToolbar.tsx +2 -2
- package/src/components/testing.ts +7 -24
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAddRow.ts +36 -0
- package/src/hooks/useTableModel.ts +21 -17
- package/src/index.ts +1 -2
- package/src/model/modal-controller.ts +1 -1
- package/src/model/selection-model.ts +6 -3
- package/src/model/table-model.test.ts +8 -8
- package/src/model/table-model.ts +278 -86
- package/src/model/table-presentation.test.ts +8 -8
- package/src/model/table-presentation.ts +240 -135
- package/src/model/table-sorting.ts +11 -11
- package/src/playwright/playwright.config.ts +17 -0
- package/src/playwright/smoke.spec.ts +113 -20
- package/src/testing/index.ts +5 -0
- package/src/testing/useTestTableModel.ts +123 -0
- package/src/translations.ts +7 -5
- package/src/types/index.ts +2 -2
- package/src/types/schema.ts +16 -0
- package/src/util/dynamic-table.ts +20 -18
- package/src/util/schema.ts +1 -1
- package/src/util/table-buttons.ts +26 -2
- package/src/util/table-controls.ts +4 -1
- package/src/util/table.ts +37 -111
- 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/relations.stories.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/src/playwright/playwright.config.cts +0 -18
- package/src/types/table.ts +0 -27
package/src/model/table-model.ts
CHANGED
|
@@ -2,44 +2,60 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { computed, effect, signal
|
|
5
|
+
import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
|
|
6
6
|
|
|
7
|
-
import { type Space } from '@dxos/client/echo';
|
|
8
7
|
import { Resource } from '@dxos/context';
|
|
8
|
+
import { Obj, Ref } from '@dxos/echo';
|
|
9
9
|
import {
|
|
10
10
|
type FieldSortType,
|
|
11
11
|
FormatEnum,
|
|
12
|
-
getValue,
|
|
13
|
-
setValue,
|
|
14
12
|
type JsonProp,
|
|
15
|
-
|
|
13
|
+
type JsonSchemaType,
|
|
16
14
|
getSchema,
|
|
15
|
+
getSnapshot,
|
|
16
|
+
getValue,
|
|
17
|
+
setValue,
|
|
18
|
+
toEffectSchema,
|
|
17
19
|
} from '@dxos/echo-schema';
|
|
18
20
|
import { invariant } from '@dxos/invariant';
|
|
19
|
-
import {
|
|
21
|
+
import { ObjectId } from '@dxos/keys';
|
|
22
|
+
import { isLiveObject } from '@dxos/live-object';
|
|
23
|
+
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
24
|
+
import { type Label } from '@dxos/react-ui';
|
|
20
25
|
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
21
26
|
import {
|
|
22
27
|
type DxGridAxisMeta,
|
|
23
|
-
type DxGridPlaneRange,
|
|
24
28
|
type DxGridPlanePosition,
|
|
29
|
+
type DxGridPlaneRange,
|
|
25
30
|
type DxGridPosition,
|
|
26
31
|
} from '@dxos/react-ui-grid';
|
|
27
|
-
import { type
|
|
32
|
+
import { type DataType, ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
|
|
28
33
|
|
|
29
|
-
import {
|
|
30
|
-
import { TableSorting } from './table-sorting';
|
|
34
|
+
import { TableView } from '../types';
|
|
31
35
|
import { touch } from '../util';
|
|
32
36
|
import { extractTagIds } from '../util/tag';
|
|
33
37
|
|
|
38
|
+
import { type SelectionMode, SelectionModel } from './selection-model';
|
|
39
|
+
import { TableSorting } from './table-sorting';
|
|
40
|
+
|
|
41
|
+
// Domain types for cell classification
|
|
42
|
+
export type TableCellType = 'standard' | 'draft' | 'header';
|
|
43
|
+
|
|
34
44
|
// TODO(ZaymonFC): Use a common type?
|
|
35
45
|
export type ValidationResult = { valid: true } | { valid: false; error: string };
|
|
36
46
|
|
|
47
|
+
export type DraftRow<T extends TableRow = TableRow> = {
|
|
48
|
+
data: T;
|
|
49
|
+
valid: boolean;
|
|
50
|
+
validationErrors: ValidationError[];
|
|
51
|
+
};
|
|
52
|
+
|
|
37
53
|
// TODO(burdon): Use schema types.
|
|
38
54
|
export type TableRow = Record<JsonProp, any> & { id: string };
|
|
39
55
|
|
|
40
56
|
export type TableRowAction = {
|
|
41
57
|
id: string;
|
|
42
|
-
|
|
58
|
+
label: Label;
|
|
43
59
|
};
|
|
44
60
|
|
|
45
61
|
export type TableFeatures = {
|
|
@@ -55,15 +71,16 @@ const defaultFeatures: TableFeatures = {
|
|
|
55
71
|
};
|
|
56
72
|
|
|
57
73
|
export type TableModelProps<T extends TableRow = TableRow> = {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
projection: ViewProjection;
|
|
74
|
+
view: DataType.View;
|
|
75
|
+
schema: JsonSchemaType;
|
|
76
|
+
projection?: ProjectionModel;
|
|
62
77
|
features?: Partial<TableFeatures>;
|
|
63
78
|
sorting?: FieldSortType[];
|
|
79
|
+
initialSelection?: string[];
|
|
64
80
|
pinnedRows?: { top: number[]; bottom: number[] };
|
|
65
81
|
rowActions?: TableRowAction[];
|
|
66
|
-
|
|
82
|
+
onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
|
|
83
|
+
onInsertRow?: (data?: any) => boolean;
|
|
67
84
|
onDeleteRows?: (index: number, obj: T[]) => void;
|
|
68
85
|
onDeleteColumn?: (fieldId: string) => void;
|
|
69
86
|
onCellUpdate?: (cell: DxGridPosition) => void;
|
|
@@ -72,17 +89,16 @@ export type TableModelProps<T extends TableRow = TableRow> = {
|
|
|
72
89
|
};
|
|
73
90
|
|
|
74
91
|
export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
75
|
-
private readonly
|
|
76
|
-
private readonly
|
|
77
|
-
private
|
|
78
|
-
private readonly _projection: ViewProjection;
|
|
92
|
+
private readonly _view: DataType.View;
|
|
93
|
+
private readonly _projection: ProjectionModel;
|
|
94
|
+
private _table?: TableView;
|
|
79
95
|
|
|
80
96
|
private readonly _visibleRange = signal<DxGridPlaneRange>({
|
|
81
97
|
start: { row: 0, col: 0 },
|
|
82
98
|
end: { row: 0, col: 0 },
|
|
83
99
|
});
|
|
84
100
|
|
|
85
|
-
private readonly _onInsertRow?:
|
|
101
|
+
private readonly _onInsertRow?: (data?: any) => boolean;
|
|
86
102
|
private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
|
|
87
103
|
private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
|
|
88
104
|
private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
|
|
@@ -92,6 +108,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
92
108
|
private readonly _features: TableFeatures;
|
|
93
109
|
|
|
94
110
|
private readonly _rows = signal<T[]>([]);
|
|
111
|
+
private readonly _draftRows = signal<DraftRow<T>[]>([]);
|
|
95
112
|
private readonly _sorting: TableSorting<T>;
|
|
96
113
|
|
|
97
114
|
private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
|
|
@@ -99,12 +116,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
99
116
|
private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
|
|
100
117
|
|
|
101
118
|
constructor({
|
|
102
|
-
id,
|
|
103
|
-
space,
|
|
104
119
|
view,
|
|
120
|
+
schema,
|
|
105
121
|
projection,
|
|
106
122
|
features = {},
|
|
107
123
|
sorting = [],
|
|
124
|
+
initialSelection = [],
|
|
108
125
|
pinnedRows = { top: [], bottom: [] },
|
|
109
126
|
rowActions = [],
|
|
110
127
|
onCellUpdate,
|
|
@@ -115,10 +132,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
115
132
|
onRowAction,
|
|
116
133
|
}: TableModelProps<T>) {
|
|
117
134
|
super();
|
|
118
|
-
this._id = id;
|
|
119
|
-
this._space = space;
|
|
120
135
|
this._view = view;
|
|
121
|
-
this._projection = projection;
|
|
136
|
+
this._projection = projection ?? new ProjectionModel(schema, view.projection);
|
|
122
137
|
|
|
123
138
|
// TODO(ZaymonFC): Use our more robust config merging module?
|
|
124
139
|
this._features = { ...defaultFeatures, ...features };
|
|
@@ -128,13 +143,20 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
128
143
|
'Single selection is not compatible with editable tables.',
|
|
129
144
|
);
|
|
130
145
|
|
|
131
|
-
this._sorting = new TableSorting(this._rows, this._view,
|
|
146
|
+
this._sorting = new TableSorting(this._rows, this._view, this._projection);
|
|
132
147
|
|
|
133
148
|
if (sorting.length > 0) {
|
|
134
149
|
const [sort] = sorting;
|
|
135
150
|
this._sorting.setSort(sort.fieldId, sort.direction);
|
|
136
151
|
}
|
|
137
152
|
|
|
153
|
+
this._selection = new SelectionModel(
|
|
154
|
+
this._sorting.sortedRows,
|
|
155
|
+
this._features.selection.mode ?? 'multiple',
|
|
156
|
+
initialSelection,
|
|
157
|
+
() => this._onRowOrderChange?.(),
|
|
158
|
+
);
|
|
159
|
+
|
|
138
160
|
this._pinnedRows = pinnedRows;
|
|
139
161
|
this._rowActions = rowActions;
|
|
140
162
|
this._onInsertRow = onInsertRow;
|
|
@@ -145,24 +167,25 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
145
167
|
this._onRowAction = onRowAction;
|
|
146
168
|
}
|
|
147
169
|
|
|
148
|
-
public get id() {
|
|
149
|
-
return this.
|
|
170
|
+
public get id(): string {
|
|
171
|
+
return fullyQualifiedId(this._view);
|
|
150
172
|
}
|
|
151
173
|
|
|
152
|
-
public get
|
|
153
|
-
return this.
|
|
174
|
+
public get view(): DataType.View {
|
|
175
|
+
return this._view;
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
public get
|
|
157
|
-
|
|
178
|
+
public get table(): TableView {
|
|
179
|
+
invariant(this._table, 'Model not initialized');
|
|
180
|
+
return this._table;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
|
-
public get projection() {
|
|
183
|
+
public get projection(): ProjectionModel {
|
|
161
184
|
return this._projection;
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
public get rows(): ReadonlySignal<T[]> {
|
|
165
|
-
return this.
|
|
188
|
+
return this.sorting.sortedRows;
|
|
166
189
|
}
|
|
167
190
|
|
|
168
191
|
public get features(): TableFeatures {
|
|
@@ -172,7 +195,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
172
195
|
/**
|
|
173
196
|
* @reactive
|
|
174
197
|
*/
|
|
175
|
-
public get sorting(): TableSorting<T>
|
|
198
|
+
public get sorting(): TableSorting<T> {
|
|
176
199
|
return this._sorting;
|
|
177
200
|
}
|
|
178
201
|
|
|
@@ -180,11 +203,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
180
203
|
* Gets the row data at the specified display index.
|
|
181
204
|
*/
|
|
182
205
|
public getRowAt(displayIndex: number): T | undefined {
|
|
183
|
-
if (displayIndex < 0 || displayIndex >= this.
|
|
206
|
+
if (displayIndex < 0 || displayIndex >= this.sorting.sortedRows.value.length) {
|
|
184
207
|
return undefined;
|
|
185
208
|
}
|
|
186
209
|
|
|
187
|
-
return this.
|
|
210
|
+
return this.sorting.sortedRows.value[displayIndex];
|
|
188
211
|
}
|
|
189
212
|
|
|
190
213
|
public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
|
|
@@ -202,7 +225,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
202
225
|
|
|
203
226
|
/** @reactive */
|
|
204
227
|
public get isViewDirty(): boolean {
|
|
205
|
-
return this.
|
|
228
|
+
return this.sorting.isDirty;
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
public get rowActions(): TableRowAction[] {
|
|
@@ -214,19 +237,20 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
214
237
|
//
|
|
215
238
|
|
|
216
239
|
protected override async _open(): Promise<void> {
|
|
240
|
+
const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
|
|
241
|
+
invariant(Obj.instanceOf(TableView, presentation));
|
|
242
|
+
this._table = presentation;
|
|
243
|
+
|
|
217
244
|
this.initializeColumnMeta();
|
|
218
245
|
this.initializeEffects();
|
|
219
|
-
this._selection = new SelectionModel(this._sorting.sortedRows, this._features.selection.mode ?? 'multiple', () =>
|
|
220
|
-
this._onRowOrderChange?.(),
|
|
221
|
-
);
|
|
222
246
|
await this._selection.open(this._ctx);
|
|
223
247
|
}
|
|
224
248
|
|
|
225
249
|
private initializeColumnMeta(): void {
|
|
226
250
|
this._columnMeta = computed(() => {
|
|
227
|
-
const fields = this.
|
|
251
|
+
const fields = this._projection?.fields ?? [];
|
|
228
252
|
const meta = Object.fromEntries(
|
|
229
|
-
fields.map((field, index: number) => [index, { size: field
|
|
253
|
+
fields.map((field, index: number) => [index, { size: this.table.sizes[field.path] ?? 256, resizeable: true }]),
|
|
230
254
|
);
|
|
231
255
|
|
|
232
256
|
return {
|
|
@@ -239,7 +263,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
239
263
|
|
|
240
264
|
private initializeEffects(): void {
|
|
241
265
|
const rowOrderWatcher = effect(() => {
|
|
242
|
-
touch(this.
|
|
266
|
+
touch(this.sorting.sortedRows.value);
|
|
243
267
|
this._onRowOrderChange?.();
|
|
244
268
|
});
|
|
245
269
|
this._ctx.onDispose(rowOrderWatcher);
|
|
@@ -255,8 +279,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
255
279
|
for (let row = start.row; row <= end.row; row++) {
|
|
256
280
|
rowEffects.push(
|
|
257
281
|
effect(() => {
|
|
258
|
-
const obj = this.
|
|
259
|
-
this.
|
|
282
|
+
const obj = this.sorting.sortedRows.value[row];
|
|
283
|
+
this._projection?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
260
284
|
this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
|
|
261
285
|
}),
|
|
262
286
|
);
|
|
@@ -265,6 +289,13 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
265
289
|
return () => rowEffects.forEach((cleanup) => cleanup());
|
|
266
290
|
});
|
|
267
291
|
this._ctx.onDispose(rowEffectManager);
|
|
292
|
+
|
|
293
|
+
const draftRowsWatcher = effect(() => {
|
|
294
|
+
touch(this._draftRows.value);
|
|
295
|
+
// Notify grid that draft rows have changed
|
|
296
|
+
this._onRowOrderChange?.();
|
|
297
|
+
});
|
|
298
|
+
this._ctx.onDispose(draftRowsWatcher);
|
|
268
299
|
}
|
|
269
300
|
|
|
270
301
|
//
|
|
@@ -277,11 +308,101 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
277
308
|
|
|
278
309
|
public getRowCount = (): number => this._rows.value.length;
|
|
279
310
|
|
|
280
|
-
|
|
311
|
+
/**
|
|
312
|
+
* @reactive
|
|
313
|
+
*/
|
|
314
|
+
public getDraftRowCount = (): number => this._draftRows.value.length;
|
|
315
|
+
|
|
316
|
+
// TODO(ZaymonFC): Return .value instead of exposing the signal.
|
|
317
|
+
public get draftRows(): ReadonlySignal<DraftRow<T>[]> {
|
|
318
|
+
return this._draftRows;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Checks if a specific field path has validation errors for a given draft row.
|
|
323
|
+
* @param draftRowIndex - The index of the draft row to check
|
|
324
|
+
* @param fieldPath - The path of the field to check for validation errors
|
|
325
|
+
* @returns true if the field has validation errors, false otherwise
|
|
326
|
+
*/
|
|
327
|
+
public hasDraftRowValidationError(draftRowIndex: number, fieldPath: string): boolean {
|
|
328
|
+
if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const draftRow = this._draftRows.value[draftRowIndex];
|
|
333
|
+
const validationErrors = draftRow.validationErrors || [];
|
|
334
|
+
return validationErrors.some((error) => error.path === fieldPath);
|
|
335
|
+
}
|
|
281
336
|
|
|
282
|
-
public
|
|
283
|
-
|
|
284
|
-
|
|
337
|
+
public getColumnCount = (): number => this._projection?.fields.length ?? 0;
|
|
338
|
+
|
|
339
|
+
public insertRow = (): void => {
|
|
340
|
+
const result = this._onInsertRow?.();
|
|
341
|
+
if (result === false && this._draftRows.value.length === 0) {
|
|
342
|
+
this.createDraftRow();
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
private createDraftRow(): void {
|
|
347
|
+
if (!this._projection) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// NOTE(ZaymonFC): This is initialized with id because it's required in all schemata?
|
|
352
|
+
const draftData = { id: ObjectId.random() } as any as T;
|
|
353
|
+
const validationErrors = this.validateDraftRowData(draftData);
|
|
354
|
+
|
|
355
|
+
const draftRow: DraftRow<T> = {
|
|
356
|
+
data: draftData,
|
|
357
|
+
valid: validationErrors.length === 0,
|
|
358
|
+
validationErrors,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
this._draftRows.value = [...this._draftRows.value, draftRow];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private validateDraftRow(draftRowIndex: number): void {
|
|
365
|
+
if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const draftRow = this._draftRows.value[draftRowIndex];
|
|
370
|
+
const validationErrors = this.validateDraftRowData(draftRow.data);
|
|
371
|
+
|
|
372
|
+
const updatedDraftRow = {
|
|
373
|
+
...draftRow,
|
|
374
|
+
valid: validationErrors.length === 0,
|
|
375
|
+
validationErrors,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const newDraftRows = [...this._draftRows.value];
|
|
379
|
+
newDraftRows[draftRowIndex] = updatedDraftRow;
|
|
380
|
+
this._draftRows.value = newDraftRows;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private validateDraftRowData(data: T): ValidationError[] {
|
|
384
|
+
const schema = toEffectSchema(this._projection.baseSchema);
|
|
385
|
+
return validateSchema(schema, data) || [];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
public commitDraftRow = (draftRowIndex: number): boolean => {
|
|
389
|
+
if (draftRowIndex < 0 || draftRowIndex >= this._draftRows.value.length) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const draftRow = this._draftRows.value[draftRowIndex];
|
|
394
|
+
if (!draftRow.valid) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const success = this._onInsertRow?.(draftRow.data);
|
|
399
|
+
|
|
400
|
+
if (success) {
|
|
401
|
+
const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
|
|
402
|
+
this._draftRows.value = newDraftRows;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return success ?? false;
|
|
285
406
|
};
|
|
286
407
|
|
|
287
408
|
public deleteRow = (rowIndex: number): void => {
|
|
@@ -312,15 +433,29 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
312
433
|
}
|
|
313
434
|
};
|
|
314
435
|
|
|
315
|
-
public getCellData = (
|
|
316
|
-
const
|
|
436
|
+
public getCellData = (cell: DxGridPosition): any => {
|
|
437
|
+
const { col, row, plane } = cell;
|
|
438
|
+
const fields = this._projection?.fields ?? [];
|
|
317
439
|
if (col < 0 || col >= fields.length) {
|
|
318
440
|
return undefined;
|
|
319
441
|
}
|
|
320
442
|
|
|
321
443
|
const field = fields[col];
|
|
322
|
-
|
|
323
|
-
|
|
444
|
+
let value: any;
|
|
445
|
+
|
|
446
|
+
if (plane === 'frozenRowsEnd') {
|
|
447
|
+
// Get data from draft row
|
|
448
|
+
if (row >= 0 && row < this._draftRows.value.length) {
|
|
449
|
+
value = getValue(this._draftRows.value[row].data, field.path);
|
|
450
|
+
} else {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
// Get data from regular row
|
|
455
|
+
const dataIndex = this._sorting.getDataIndex(row);
|
|
456
|
+
value = getValue(this._rows.value[dataIndex], field.path);
|
|
457
|
+
}
|
|
458
|
+
|
|
324
459
|
if (value == null) {
|
|
325
460
|
return '';
|
|
326
461
|
}
|
|
@@ -341,42 +476,62 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
341
476
|
}
|
|
342
477
|
};
|
|
343
478
|
|
|
344
|
-
public validateCellData = async ({ col, row }:
|
|
345
|
-
const
|
|
346
|
-
const fields = this._view?.fields ?? [];
|
|
479
|
+
public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
|
|
480
|
+
const fields = this._projection?.fields ?? [];
|
|
347
481
|
if (col < 0 || col >= fields.length) {
|
|
348
482
|
return { valid: false, error: 'Invalid column index' };
|
|
349
483
|
}
|
|
350
484
|
|
|
351
485
|
const field = fields[col];
|
|
352
486
|
const { props } = this._projection.getFieldProjection(field.id);
|
|
487
|
+
const transformedValue = editorTextToCellValue(props, value);
|
|
353
488
|
|
|
354
|
-
const
|
|
355
|
-
invariant(currentRow, 'Invalid row index');
|
|
489
|
+
const isDraftRow = plane === 'frozenRowsEnd';
|
|
356
490
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
491
|
+
if (isDraftRow) {
|
|
492
|
+
const isInvalidDraftRowIndex = row < 0 || row >= this._draftRows.value.length;
|
|
493
|
+
if (isInvalidDraftRowIndex) {
|
|
494
|
+
return { valid: false, error: 'Invalid draft row index' };
|
|
495
|
+
}
|
|
360
496
|
|
|
361
|
-
|
|
362
|
-
|
|
497
|
+
const draftRow = this._draftRows.value[row];
|
|
498
|
+
const snapshot = { ...draftRow.data };
|
|
499
|
+
setValue(snapshot, field.path, transformedValue);
|
|
363
500
|
|
|
364
|
-
|
|
365
|
-
|
|
501
|
+
const validationErrors = this.validateDraftRowData(snapshot);
|
|
502
|
+
// TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
|
|
503
|
+
if (validationErrors.length > 0) {
|
|
504
|
+
const error = validationErrors.find((err) => err.path === field.path);
|
|
505
|
+
if (error) {
|
|
506
|
+
return { valid: false, error: error.message };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return { valid: true };
|
|
510
|
+
} else {
|
|
511
|
+
const rowIdx = this._sorting.getDataIndex(row);
|
|
512
|
+
const currentRow = this._rows.value[rowIdx];
|
|
513
|
+
invariant(currentRow, 'Invalid row index');
|
|
366
514
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const error = validationResult[0];
|
|
370
|
-
invariant(error.path === field.path);
|
|
371
|
-
return { valid: false, error: error.message };
|
|
372
|
-
}
|
|
515
|
+
const snapshot = getSnapshot(currentRow);
|
|
516
|
+
setValue(snapshot, field.path, transformedValue);
|
|
373
517
|
|
|
374
|
-
|
|
518
|
+
const schema = getSchema(currentRow);
|
|
519
|
+
invariant(schema);
|
|
520
|
+
|
|
521
|
+
const validationResult = validateSchema(schema, snapshot);
|
|
522
|
+
if (validationResult && validationResult.length > 0) {
|
|
523
|
+
const error = validationResult[0];
|
|
524
|
+
invariant(error.path === field.path);
|
|
525
|
+
return { valid: false, error: error.message };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return { valid: true };
|
|
529
|
+
}
|
|
375
530
|
};
|
|
376
531
|
|
|
377
|
-
public setCellData = (
|
|
378
|
-
const
|
|
379
|
-
const fields = this.
|
|
532
|
+
public setCellData = (cell: DxGridPosition, value: any): void => {
|
|
533
|
+
const { col, row, plane } = cell;
|
|
534
|
+
const fields = this._projection?.fields ?? [];
|
|
380
535
|
if (col < 0 || col >= fields.length) {
|
|
381
536
|
return;
|
|
382
537
|
}
|
|
@@ -387,12 +542,21 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
387
542
|
|
|
388
543
|
// Special handling for Ref format to preserve existing behavior
|
|
389
544
|
if (props.format === FormatEnum.Ref && !isLiveObject(value)) {
|
|
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?
|
|
392
545
|
return;
|
|
393
546
|
}
|
|
394
547
|
|
|
395
|
-
|
|
548
|
+
if (plane === 'frozenRowsEnd') {
|
|
549
|
+
// Update draft row data
|
|
550
|
+
if (row >= 0 && row < this._draftRows.value.length) {
|
|
551
|
+
setValue(this._draftRows.value[row].data, field.path, transformedValue);
|
|
552
|
+
// Re-validate the draft row after the update
|
|
553
|
+
this.validateDraftRow(row);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// Update regular row data
|
|
557
|
+
const rowIdx = this._sorting.getDataIndex(row);
|
|
558
|
+
setValue(this._rows.value[rowIdx], field.path, transformedValue);
|
|
559
|
+
}
|
|
396
560
|
};
|
|
397
561
|
|
|
398
562
|
/**
|
|
@@ -402,7 +566,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
402
566
|
*/
|
|
403
567
|
public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
|
|
404
568
|
const dataRow = this._sorting.getDataIndex(row);
|
|
405
|
-
const fields = this.
|
|
569
|
+
const fields = this._projection?.fields ?? [];
|
|
406
570
|
const field = fields[col];
|
|
407
571
|
|
|
408
572
|
const value = getValue(this._rows.value[dataRow], field.path);
|
|
@@ -410,16 +574,43 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
410
574
|
setValue(this._rows.value[dataRow], field.path, updatedValue);
|
|
411
575
|
}
|
|
412
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Gets the domain-appropriate cell type for a given cell position.
|
|
579
|
+
* This abstracts away the low-level grid plane concepts and provides domain-specific types.
|
|
580
|
+
* @param cell - The cell position to classify
|
|
581
|
+
* @returns The domain-appropriate cell type
|
|
582
|
+
*/
|
|
583
|
+
public getCellType(cell: DxGridPosition): TableCellType {
|
|
584
|
+
switch (cell.plane) {
|
|
585
|
+
case 'frozenRowsEnd':
|
|
586
|
+
return 'draft';
|
|
587
|
+
case 'frozenRowsStart':
|
|
588
|
+
return 'header';
|
|
589
|
+
case 'grid':
|
|
590
|
+
default:
|
|
591
|
+
return 'standard';
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Convenience method to check if a cell is a draft cell.
|
|
597
|
+
* @param cell - The cell position to check
|
|
598
|
+
* @returns true if the cell is a draft cell, false otherwise
|
|
599
|
+
*/
|
|
600
|
+
public isDraftCell(cell: DxGridPosition): boolean {
|
|
601
|
+
return this.getCellType(cell) === 'draft';
|
|
602
|
+
}
|
|
603
|
+
|
|
413
604
|
//
|
|
414
605
|
// Column Operations
|
|
415
606
|
//
|
|
416
607
|
|
|
417
608
|
public deleteColumn(fieldId: string): void {
|
|
418
|
-
if (!this.
|
|
609
|
+
if (!this._projection) {
|
|
419
610
|
return;
|
|
420
611
|
}
|
|
421
612
|
|
|
422
|
-
const field = this.
|
|
613
|
+
const field = this._projection?.fields.find((field) => field.id === fieldId);
|
|
423
614
|
if (field && this._onDeleteColumn) {
|
|
424
615
|
this._onDeleteColumn(field.id);
|
|
425
616
|
}
|
|
@@ -430,12 +621,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
430
621
|
//
|
|
431
622
|
|
|
432
623
|
public setColumnWidth(columnIndex: number, width: number): void {
|
|
433
|
-
const fields = this.
|
|
624
|
+
const fields = this._projection?.fields ?? [];
|
|
434
625
|
if (columnIndex < fields.length) {
|
|
435
626
|
const newWidth = Math.max(0, width);
|
|
436
627
|
const field = fields[columnIndex];
|
|
437
628
|
if (field) {
|
|
438
|
-
field.
|
|
629
|
+
this.table.sizes[field.path] = newWidth;
|
|
439
630
|
}
|
|
440
631
|
}
|
|
441
632
|
}
|
|
@@ -457,6 +648,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
457
648
|
//
|
|
458
649
|
// View operations
|
|
459
650
|
//
|
|
651
|
+
|
|
460
652
|
public saveView(): void {
|
|
461
653
|
this._sorting.save();
|
|
462
654
|
}
|
|
@@ -466,7 +658,7 @@ const editorTextToCellValue = (props: PropertyType, value: any): any => {
|
|
|
466
658
|
switch (props.format) {
|
|
467
659
|
case FormatEnum.Ref: {
|
|
468
660
|
if (isLiveObject(value)) {
|
|
469
|
-
return
|
|
661
|
+
return Ref.make(value);
|
|
470
662
|
} else {
|
|
471
663
|
return value;
|
|
472
664
|
}
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Schema } from 'effect';
|
|
6
|
-
import { describe, expect, it
|
|
6
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
7
|
|
|
8
|
-
import { Obj
|
|
8
|
+
import { Obj } from '@dxos/echo';
|
|
9
9
|
import { TypedObject } from '@dxos/echo-schema';
|
|
10
10
|
import { live } from '@dxos/live-object';
|
|
11
11
|
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
12
|
-
import { createView
|
|
12
|
+
import { createView } from '@dxos/schema';
|
|
13
|
+
|
|
14
|
+
import { TableView } from '../types';
|
|
13
15
|
|
|
14
16
|
import { TableModel, type TableModelProps } from './table-model';
|
|
15
17
|
import { TablePresentation } from './table-presentation';
|
|
16
|
-
import { TableType } from '../types';
|
|
17
18
|
|
|
18
19
|
describe('TablePresentation', () => {
|
|
19
20
|
describe('row reactivity', () => {
|
|
@@ -97,8 +98,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
|
|
|
97
98
|
|
|
98
99
|
const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|
|
99
100
|
const schema = createEchoSchema(Test);
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
return new TableModel({ id: table.id, view, projection, ...props });
|
|
101
|
+
const table = Obj.make(TableView, { sizes: {} });
|
|
102
|
+
const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
|
|
103
|
+
return new TableModel({ view, schema: schema.jsonSchema, ...props });
|
|
104
104
|
};
|