@dxos/react-ui-table 0.7.4-staging.f7e8224 → 0.7.5-labs.071a3e2
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-3FR6NHPD.mjs} +7 -5
- package/dist/lib/browser/chunk-3FR6NHPD.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1424 -1067
- 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-NB3SFHGA.cjs} +9 -7
- package/dist/lib/node/chunk-NB3SFHGA.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1429 -1052
- 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-RLVVOZW5.mjs} +7 -5
- package/dist/lib/node-esm/chunk-RLVVOZW5.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1424 -1067
- 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 +1 -4
- 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/TableToolbar/TableToolbar.d.ts +18 -0
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -0
- package/dist/types/src/components/TableToolbar/index.d.ts +2 -0
- package/dist/types/src/components/TableToolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +31 -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 +19 -31
- 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 +53 -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/translations.d.ts +2 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/table.d.ts +46 -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 +63 -19
- 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 +3 -2
- package/dist/types/src/util/table.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +35 -34
- package/src/_deprecated/schema/Schema.stories.tsx +1 -1
- package/src/components/Table/ColumnActionsMenu.tsx +12 -12
- 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 +85 -56
- package/src/components/Table/Table.tsx +239 -148
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -12
- package/src/components/TableCellEditor/TableCellEditor.tsx +6 -4
- package/src/components/TableToolbar/TableToolbar.tsx +72 -0
- package/src/components/{Toolbar → TableToolbar}/index.ts +1 -1
- package/src/components/index.ts +1 -1
- 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 -76
- package/src/model/table-model.ts +73 -304
- package/src/model/table-presentation.test.ts +102 -0
- package/src/model/table-presentation.ts +235 -0
- package/src/model/table-sorting.ts +215 -0
- package/src/playwright/TableManager.ts +5 -1
- package/src/playwright/smoke.spec.ts +94 -9
- package/src/translations.ts +2 -1
- package/src/types/table.ts +4 -4
- package/src/util/index.ts +1 -0
- package/src/util/table-buttons.ts +127 -48
- package/src/util/table-controls.ts +71 -7
- package/src/util/table.ts +93 -52
- package/dist/lib/browser/chunk-IYCSJDMB.mjs.map +0 -7
- package/dist/lib/node/chunk-MHBMVCFZ.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-BMWGPXPL.mjs.map +0 -7
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +0 -21
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +0 -1
- package/dist/types/src/components/Toolbar/common.d.ts +0 -14
- package/dist/types/src/components/Toolbar/common.d.ts.map +0 -1
- package/dist/types/src/components/Toolbar/index.d.ts +0 -2
- package/dist/types/src/components/Toolbar/index.d.ts.map +0 -1
- package/src/components/Toolbar/Toolbar.tsx +0 -87
- package/src/components/Toolbar/common.tsx +0 -71
package/src/model/table-model.ts
CHANGED
|
@@ -3,47 +3,32 @@
|
|
|
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
|
-
import { getValue, setValue,
|
|
8
|
+
import { type FieldSortType, FormatEnum, getValue, setValue, 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 { 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
|
|
|
43
28
|
export type TableModelProps<T extends BaseTableRow = { id: string }> = {
|
|
44
29
|
table: TableType;
|
|
45
30
|
projection: ViewProjection;
|
|
46
|
-
sorting?:
|
|
31
|
+
sorting?: FieldSortType[];
|
|
47
32
|
pinnedRows?: { top: number[]; bottom: number[] };
|
|
48
33
|
onInsertRow?: (index?: number) => void;
|
|
49
34
|
onDeleteRows?: (index: number, obj: T[]) => void;
|
|
@@ -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.view?.target, 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(): TableSorting<T> | undefined {
|
|
109
|
+
return this._sorting;
|
|
110
|
+
}
|
|
111
|
+
|
|
121
112
|
public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
|
|
122
113
|
return this._pinnedRows;
|
|
123
114
|
}
|
|
@@ -127,33 +118,29 @@ 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
|
}
|
|
141
124
|
|
|
125
|
+
/** @reactive */
|
|
126
|
+
public get isViewDirty(): boolean {
|
|
127
|
+
return this._sorting.isDirty;
|
|
128
|
+
}
|
|
129
|
+
|
|
142
130
|
//
|
|
143
131
|
// Initialisation
|
|
144
132
|
//
|
|
145
133
|
|
|
146
134
|
protected override async _open() {
|
|
147
135
|
this.initializeColumnMeta();
|
|
148
|
-
this.initializeSorting();
|
|
149
136
|
this.initializeEffects();
|
|
150
|
-
this._selection = new SelectionModel(this.
|
|
137
|
+
this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
|
|
151
138
|
await this._selection.open(this._ctx);
|
|
152
139
|
}
|
|
153
140
|
|
|
154
141
|
private initializeColumnMeta(): void {
|
|
155
142
|
this._columnMeta = computed(() => {
|
|
156
|
-
const fields = this._table.view?.fields ?? [];
|
|
143
|
+
const fields = this._table.view?.target?.fields ?? [];
|
|
157
144
|
const meta = Object.fromEntries(
|
|
158
145
|
fields.map((field, index: number) => [index, { size: field?.size ?? 256, resizeable: true }]),
|
|
159
146
|
);
|
|
@@ -166,39 +153,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
166
153
|
});
|
|
167
154
|
}
|
|
168
155
|
|
|
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
156
|
private initializeEffects(): void {
|
|
200
157
|
const rowOrderWatcher = effect(() => {
|
|
201
|
-
touch(this.
|
|
158
|
+
touch(this._sorting.sortedRows.value);
|
|
202
159
|
this._onRowOrderChanged?.();
|
|
203
160
|
});
|
|
204
161
|
this._ctx.onDispose(rowOrderWatcher);
|
|
@@ -214,8 +171,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
214
171
|
for (let row = start.row; row <= end.row; row++) {
|
|
215
172
|
rowEffects.push(
|
|
216
173
|
effect(() => {
|
|
217
|
-
const obj = this.
|
|
218
|
-
this?._table?.view?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
174
|
+
const obj = this._sorting.sortedRows.value[row];
|
|
175
|
+
this?._table?.view?.target?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
219
176
|
this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
|
|
220
177
|
}),
|
|
221
178
|
);
|
|
@@ -226,181 +183,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
226
183
|
this._ctx.onDispose(rowEffectManager);
|
|
227
184
|
}
|
|
228
185
|
|
|
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
186
|
//
|
|
405
187
|
// Data
|
|
406
188
|
//
|
|
@@ -411,15 +193,15 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
411
193
|
|
|
412
194
|
public getRowCount = (): number => this._rows.value.length;
|
|
413
195
|
|
|
414
|
-
public getColumnCount = (): number => this.table.view?.fields.length ?? 0;
|
|
196
|
+
public getColumnCount = (): number => this.table.view?.target?.fields.length ?? 0;
|
|
415
197
|
|
|
416
198
|
public insertRow = (rowIndex?: number): void => {
|
|
417
|
-
const row = rowIndex !== undefined ? this.
|
|
199
|
+
const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
|
|
418
200
|
this._onInsertRow?.(row);
|
|
419
201
|
};
|
|
420
202
|
|
|
421
203
|
public deleteRow = (rowIndex: number): void => {
|
|
422
|
-
const row = this.
|
|
204
|
+
const row = this._sorting.getDataIndex(rowIndex);
|
|
423
205
|
const obj = this._rows.value[row];
|
|
424
206
|
const objectsToDelete = [];
|
|
425
207
|
|
|
@@ -434,13 +216,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
434
216
|
};
|
|
435
217
|
|
|
436
218
|
public getCellData = ({ col, row }: DxGridPlanePosition): any => {
|
|
437
|
-
const fields = this.table.view?.fields ?? [];
|
|
219
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
438
220
|
if (col < 0 || col >= fields.length) {
|
|
439
221
|
return undefined;
|
|
440
222
|
}
|
|
441
223
|
|
|
442
224
|
const field = fields[col];
|
|
443
|
-
const dataIndex = this.
|
|
225
|
+
const dataIndex = this._sorting.getDataIndex(row);
|
|
444
226
|
const value = getValue(this._rows.value[dataIndex], field.path);
|
|
445
227
|
if (value == null) {
|
|
446
228
|
return '';
|
|
@@ -453,7 +235,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
453
235
|
return ''; // TODO(burdon): Show error.
|
|
454
236
|
}
|
|
455
237
|
|
|
456
|
-
return getValue(value, field.referencePath);
|
|
238
|
+
return getValue(value.target, field.referencePath);
|
|
457
239
|
}
|
|
458
240
|
|
|
459
241
|
default: {
|
|
@@ -463,8 +245,8 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
463
245
|
};
|
|
464
246
|
|
|
465
247
|
public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
|
|
466
|
-
const rowIdx = this.
|
|
467
|
-
const fields = this.table.view?.fields ?? [];
|
|
248
|
+
const rowIdx = this._sorting.getDataIndex(row);
|
|
249
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
468
250
|
if (col < 0 || col >= fields.length) {
|
|
469
251
|
return;
|
|
470
252
|
}
|
|
@@ -473,7 +255,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
473
255
|
const { props } = this._projection.getFieldProjection(field.id);
|
|
474
256
|
switch (props.format) {
|
|
475
257
|
case FormatEnum.Ref: {
|
|
476
|
-
|
|
258
|
+
// TODO(ZaymonFC): This get's called an additional time by the cell editor onBlur, but with the cell editors
|
|
259
|
+
// plain string value. Maybe onBlur should be called with the actual value?
|
|
260
|
+
if (isReactiveObject(value)) {
|
|
261
|
+
setValue(this._rows.value[rowIdx], field.path, makeRef(value));
|
|
262
|
+
}
|
|
477
263
|
break;
|
|
478
264
|
}
|
|
479
265
|
|
|
@@ -492,6 +278,21 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
492
278
|
}
|
|
493
279
|
};
|
|
494
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Updates a cell's value using a transform function.
|
|
283
|
+
* @param {DxGridPlanePosition} position - The position of the cell to update.
|
|
284
|
+
* @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
|
|
285
|
+
*/
|
|
286
|
+
public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
|
|
287
|
+
const dataRow = this._sorting.getDataIndex(row);
|
|
288
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
289
|
+
const field = fields[col];
|
|
290
|
+
|
|
291
|
+
const value = getValue(this._rows.value[dataRow], field.path);
|
|
292
|
+
const updatedValue = update(value);
|
|
293
|
+
setValue(this._rows.value[dataRow], field.path, updatedValue);
|
|
294
|
+
}
|
|
295
|
+
|
|
495
296
|
//
|
|
496
297
|
// Column Operations
|
|
497
298
|
//
|
|
@@ -501,45 +302,18 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
501
302
|
return;
|
|
502
303
|
}
|
|
503
304
|
|
|
504
|
-
const field = this.table.view.fields.find((field) => field.id === fieldId);
|
|
305
|
+
const field = this.table.view.target?.fields.find((field) => field.id === fieldId);
|
|
505
306
|
if (field && this._onDeleteColumn) {
|
|
506
|
-
if (this._sorting.value?.fieldId === fieldId) {
|
|
507
|
-
this.clearSort();
|
|
508
|
-
}
|
|
509
307
|
this._onDeleteColumn(field.id);
|
|
510
308
|
}
|
|
511
309
|
}
|
|
512
310
|
|
|
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
311
|
//
|
|
538
312
|
// Resize
|
|
539
313
|
//
|
|
540
314
|
|
|
541
315
|
public setColumnWidth(columnIndex: number, width: number): void {
|
|
542
|
-
const fields = this.table.view?.fields ?? [];
|
|
316
|
+
const fields = this.table.view?.target?.fields ?? [];
|
|
543
317
|
if (columnIndex < fields.length) {
|
|
544
318
|
const newWidth = Math.max(0, width);
|
|
545
319
|
const field = fields[columnIndex];
|
|
@@ -549,18 +323,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
549
323
|
}
|
|
550
324
|
}
|
|
551
325
|
|
|
552
|
-
//
|
|
553
|
-
// Sorting
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
public setSort(fieldId: string, direction: SortDirection): void {
|
|
557
|
-
this._sorting.value = { fieldId, direction };
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
public clearSort(): void {
|
|
561
|
-
this._sorting.value = undefined;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
326
|
//
|
|
565
327
|
// Pinning
|
|
566
328
|
//
|
|
@@ -574,4 +336,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
574
336
|
this._pinnedRows.top = this._pinnedRows.top.filter((index: number) => index !== rowIndex);
|
|
575
337
|
this._pinnedRows.bottom = this._pinnedRows.bottom.filter((index: number) => index !== rowIndex);
|
|
576
338
|
}
|
|
339
|
+
|
|
340
|
+
//
|
|
341
|
+
// View operations
|
|
342
|
+
//
|
|
343
|
+
public saveView(): void {
|
|
344
|
+
this._sorting.save();
|
|
345
|
+
}
|
|
577
346
|
}
|
|
@@ -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
|
+
};
|