@dxos/react-ui-table 0.7.4 → 0.7.5-main.937ce75
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-IYCSJDMB.mjs → chunk-AT7O4RAD.mjs} +4 -4
- package/dist/lib/browser/{chunk-IYCSJDMB.mjs.map → chunk-AT7O4RAD.mjs.map} +2 -2
- package/dist/lib/browser/index.mjs +1442 -1219
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node/{chunk-MHBMVCFZ.cjs → chunk-YSBXNBDJ.cjs} +6 -6
- package/dist/lib/node/{chunk-MHBMVCFZ.cjs.map → chunk-YSBXNBDJ.cjs.map} +2 -2
- package/dist/lib/node/index.cjs +1351 -1111
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types/index.cjs +3 -3
- package/dist/lib/node/types/index.cjs.map +1 -1
- package/dist/lib/node-esm/{chunk-BMWGPXPL.mjs → chunk-6OH2NHUX.mjs} +4 -4
- package/dist/lib/node-esm/{chunk-BMWGPXPL.mjs.map → chunk-6OH2NHUX.mjs.map} +2 -2
- package/dist/lib/node-esm/index.mjs +1442 -1219
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/_deprecated/hooks/useColumnSorting.d.ts +1 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +4 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/CreateRefPanel.d.ts +3 -2
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/RefPanel.d.ts +4 -3
- package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts +3 -2
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +2 -1
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.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.map +1 -1
- package/dist/types/src/components/testing.d.ts +55 -71
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/model/index.d.ts +1 -0
- package/dist/types/src/model/index.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +6 -2
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +14 -27
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +22 -0
- package/dist/types/src/model/table-presentation.d.ts.map +1 -0
- package/dist/types/src/model/table-presentation.test.d.ts +2 -0
- package/dist/types/src/model/table-presentation.test.d.ts.map +1 -0
- package/dist/types/src/model/table-sorting.d.ts +48 -0
- package/dist/types/src/model/table-sorting.d.ts.map +1 -0
- 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/types/table.d.ts +118 -154
- package/dist/types/src/types/table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts +50 -20
- package/dist/types/src/util/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts +32 -9
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/src/util/table.d.ts +2 -2
- package/dist/types/src/util/table.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +29 -29
- package/src/components/Table/ColumnActionsMenu.tsx +8 -8
- package/src/components/Table/ColumnSettings.tsx +11 -11
- package/src/components/Table/CreateRefPanel.tsx +13 -14
- package/src/components/Table/RefPanel.tsx +9 -9
- package/src/components/Table/RowActionsMenu.tsx +6 -7
- package/src/components/Table/Table.stories.tsx +40 -23
- package/src/components/Table/Table.tsx +235 -148
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -12
- package/src/components/TableCellEditor/TableCellEditor.tsx +6 -4
- package/src/components/testing.ts +10 -8
- package/src/index.ts +2 -0
- package/src/model/index.ts +1 -0
- package/src/model/modal-controller.ts +15 -48
- package/src/model/selection-model.ts +12 -1
- package/src/model/table-model.test.ts +4 -71
- package/src/model/table-model.ts +61 -292
- package/src/model/table-presentation.test.ts +102 -0
- package/src/model/table-presentation.ts +217 -0
- package/src/model/table-sorting.ts +131 -0
- package/src/playwright/TableManager.ts +5 -1
- package/src/playwright/smoke.spec.ts +79 -1
- package/src/types/table.ts +3 -3
- package/src/util/index.ts +1 -0
- package/src/util/table-buttons.ts +97 -48
- package/src/util/table-controls.ts +71 -7
- package/src/util/table.ts +21 -13
package/src/model/table-model.ts
CHANGED
|
@@ -3,40 +3,25 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
|
|
6
|
-
import orderBy from 'lodash.orderby';
|
|
7
6
|
|
|
8
7
|
import { Resource } from '@dxos/context';
|
|
9
8
|
import { getValue, setValue, FormatEnum, type JsonProp } from '@dxos/echo-schema';
|
|
10
9
|
import { invariant } from '@dxos/invariant';
|
|
10
|
+
import { isReactiveObject, makeRef } from '@dxos/live-object';
|
|
11
11
|
import { PublicKey } from '@dxos/react-client';
|
|
12
|
-
import {
|
|
13
|
-
cellClassesForFieldType,
|
|
14
|
-
cellClassesForRowSelection,
|
|
15
|
-
formatForDisplay,
|
|
16
|
-
formatForEditing,
|
|
17
|
-
parseValue,
|
|
18
|
-
} from '@dxos/react-ui-form';
|
|
12
|
+
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
19
13
|
import {
|
|
20
14
|
type DxGridAxisMeta,
|
|
21
|
-
type DxGridCellValue,
|
|
22
|
-
type DxGridPlane,
|
|
23
|
-
type DxGridPlaneCells,
|
|
24
15
|
type DxGridPlaneRange,
|
|
25
16
|
type DxGridPlanePosition,
|
|
26
|
-
toPlaneCellIndex,
|
|
27
17
|
type DxGridPosition,
|
|
28
18
|
} from '@dxos/react-ui-grid';
|
|
29
|
-
import {
|
|
30
|
-
import { VIEW_FIELD_LIMIT, type ViewProjection, type FieldType } from '@dxos/schema';
|
|
19
|
+
import { type ViewProjection } from '@dxos/schema';
|
|
31
20
|
|
|
32
|
-
import { ModalController } from './modal-controller';
|
|
33
21
|
import { SelectionModel } from './selection-model';
|
|
22
|
+
import { type SortConfig, type SortDirection, TableSorting } from './table-sorting';
|
|
34
23
|
import { type TableType } from '../types';
|
|
35
|
-
import {
|
|
36
|
-
import { tableControls } from '../util/table-controls';
|
|
37
|
-
|
|
38
|
-
export type SortDirection = 'asc' | 'desc';
|
|
39
|
-
export type SortConfig = { fieldId: string; direction: SortDirection };
|
|
24
|
+
import { touch } from '../util';
|
|
40
25
|
|
|
41
26
|
export type BaseTableRow = Record<JsonProp, any> & { id: string };
|
|
42
27
|
|
|
@@ -63,28 +48,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
63
48
|
end: { row: 0, col: 0 },
|
|
64
49
|
});
|
|
65
50
|
|
|
66
|
-
/**
|
|
67
|
-
* Maps display indices to data indices.
|
|
68
|
-
* Used for translating between sorted/displayed order and original data order.
|
|
69
|
-
* Keys are display indices, values are corresponding data indices.
|
|
70
|
-
*/
|
|
71
|
-
private readonly _displayToDataIndex: Map<number, number> = new Map();
|
|
72
|
-
|
|
73
51
|
private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
|
|
74
52
|
private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
|
|
75
53
|
private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
|
|
76
54
|
private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
|
|
77
55
|
private readonly _onRowOrderChanged?: TableModelProps<T>['onRowOrderChanged'];
|
|
78
56
|
|
|
79
|
-
private readonly
|
|
80
|
-
private
|
|
81
|
-
private _rows = signal<T[]>([]);
|
|
57
|
+
private readonly _rows = signal<T[]>([]);
|
|
58
|
+
private readonly _sorting: TableSorting<T>;
|
|
82
59
|
|
|
83
60
|
private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
|
|
84
|
-
|
|
85
61
|
private _selection!: SelectionModel<T>;
|
|
86
62
|
private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
|
|
87
|
-
private readonly _modalController = new ModalController();
|
|
88
63
|
|
|
89
64
|
constructor({
|
|
90
65
|
table,
|
|
@@ -100,8 +75,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
100
75
|
super();
|
|
101
76
|
this._table = table;
|
|
102
77
|
this._projection = projection;
|
|
78
|
+
this._sorting = new TableSorting(this._rows, table, projection);
|
|
79
|
+
|
|
80
|
+
if (sorting.length > 0) {
|
|
81
|
+
const [sort] = sorting;
|
|
82
|
+
this._sorting.setSort(sort.fieldId, sort.direction);
|
|
83
|
+
}
|
|
103
84
|
|
|
104
|
-
this._sorting.value = sorting.at(0);
|
|
105
85
|
this._pinnedRows = pinnedRows;
|
|
106
86
|
this._onInsertRow = onInsertRow;
|
|
107
87
|
this._onDeleteRows = onDeleteRows;
|
|
@@ -118,6 +98,17 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
118
98
|
return this._projection;
|
|
119
99
|
}
|
|
120
100
|
|
|
101
|
+
public get rows(): ReadonlySignal<T[]> {
|
|
102
|
+
return this._sorting.sortedRows;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @reactive
|
|
107
|
+
*/
|
|
108
|
+
public get sorting(): SortConfig | undefined {
|
|
109
|
+
return this._sorting.sorting;
|
|
110
|
+
}
|
|
111
|
+
|
|
121
112
|
public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
|
|
122
113
|
return this._pinnedRows;
|
|
123
114
|
}
|
|
@@ -127,14 +118,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
127
118
|
return this._columnMeta;
|
|
128
119
|
}
|
|
129
120
|
|
|
130
|
-
public get sorting(): SortConfig | undefined {
|
|
131
|
-
return this._sorting.value;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
public get modalController() {
|
|
135
|
-
return this._modalController;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
121
|
public get selection() {
|
|
139
122
|
return this._selection;
|
|
140
123
|
}
|
|
@@ -145,15 +128,14 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
145
128
|
|
|
146
129
|
protected override async _open() {
|
|
147
130
|
this.initializeColumnMeta();
|
|
148
|
-
this.initializeSorting();
|
|
149
131
|
this.initializeEffects();
|
|
150
|
-
this._selection = new SelectionModel(this.
|
|
132
|
+
this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
|
|
151
133
|
await this._selection.open(this._ctx);
|
|
152
134
|
}
|
|
153
135
|
|
|
154
136
|
private initializeColumnMeta(): void {
|
|
155
137
|
this._columnMeta = computed(() => {
|
|
156
|
-
const fields = this._table.view?.fields ?? [];
|
|
138
|
+
const fields = this._table.view?.target?.fields ?? [];
|
|
157
139
|
const meta = Object.fromEntries(
|
|
158
140
|
fields.map((field, index: number) => [index, { size: field?.size ?? 256, resizeable: true }]),
|
|
159
141
|
);
|
|
@@ -166,39 +148,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
166
148
|
});
|
|
167
149
|
}
|
|
168
150
|
|
|
169
|
-
private initializeSorting(): void {
|
|
170
|
-
this._sortedRows = computed(() => {
|
|
171
|
-
this._displayToDataIndex.clear();
|
|
172
|
-
const sort = this._sorting.value;
|
|
173
|
-
if (!sort) {
|
|
174
|
-
return this._rows.value;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const field = this._table.view?.fields.find((field) => field.id === sort.fieldId);
|
|
178
|
-
if (!field) {
|
|
179
|
-
return this._rows.value;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const dataWithIndices = this._rows.value.map((item, index) => {
|
|
183
|
-
const value = getValue(item, field.path);
|
|
184
|
-
return { item, index, value, isEmpty: value == null || value === '' };
|
|
185
|
-
});
|
|
186
|
-
const sorted = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
|
|
187
|
-
|
|
188
|
-
for (let displayIndex = 0; displayIndex < sorted.length; displayIndex++) {
|
|
189
|
-
const { index: dataIndex } = sorted[displayIndex];
|
|
190
|
-
if (displayIndex !== dataIndex) {
|
|
191
|
-
this._displayToDataIndex.set(displayIndex, dataIndex);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return sorted.map(({ item }) => item);
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
151
|
private initializeEffects(): void {
|
|
200
152
|
const rowOrderWatcher = effect(() => {
|
|
201
|
-
touch(this.
|
|
153
|
+
touch(this._sorting.sortedRows.value);
|
|
202
154
|
this._onRowOrderChanged?.();
|
|
203
155
|
});
|
|
204
156
|
this._ctx.onDispose(rowOrderWatcher);
|
|
@@ -214,8 +166,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
214
166
|
for (let row = start.row; row <= end.row; row++) {
|
|
215
167
|
rowEffects.push(
|
|
216
168
|
effect(() => {
|
|
217
|
-
const obj = this.
|
|
218
|
-
this?._table?.view?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
169
|
+
const obj = this._sorting.sortedRows.value[row];
|
|
170
|
+
this?._table?.view?.target?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
219
171
|
this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
|
|
220
172
|
}),
|
|
221
173
|
);
|
|
@@ -226,181 +178,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
226
178
|
this._ctx.onDispose(rowEffectManager);
|
|
227
179
|
}
|
|
228
180
|
|
|
229
|
-
//
|
|
230
|
-
// Get Cells
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
public getCells = (range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells => {
|
|
234
|
-
switch (plane) {
|
|
235
|
-
case 'grid': {
|
|
236
|
-
this._visibleRange.value = range;
|
|
237
|
-
return this.getMainGridCells(range);
|
|
238
|
-
}
|
|
239
|
-
case 'frozenRowsStart': {
|
|
240
|
-
return this.getHeaderCells(range);
|
|
241
|
-
}
|
|
242
|
-
case 'frozenColsStart': {
|
|
243
|
-
return this.getSelectionColumnCells(range);
|
|
244
|
-
}
|
|
245
|
-
case 'frozenColsEnd': {
|
|
246
|
-
return this.getActionColumnCells(range);
|
|
247
|
-
}
|
|
248
|
-
case 'fixedStartStart': {
|
|
249
|
-
return this.getSelectAllCell();
|
|
250
|
-
}
|
|
251
|
-
case 'fixedStartEnd': {
|
|
252
|
-
return this.getNewColumnCell();
|
|
253
|
-
}
|
|
254
|
-
default: {
|
|
255
|
-
return {};
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
private getMainGridCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
|
|
261
|
-
const cells: DxGridPlaneCells = {};
|
|
262
|
-
const fields = this._table.view?.fields ?? [];
|
|
263
|
-
|
|
264
|
-
const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
|
|
265
|
-
const { props } = this._projection.getFieldProjection(field.id);
|
|
266
|
-
|
|
267
|
-
const cell: DxGridCellValue = {
|
|
268
|
-
get value() {
|
|
269
|
-
const value = getValue(obj, field.path);
|
|
270
|
-
if (value == null) {
|
|
271
|
-
return '';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
switch (props.format) {
|
|
275
|
-
case FormatEnum.Ref: {
|
|
276
|
-
if (!field.referencePath) {
|
|
277
|
-
return ''; // TODO(burdon): Show error.
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return getValue(value, field.referencePath);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
default: {
|
|
284
|
-
return formatForDisplay({ type: props.type, format: props.format, value });
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const classes = [];
|
|
291
|
-
const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
|
|
292
|
-
if (formatClasses) {
|
|
293
|
-
classes.push(formatClasses);
|
|
294
|
-
}
|
|
295
|
-
const rowSelectionClasses = cellClassesForRowSelection(this._selection.isObjectSelected(obj));
|
|
296
|
-
if (rowSelectionClasses) {
|
|
297
|
-
classes.push(rowSelectionClasses);
|
|
298
|
-
}
|
|
299
|
-
if (classes.length > 0) {
|
|
300
|
-
cell.className = mx(classes.flat());
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (cell.value && props.format === FormatEnum.Ref && props.referenceSchema) {
|
|
304
|
-
const targetObj = getValue(obj, field.path);
|
|
305
|
-
cell.accessoryHtml = tableButtons.referencedCell.render({
|
|
306
|
-
targetId: targetObj.id,
|
|
307
|
-
schemaId: props.referenceSchema,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
|
|
312
|
-
cells[idx] = cell;
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
for (let row = range.start.row; row <= range.end.row && row < this._sortedRows.value.length; row++) {
|
|
316
|
-
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
317
|
-
const field = fields[col];
|
|
318
|
-
if (!field) {
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
addCell(this._sortedRows.value[row], field, col, row);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return cells;
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
private getHeaderCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
|
|
330
|
-
const cells: DxGridPlaneCells = {};
|
|
331
|
-
const fields = this.table.view?.fields ?? [];
|
|
332
|
-
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
333
|
-
const { field, props } = this._projection.getFieldProjection(fields[col].id);
|
|
334
|
-
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
335
|
-
// TODO(burdon): Use same logic as form for fallback title.
|
|
336
|
-
value: props.title ?? field.path,
|
|
337
|
-
readonly: true,
|
|
338
|
-
resizeHandle: 'col',
|
|
339
|
-
accessoryHtml: tableButtons.columnSettings.render({ fieldId: field.id }),
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return cells;
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
private getSelectionColumnCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
|
|
347
|
-
const cells: DxGridPlaneCells = {};
|
|
348
|
-
for (let row = range.start.row; row <= range.end.row && row < this._rows.value.length; row++) {
|
|
349
|
-
const isSelected = this._selection.isRowIndexSelected(row);
|
|
350
|
-
const classes = cellClassesForRowSelection(isSelected);
|
|
351
|
-
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
352
|
-
value: '',
|
|
353
|
-
readonly: true,
|
|
354
|
-
className: classes ? mx(classes) : undefined,
|
|
355
|
-
accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return cells;
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
private getActionColumnCells = (range: DxGridPlaneRange): DxGridPlaneCells => {
|
|
363
|
-
const cells: DxGridPlaneCells = {};
|
|
364
|
-
for (let row = range.start.row; row <= range.end.row && row < this._rows.value.length; row++) {
|
|
365
|
-
const isSelected = this._selection.isRowIndexSelected(row);
|
|
366
|
-
const classes = cellClassesForRowSelection(isSelected);
|
|
367
|
-
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
368
|
-
value: '',
|
|
369
|
-
readonly: true,
|
|
370
|
-
className: classes ? mx(classes) : undefined,
|
|
371
|
-
accessoryHtml: tableButtons.rowMenu.render({ rowIndex: row }),
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return cells;
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
private getSelectAllCell = (): DxGridPlaneCells => {
|
|
379
|
-
return {
|
|
380
|
-
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
381
|
-
value: '',
|
|
382
|
-
accessoryHtml: tableControls.checkbox.render({
|
|
383
|
-
rowIndex: 0,
|
|
384
|
-
header: true,
|
|
385
|
-
checked: this._selection.allRowsSeleted.value,
|
|
386
|
-
}),
|
|
387
|
-
readonly: true,
|
|
388
|
-
},
|
|
389
|
-
};
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
private getNewColumnCell = (): DxGridPlaneCells => {
|
|
393
|
-
return {
|
|
394
|
-
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
395
|
-
value: '',
|
|
396
|
-
accessoryHtml: tableButtons.addColumn.render({
|
|
397
|
-
disabled: (this._table.view?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
|
|
398
|
-
}),
|
|
399
|
-
readonly: true,
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
};
|
|
403
|
-
|
|
404
181
|
//
|
|
405
182
|
// Data
|
|
406
183
|
//
|
|
@@ -411,15 +188,15 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
411
188
|
|
|
412
189
|
public getRowCount = (): number => this._rows.value.length;
|
|
413
190
|
|
|
414
|
-
public getColumnCount = (): number => this.table.view?.fields.length ?? 0;
|
|
191
|
+
public getColumnCount = (): number => this.table.view?.target?.fields.length ?? 0;
|
|
415
192
|
|
|
416
193
|
public insertRow = (rowIndex?: number): void => {
|
|
417
|
-
const row = rowIndex !== undefined ? this.
|
|
194
|
+
const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
|
|
418
195
|
this._onInsertRow?.(row);
|
|
419
196
|
};
|
|
420
197
|
|
|
421
198
|
public deleteRow = (rowIndex: number): void => {
|
|
422
|
-
const row = this.
|
|
199
|
+
const row = this._sorting.getDataIndex(rowIndex);
|
|
423
200
|
const obj = this._rows.value[row];
|
|
424
201
|
const objectsToDelete = [];
|
|
425
202
|
|
|
@@ -434,13 +211,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
434
211
|
};
|
|
435
212
|
|
|
436
213
|
public getCellData = ({ col, row }: DxGridPlanePosition): any => {
|
|
437
|
-
const fields = this.table.view?.fields ?? [];
|
|
214
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
438
215
|
if (col < 0 || col >= fields.length) {
|
|
439
216
|
return undefined;
|
|
440
217
|
}
|
|
441
218
|
|
|
442
219
|
const field = fields[col];
|
|
443
|
-
const dataIndex = this.
|
|
220
|
+
const dataIndex = this._sorting.getDataIndex(row);
|
|
444
221
|
const value = getValue(this._rows.value[dataIndex], field.path);
|
|
445
222
|
if (value == null) {
|
|
446
223
|
return '';
|
|
@@ -453,7 +230,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
453
230
|
return ''; // TODO(burdon): Show error.
|
|
454
231
|
}
|
|
455
232
|
|
|
456
|
-
return getValue(value, field.referencePath);
|
|
233
|
+
return getValue(value.target, field.referencePath);
|
|
457
234
|
}
|
|
458
235
|
|
|
459
236
|
default: {
|
|
@@ -463,8 +240,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
463
240
|
};
|
|
464
241
|
|
|
465
242
|
public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
|
|
466
|
-
const rowIdx = this.
|
|
467
|
-
const fields = this.table.view?.fields ?? [];
|
|
243
|
+
const rowIdx = this._sorting.getDataIndex(row);
|
|
244
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
468
245
|
if (col < 0 || col >= fields.length) {
|
|
469
246
|
return;
|
|
470
247
|
}
|
|
@@ -473,7 +250,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
473
250
|
const { props } = this._projection.getFieldProjection(field.id);
|
|
474
251
|
switch (props.format) {
|
|
475
252
|
case FormatEnum.Ref: {
|
|
476
|
-
|
|
253
|
+
// TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
|
|
254
|
+
// plain string value. Maybe onBlur should be called with the actual value?
|
|
255
|
+
if (isReactiveObject(value)) {
|
|
256
|
+
setValue(this._rows.value[rowIdx], field.path, makeRef(value));
|
|
257
|
+
}
|
|
477
258
|
break;
|
|
478
259
|
}
|
|
479
260
|
|
|
@@ -492,6 +273,21 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
492
273
|
}
|
|
493
274
|
};
|
|
494
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Updates a cell's value using a transform function.
|
|
278
|
+
* @param {DxGridPlanePosition} position - The position of the cell to update.
|
|
279
|
+
* @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
|
|
280
|
+
*/
|
|
281
|
+
public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
|
|
282
|
+
const dataRow = this._sorting.getDataIndex(row);
|
|
283
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
284
|
+
const field = fields[col];
|
|
285
|
+
|
|
286
|
+
const value = getValue(this._rows.value[dataRow], field.path);
|
|
287
|
+
const updatedValue = update(value);
|
|
288
|
+
setValue(this._rows.value[dataRow], field.path, updatedValue);
|
|
289
|
+
}
|
|
290
|
+
|
|
495
291
|
//
|
|
496
292
|
// Column Operations
|
|
497
293
|
//
|
|
@@ -501,45 +297,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
501
297
|
return;
|
|
502
298
|
}
|
|
503
299
|
|
|
504
|
-
const field = this.table.view.fields.find((field) => field.id === fieldId);
|
|
300
|
+
const field = this.table.view.target?.fields.find((field) => field.id === fieldId);
|
|
505
301
|
if (field && this._onDeleteColumn) {
|
|
506
|
-
if (this._sorting.value?.fieldId === fieldId) {
|
|
507
|
-
this.clearSort();
|
|
508
|
-
}
|
|
509
302
|
this._onDeleteColumn(field.id);
|
|
510
303
|
}
|
|
511
304
|
}
|
|
512
305
|
|
|
513
|
-
//
|
|
514
|
-
// Interactions
|
|
515
|
-
//
|
|
516
|
-
public handleGridClick = (event: React.MouseEvent): void => {
|
|
517
|
-
if (!this._modalController.handleClick(event)) {
|
|
518
|
-
const target = event.target as HTMLElement;
|
|
519
|
-
|
|
520
|
-
const selectionCheckbox = target.closest(`input[${tableControls.checkbox.attributes.checkbox}]`);
|
|
521
|
-
if (selectionCheckbox) {
|
|
522
|
-
const isHeader = selectionCheckbox.hasAttribute(tableControls.checkbox.attributes.header);
|
|
523
|
-
if (isHeader) {
|
|
524
|
-
if (this._selection.allRowsSeleted.value) {
|
|
525
|
-
this._selection.setSelection('none');
|
|
526
|
-
} else {
|
|
527
|
-
this._selection.setSelection('all');
|
|
528
|
-
}
|
|
529
|
-
} else {
|
|
530
|
-
const rowIndex = Number(selectionCheckbox.getAttribute(tableControls.checkbox.attributes.checkbox));
|
|
531
|
-
this._selection.toggleSelectionForRowIndex(rowIndex);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
};
|
|
536
|
-
|
|
537
306
|
//
|
|
538
307
|
// Resize
|
|
539
308
|
//
|
|
540
309
|
|
|
541
310
|
public setColumnWidth(columnIndex: number, width: number): void {
|
|
542
|
-
const fields = this.table.view?.fields ?? [];
|
|
311
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
543
312
|
if (columnIndex < fields.length) {
|
|
544
313
|
const newWidth = Math.max(0, width);
|
|
545
314
|
const field = fields[columnIndex];
|
|
@@ -554,11 +323,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
554
323
|
//
|
|
555
324
|
|
|
556
325
|
public setSort(fieldId: string, direction: SortDirection): void {
|
|
557
|
-
this._sorting.
|
|
326
|
+
this._sorting.setSort(fieldId, direction);
|
|
558
327
|
}
|
|
559
328
|
|
|
560
329
|
public clearSort(): void {
|
|
561
|
-
this._sorting.
|
|
330
|
+
this._sorting.setSort('', 'asc');
|
|
562
331
|
}
|
|
563
332
|
|
|
564
333
|
//
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { S, TypedObject } from '@dxos/echo-schema';
|
|
8
|
+
import { create, makeRef } from '@dxos/live-object';
|
|
9
|
+
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
10
|
+
import { createView, ViewProjection } from '@dxos/schema';
|
|
11
|
+
|
|
12
|
+
import { TableModel, type TableModelProps } from './table-model';
|
|
13
|
+
import { TablePresentation } from './table-presentation';
|
|
14
|
+
import { TableType } from '../types';
|
|
15
|
+
|
|
16
|
+
describe('TablePresentation', () => {
|
|
17
|
+
describe('row reactivity', () => {
|
|
18
|
+
let data: any[];
|
|
19
|
+
let updateCount: number;
|
|
20
|
+
let model: any;
|
|
21
|
+
let presentation: TablePresentation;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
updateCount = 0;
|
|
25
|
+
({ data } = create({
|
|
26
|
+
data: [
|
|
27
|
+
{ col1: 'A', col2: 1, col3: true },
|
|
28
|
+
{ col1: 'B', col2: 2, col3: false },
|
|
29
|
+
],
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
model = createTableModel({
|
|
33
|
+
onCellUpdate: () => updateCount++,
|
|
34
|
+
});
|
|
35
|
+
model.setRows(data);
|
|
36
|
+
await model.open();
|
|
37
|
+
presentation = new TablePresentation(model);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should update with row-level reactivity', () => {
|
|
41
|
+
// Set up visible range to include our test data.
|
|
42
|
+
presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
|
|
43
|
+
|
|
44
|
+
// Trigger a row update.
|
|
45
|
+
data[0].col1 = 'New Value';
|
|
46
|
+
expect(updateCount).toBe(1);
|
|
47
|
+
|
|
48
|
+
// Verify the new value through getCells.
|
|
49
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'grid');
|
|
50
|
+
expect(cells['0,0']?.value).toBe('New Value');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should update reactively when adding a new row', () => {
|
|
54
|
+
// Set up visible range to include our test data.
|
|
55
|
+
presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
|
|
56
|
+
|
|
57
|
+
data.push({ col1: 'C', col2: 3, col3: true });
|
|
58
|
+
expect(updateCount).toBe(1);
|
|
59
|
+
|
|
60
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
|
|
61
|
+
|
|
62
|
+
// Verify the new row's data.
|
|
63
|
+
expect(cells['0,2']?.value).toBe('C');
|
|
64
|
+
expect(cells['1,2']?.value).toBe('3');
|
|
65
|
+
expect(cells['2,2']?.value).toBe('true');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle combined operations reactively', () => {
|
|
69
|
+
// Set up visible range.
|
|
70
|
+
presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
|
|
71
|
+
|
|
72
|
+
// Multiple operations.
|
|
73
|
+
data[0].col1 = 'Updated A';
|
|
74
|
+
data.push({ col1: 'C', col2: 3, col3: true });
|
|
75
|
+
data.splice(1, 1);
|
|
76
|
+
|
|
77
|
+
// We expect one update per operation affecting visible rows.
|
|
78
|
+
expect(updateCount).toBe(3);
|
|
79
|
+
|
|
80
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
|
|
81
|
+
|
|
82
|
+
// Verify final state.
|
|
83
|
+
expect(cells['0,0']?.value).toBe('Updated A');
|
|
84
|
+
expect(cells['0,1']?.value).toBe('C');
|
|
85
|
+
expect(cells['1,1']?.value).toBe('3');
|
|
86
|
+
expect(cells['2,1']?.value).toBe('true');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
|
|
92
|
+
title: S.String,
|
|
93
|
+
completed: S.Boolean,
|
|
94
|
+
}) {}
|
|
95
|
+
|
|
96
|
+
const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|
|
97
|
+
const schema = createEchoSchema(Test);
|
|
98
|
+
const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
|
|
99
|
+
const projection = new ViewProjection(schema, view);
|
|
100
|
+
const table = create(TableType, { view: makeRef(view) });
|
|
101
|
+
return new TableModel({ table, projection, ...props });
|
|
102
|
+
};
|