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