@dxos/react-ui-table 0.7.5-main.9d26e3a → 0.7.5-main.9d2a38b
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-AT7O4RAD.mjs → chunk-T7CTRLHA.mjs} +2 -2
- package/dist/lib/browser/{chunk-AT7O4RAD.mjs.map → chunk-T7CTRLHA.mjs.map} +1 -1
- package/dist/lib/browser/index.mjs +425 -280
- 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-YSBXNBDJ.cjs → chunk-XQRKYO7Q.cjs} +6 -6
- package/dist/lib/node/chunk-XQRKYO7Q.cjs.map +7 -0
- package/dist/lib/node/index.cjs +417 -266
- 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-6OH2NHUX.mjs → chunk-6HE6MLBK.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-6OH2NHUX.mjs.map → chunk-6HE6MLBK.mjs.map} +1 -1
- package/dist/lib/node-esm/index.mjs +425 -280
- 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/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/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 +1 -1
- package/dist/types/src/model/table-model.d.ts +11 -19
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- 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/translations.d.ts +2 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/table.d.ts +3 -3
- package/dist/types/src/types/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 +2 -1
- package/dist/types/src/util/table.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +30 -29
- package/src/components/Table/ColumnActionsMenu.tsx +4 -4
- package/src/components/Table/Table.stories.tsx +33 -21
- package/src/components/Table/Table.tsx +4 -0
- 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/model/table-model.test.ts +0 -5
- package/src/model/table-model.ts +40 -98
- package/src/model/table-presentation.ts +12 -3
- package/src/model/table-sorting.ts +201 -0
- package/src/playwright/smoke.spec.ts +18 -11
- package/src/translations.ts +2 -1
- package/src/types/table.ts +1 -1
- package/src/util/table-buttons.ts +33 -3
- package/src/util/table.ts +78 -52
- package/dist/lib/node/chunk-YSBXNBDJ.cjs.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,14 +3,13 @@
|
|
|
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';
|
|
11
10
|
import { isReactiveObject, makeRef } from '@dxos/live-object';
|
|
12
11
|
import { PublicKey } from '@dxos/react-client';
|
|
13
|
-
import {
|
|
12
|
+
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
14
13
|
import {
|
|
15
14
|
type DxGridAxisMeta,
|
|
16
15
|
type DxGridPlaneRange,
|
|
@@ -20,18 +19,16 @@ import {
|
|
|
20
19
|
import { type ViewProjection } from '@dxos/schema';
|
|
21
20
|
|
|
22
21
|
import { SelectionModel } from './selection-model';
|
|
22
|
+
import { TableSorting } from './table-sorting';
|
|
23
23
|
import { type TableType } from '../types';
|
|
24
24
|
import { touch } from '../util';
|
|
25
25
|
|
|
26
|
-
export type SortDirection = 'asc' | 'desc';
|
|
27
|
-
export type SortConfig = { fieldId: string; direction: SortDirection };
|
|
28
|
-
|
|
29
26
|
export type BaseTableRow = Record<JsonProp, any> & { id: string };
|
|
30
27
|
|
|
31
28
|
export type TableModelProps<T extends BaseTableRow = { id: string }> = {
|
|
32
29
|
table: TableType;
|
|
33
30
|
projection: ViewProjection;
|
|
34
|
-
sorting?:
|
|
31
|
+
sorting?: FieldSortType[];
|
|
35
32
|
pinnedRows?: { top: number[]; bottom: number[] };
|
|
36
33
|
onInsertRow?: (index?: number) => void;
|
|
37
34
|
onDeleteRows?: (index: number, obj: T[]) => void;
|
|
@@ -51,25 +48,16 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
51
48
|
end: { row: 0, col: 0 },
|
|
52
49
|
});
|
|
53
50
|
|
|
54
|
-
/**
|
|
55
|
-
* Maps display indices to data indices.
|
|
56
|
-
* Used for translating between sorted/displayed order and original data order.
|
|
57
|
-
* Keys are display indices, values are corresponding data indices.
|
|
58
|
-
*/
|
|
59
|
-
private readonly _displayToDataIndex: Map<number, number> = new Map();
|
|
60
|
-
|
|
61
51
|
private readonly _onInsertRow?: TableModelProps<T>['onInsertRow'];
|
|
62
52
|
private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
|
|
63
53
|
private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
|
|
64
54
|
private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
|
|
65
55
|
private readonly _onRowOrderChanged?: TableModelProps<T>['onRowOrderChanged'];
|
|
66
56
|
|
|
67
|
-
private readonly
|
|
68
|
-
private
|
|
69
|
-
private _rows = signal<T[]>([]);
|
|
57
|
+
private readonly _rows = signal<T[]>([]);
|
|
58
|
+
private readonly _sorting: TableSorting<T>;
|
|
70
59
|
|
|
71
60
|
private _pinnedRows: NonNullable<TableModelProps<T>['pinnedRows']>;
|
|
72
|
-
|
|
73
61
|
private _selection!: SelectionModel<T>;
|
|
74
62
|
private _columnMeta?: ReadonlySignal<DxGridAxisMeta>;
|
|
75
63
|
|
|
@@ -87,8 +75,13 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
87
75
|
super();
|
|
88
76
|
this._table = table;
|
|
89
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
|
+
}
|
|
90
84
|
|
|
91
|
-
this._sorting.value = sorting.at(0);
|
|
92
85
|
this._pinnedRows = pinnedRows;
|
|
93
86
|
this._onInsertRow = onInsertRow;
|
|
94
87
|
this._onDeleteRows = onDeleteRows;
|
|
@@ -106,7 +99,14 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
106
99
|
}
|
|
107
100
|
|
|
108
101
|
public get rows(): ReadonlySignal<T[]> {
|
|
109
|
-
return this.
|
|
102
|
+
return this._sorting.sortedRows;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @reactive
|
|
107
|
+
*/
|
|
108
|
+
public get sorting(): TableSorting<T> | undefined {
|
|
109
|
+
return this._sorting;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
public get pinnedRows(): NonNullable<TableModelProps<T>['pinnedRows']> {
|
|
@@ -118,23 +118,23 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
118
118
|
return this._columnMeta;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
public get sorting(): SortConfig | undefined {
|
|
122
|
-
return this._sorting.value;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
121
|
public get selection() {
|
|
126
122
|
return this._selection;
|
|
127
123
|
}
|
|
128
124
|
|
|
125
|
+
/** @reactive */
|
|
126
|
+
public get isViewDirty(): boolean {
|
|
127
|
+
return this._sorting.isDirty;
|
|
128
|
+
}
|
|
129
|
+
|
|
129
130
|
//
|
|
130
131
|
// Initialisation
|
|
131
132
|
//
|
|
132
133
|
|
|
133
134
|
protected override async _open() {
|
|
134
135
|
this.initializeColumnMeta();
|
|
135
|
-
this.initializeSorting();
|
|
136
136
|
this.initializeEffects();
|
|
137
|
-
this._selection = new SelectionModel(this.
|
|
137
|
+
this._selection = new SelectionModel(this._sorting.sortedRows, () => this._onRowOrderChanged?.());
|
|
138
138
|
await this._selection.open(this._ctx);
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -153,59 +153,9 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
153
153
|
});
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
private initializeSorting(): void {
|
|
157
|
-
this._sortedRows = computed(() => {
|
|
158
|
-
this._displayToDataIndex.clear();
|
|
159
|
-
const sort = this._sorting.value;
|
|
160
|
-
if (!sort) {
|
|
161
|
-
return this._rows.value;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const field = this._table.view?.target?.fields.find((field) => field.id === sort.fieldId);
|
|
165
|
-
if (!field) {
|
|
166
|
-
return this._rows.value;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const { props } = this._projection.getFieldProjection(field.id);
|
|
170
|
-
|
|
171
|
-
const dataWithIndices = this._rows.value.map((item, index) => {
|
|
172
|
-
const rawValue = getValue(item, field.path);
|
|
173
|
-
let sortValue = rawValue;
|
|
174
|
-
|
|
175
|
-
if (props.type.startsWith('boolean')) {
|
|
176
|
-
sortValue = !!rawValue; // Coerce null/undefined/false to false for booleans
|
|
177
|
-
} else if (rawValue != null) {
|
|
178
|
-
if (props.format === FormatEnum.Ref && field.referencePath) {
|
|
179
|
-
sortValue = getValue(rawValue, field.referencePath);
|
|
180
|
-
} else {
|
|
181
|
-
sortValue = formatForDisplay({ type: props.type, format: props.format, value: rawValue });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
item,
|
|
187
|
-
index,
|
|
188
|
-
value: sortValue,
|
|
189
|
-
isEmpty: !props.type.startsWith('boolean') && (sortValue == null || sortValue === ''),
|
|
190
|
-
};
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
const sorted = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
|
|
194
|
-
|
|
195
|
-
for (let displayIndex = 0; displayIndex < sorted.length; displayIndex++) {
|
|
196
|
-
const { index: dataIndex } = sorted[displayIndex];
|
|
197
|
-
if (displayIndex !== dataIndex) {
|
|
198
|
-
this._displayToDataIndex.set(displayIndex, dataIndex);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return sorted.map(({ item }) => item);
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
156
|
private initializeEffects(): void {
|
|
207
157
|
const rowOrderWatcher = effect(() => {
|
|
208
|
-
touch(this.
|
|
158
|
+
touch(this._sorting.sortedRows.value);
|
|
209
159
|
this._onRowOrderChanged?.();
|
|
210
160
|
});
|
|
211
161
|
this._ctx.onDispose(rowOrderWatcher);
|
|
@@ -221,7 +171,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
221
171
|
for (let row = start.row; row <= end.row; row++) {
|
|
222
172
|
rowEffects.push(
|
|
223
173
|
effect(() => {
|
|
224
|
-
const obj = this.
|
|
174
|
+
const obj = this._sorting.sortedRows.value[row];
|
|
225
175
|
this?._table?.view?.target?.fields.forEach((field) => touch(getValue(obj, field.path)));
|
|
226
176
|
this._onCellUpdate?.({ row, col: start.col, plane: 'grid' });
|
|
227
177
|
}),
|
|
@@ -246,12 +196,12 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
246
196
|
public getColumnCount = (): number => this.table.view?.target?.fields.length ?? 0;
|
|
247
197
|
|
|
248
198
|
public insertRow = (rowIndex?: number): void => {
|
|
249
|
-
const row = rowIndex !== undefined ? this.
|
|
199
|
+
const row = rowIndex !== undefined ? this._sorting.getDataIndex(rowIndex) : this._rows.value.length;
|
|
250
200
|
this._onInsertRow?.(row);
|
|
251
201
|
};
|
|
252
202
|
|
|
253
203
|
public deleteRow = (rowIndex: number): void => {
|
|
254
|
-
const row = this.
|
|
204
|
+
const row = this._sorting.getDataIndex(rowIndex);
|
|
255
205
|
const obj = this._rows.value[row];
|
|
256
206
|
const objectsToDelete = [];
|
|
257
207
|
|
|
@@ -272,7 +222,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
272
222
|
}
|
|
273
223
|
|
|
274
224
|
const field = fields[col];
|
|
275
|
-
const dataIndex = this.
|
|
225
|
+
const dataIndex = this._sorting.getDataIndex(row);
|
|
276
226
|
const value = getValue(this._rows.value[dataIndex], field.path);
|
|
277
227
|
if (value == null) {
|
|
278
228
|
return '';
|
|
@@ -295,7 +245,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
295
245
|
};
|
|
296
246
|
|
|
297
247
|
public setCellData = ({ col, row }: DxGridPlanePosition, value: any): void => {
|
|
298
|
-
const rowIdx = this.
|
|
248
|
+
const rowIdx = this._sorting.getDataIndex(row);
|
|
299
249
|
const fields = this.table.view?.target?.fields ?? [];
|
|
300
250
|
if (col < 0 || col >= fields.length) {
|
|
301
251
|
return;
|
|
@@ -334,7 +284,7 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
334
284
|
* @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
|
|
335
285
|
*/
|
|
336
286
|
public updateCellData({ col, row }: DxGridPlanePosition, update: (value: any) => any): void {
|
|
337
|
-
const dataRow = this.
|
|
287
|
+
const dataRow = this._sorting.getDataIndex(row);
|
|
338
288
|
const fields = this.table.view?.target?.fields ?? [];
|
|
339
289
|
const field = fields[col];
|
|
340
290
|
|
|
@@ -354,9 +304,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
354
304
|
|
|
355
305
|
const field = this.table.view.target?.fields.find((field) => field.id === fieldId);
|
|
356
306
|
if (field && this._onDeleteColumn) {
|
|
357
|
-
if (this._sorting.value?.fieldId === fieldId) {
|
|
358
|
-
this.clearSort();
|
|
359
|
-
}
|
|
360
307
|
this._onDeleteColumn(field.id);
|
|
361
308
|
}
|
|
362
309
|
}
|
|
@@ -376,18 +323,6 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
376
323
|
}
|
|
377
324
|
}
|
|
378
325
|
|
|
379
|
-
//
|
|
380
|
-
// Sorting
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
public setSort(fieldId: string, direction: SortDirection): void {
|
|
384
|
-
this._sorting.value = { fieldId, direction };
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
public clearSort(): void {
|
|
388
|
-
this._sorting.value = undefined;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
326
|
//
|
|
392
327
|
// Pinning
|
|
393
328
|
//
|
|
@@ -401,4 +336,11 @@ export class TableModel<T extends BaseTableRow = { id: string }> extends Resourc
|
|
|
401
336
|
this._pinnedRows.top = this._pinnedRows.top.filter((index: number) => index !== rowIndex);
|
|
402
337
|
this._pinnedRows.bottom = this._pinnedRows.bottom.filter((index: number) => index !== rowIndex);
|
|
403
338
|
}
|
|
339
|
+
|
|
340
|
+
//
|
|
341
|
+
// View operations
|
|
342
|
+
//
|
|
343
|
+
public saveView(): void {
|
|
344
|
+
this._sorting.save();
|
|
345
|
+
}
|
|
404
346
|
}
|
|
@@ -142,12 +142,19 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
|
|
|
142
142
|
const fields = this.model.table.view?.target?.fields ?? [];
|
|
143
143
|
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
144
144
|
const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
|
|
145
|
+
const sorting = this.model.sorting?.sorting;
|
|
146
|
+
const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
|
|
147
|
+
|
|
145
148
|
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
146
149
|
// TODO(burdon): Use same logic as form for fallback title.
|
|
147
150
|
value: props.title ?? field.path,
|
|
148
151
|
readonly: true,
|
|
149
152
|
resizeHandle: 'col',
|
|
150
|
-
accessoryHtml:
|
|
153
|
+
accessoryHtml: `
|
|
154
|
+
${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
|
|
155
|
+
${tableButtons.columnSettings.render({ fieldId: field.id })}
|
|
156
|
+
`,
|
|
157
|
+
className: '!bg-gridHeader',
|
|
151
158
|
};
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -189,13 +196,14 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
|
|
|
189
196
|
private getSelectAllCell(): DxGridPlaneCells {
|
|
190
197
|
return {
|
|
191
198
|
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
192
|
-
value: '',
|
|
193
199
|
accessoryHtml: tableControls.checkbox.render({
|
|
194
200
|
rowIndex: 0,
|
|
195
201
|
header: true,
|
|
196
202
|
checked: this.model.selection.allRowsSeleted.value,
|
|
197
203
|
}),
|
|
204
|
+
className: '!bg-gridHeader',
|
|
198
205
|
readonly: true,
|
|
206
|
+
value: '',
|
|
199
207
|
},
|
|
200
208
|
};
|
|
201
209
|
}
|
|
@@ -203,11 +211,12 @@ export class TablePresentation<T extends BaseTableRow = { id: string }> {
|
|
|
203
211
|
private getNewColumnCell(): DxGridPlaneCells {
|
|
204
212
|
return {
|
|
205
213
|
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
206
|
-
value: '',
|
|
207
214
|
accessoryHtml: tableButtons.addColumn.render({
|
|
208
215
|
disabled: (this.model.table.view?.target?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
|
|
209
216
|
}),
|
|
217
|
+
className: '!bg-gridHeader',
|
|
210
218
|
readonly: true,
|
|
219
|
+
value: '',
|
|
211
220
|
},
|
|
212
221
|
};
|
|
213
222
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { computed, signal, type Signal, type ReadonlySignal } from '@preact/signals-core';
|
|
6
|
+
import orderBy from 'lodash.orderby';
|
|
7
|
+
|
|
8
|
+
import { getValue, FormatEnum, TypeEnum, type SortDirectionType, type FieldSortType } from '@dxos/echo-schema';
|
|
9
|
+
import { formatForDisplay } from '@dxos/react-ui-form';
|
|
10
|
+
import type { PropertyType, FieldType, ViewProjection, ViewType } from '@dxos/schema';
|
|
11
|
+
|
|
12
|
+
import { type BaseTableRow } from './table-model';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents the local sort state.
|
|
16
|
+
* - undefined: No local sort set, fallback to view sort.
|
|
17
|
+
* - cleared: Sort explicitly cleared by user.
|
|
18
|
+
* - active: Active local sort with sort configuration.
|
|
19
|
+
*/
|
|
20
|
+
type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } | undefined;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Manages table sorting functionality through display and data index mapping.
|
|
24
|
+
*
|
|
25
|
+
* When a table is sorted, rows visually reorder but we need to maintain references
|
|
26
|
+
* to the original data positions. This is handled through two types of indices:
|
|
27
|
+
*
|
|
28
|
+
* - Display indices: The visual positions after sorting
|
|
29
|
+
* - Data indices: The original positions in the source data
|
|
30
|
+
*
|
|
31
|
+
* The _displayToDataIndex map maintains these relationships, enabling lookups
|
|
32
|
+
* between visual and source positions.
|
|
33
|
+
*
|
|
34
|
+
* Example mapping between display and data indices:
|
|
35
|
+
*
|
|
36
|
+
* Original Data: Sorted Display: Mapping: (Display → Data)
|
|
37
|
+
* ┌──────────────┐ ┌──────────────┐ ┌────────┐
|
|
38
|
+
* │ 0: "Banana" │ => │ 0: "Apple" │ => │ 0 => 1 │
|
|
39
|
+
* │ 1: "Apple" │ => │ 1: "Banana" │ => │ 1 => 0 │
|
|
40
|
+
* │ 2: "Cherry" │ => │ 2: "Cherry" │ => │ 2 => 2 │
|
|
41
|
+
* └──────────────┘ └──────────────┘ └────────┘
|
|
42
|
+
*/
|
|
43
|
+
export class TableSorting<T extends BaseTableRow> {
|
|
44
|
+
private readonly _displayToDataIndex = new Map<number, number>();
|
|
45
|
+
private readonly _rows: Signal<T[]>;
|
|
46
|
+
private readonly _localSort = signal<LocalSort>(undefined);
|
|
47
|
+
private readonly _isDirty: ReadonlySignal<boolean>;
|
|
48
|
+
public readonly sortedRows: ReadonlySignal<T[]>;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
rows: Signal<T[]>,
|
|
52
|
+
private readonly _view: ViewType | undefined,
|
|
53
|
+
private readonly _projection: ViewProjection,
|
|
54
|
+
) {
|
|
55
|
+
this._rows = rows;
|
|
56
|
+
this._isDirty = computed(() => {
|
|
57
|
+
const local = this._localSort.value;
|
|
58
|
+
const viewSort = this._view?.query.sort?.[0];
|
|
59
|
+
if (local?.type === 'cleared') {
|
|
60
|
+
return viewSort !== undefined;
|
|
61
|
+
}
|
|
62
|
+
if (local?.type === 'active') {
|
|
63
|
+
if (!viewSort) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return local.sort.fieldId !== viewSort.fieldId || local.sort.direction !== viewSort.direction;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
this.sortedRows = this.initialiseSortedRows();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @reactive
|
|
75
|
+
* @returns Local sort if present, falls back to view sort.
|
|
76
|
+
*/
|
|
77
|
+
public get sorting(): FieldSortType | undefined {
|
|
78
|
+
const local = this._localSort.value;
|
|
79
|
+
if (local?.type === 'cleared') {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
if (local?.type === 'active') {
|
|
83
|
+
return local.sort;
|
|
84
|
+
}
|
|
85
|
+
return this._view?.query.sort?.[0];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @reactive
|
|
90
|
+
* @returns Whether local sort differs from view sort.
|
|
91
|
+
*/
|
|
92
|
+
public get isDirty(): boolean {
|
|
93
|
+
return this._isDirty.value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public getDataIndex(displayIndex: number): number {
|
|
97
|
+
return this._displayToDataIndex.get(displayIndex) ?? displayIndex;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public setSort(fieldId: string, direction: SortDirectionType): void {
|
|
101
|
+
this._localSort.value = {
|
|
102
|
+
type: 'active',
|
|
103
|
+
sort: { fieldId, direction },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public toggleSort(fieldId: string): void {
|
|
108
|
+
if (!this._view || !this.sorting || this.sorting.fieldId !== fieldId) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
switch (this.sorting.direction) {
|
|
113
|
+
case 'asc': {
|
|
114
|
+
this.setSort(fieldId, 'desc');
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'desc': {
|
|
118
|
+
this.setSort(fieldId, 'asc');
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public clearSort(): void {
|
|
125
|
+
this._localSort.value = { type: 'cleared' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public save(): void {
|
|
129
|
+
if (this._view && this._localSort.value !== undefined) {
|
|
130
|
+
if (this._localSort.value.type === 'active') {
|
|
131
|
+
this._view.query.sort = [this._localSort.value.sort];
|
|
132
|
+
} else {
|
|
133
|
+
this._view.query.sort = [];
|
|
134
|
+
}
|
|
135
|
+
this._localSort.value = undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//
|
|
140
|
+
// Initialisation.
|
|
141
|
+
//
|
|
142
|
+
|
|
143
|
+
private initialiseSortedRows(): ReadonlySignal<T[]> {
|
|
144
|
+
return computed(() => {
|
|
145
|
+
this._displayToDataIndex.clear();
|
|
146
|
+
const sort = this.sorting;
|
|
147
|
+
if (!sort || !this._view) {
|
|
148
|
+
return this._rows.value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const field = this._view.fields.find((f) => f.id === sort.fieldId);
|
|
152
|
+
if (!field) {
|
|
153
|
+
return this._rows.value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const { props } = this._projection.getFieldProjection(field.id);
|
|
157
|
+
|
|
158
|
+
const dataWithIndices = this._rows.value.map((item, index) => {
|
|
159
|
+
const sortValue = this.getSortValue(props, field, item);
|
|
160
|
+
const isEmpty = !(props.type === TypeEnum.Boolean) && (sortValue == null || sortValue === '');
|
|
161
|
+
|
|
162
|
+
return { item, index, value: sortValue, isEmpty };
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const ordered = orderBy(dataWithIndices, ['isEmpty', 'value'], ['asc', sort.direction]);
|
|
166
|
+
|
|
167
|
+
ordered.forEach(({ index: dataIndex }, displayIndex) => {
|
|
168
|
+
if (displayIndex !== dataIndex) {
|
|
169
|
+
this._displayToDataIndex.set(displayIndex, dataIndex);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return ordered.map(({ item }) => item);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private getSortValue(props: PropertyType, field: FieldType, item: T) {
|
|
178
|
+
const { type, format } = props;
|
|
179
|
+
const value = getValue(item, field.path);
|
|
180
|
+
|
|
181
|
+
if (type === TypeEnum.Boolean) {
|
|
182
|
+
return !!value; // Coerce null/undefined to false for booleans.
|
|
183
|
+
}
|
|
184
|
+
if (type === TypeEnum.Number) {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
if (value == null) {
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// TODO(ZaymonFC): Should we format this returned value for display based on the referenced prop format?
|
|
192
|
+
// Right now we're going to be sorting based on the raw values.
|
|
193
|
+
// Maybe we need to recurse this function with the referenced prop format and field?
|
|
194
|
+
if (format === FormatEnum.Ref && field.referencePath) {
|
|
195
|
+
const refValue = getValue(value.target, field.referencePath);
|
|
196
|
+
return refValue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return formatForDisplay({ type, format, value });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -13,17 +13,17 @@ const storyUrl = `http://localhost:9009/iframe.html?id=${storyId}&viewMode=story
|
|
|
13
13
|
|
|
14
14
|
// NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
|
|
15
15
|
test.describe('Table', () => {
|
|
16
|
-
test('Loads', async ({ browser }) => {
|
|
16
|
+
test('Loads', async ({ browser, browserName }) => {
|
|
17
|
+
test.skip(browserName === 'webkit');
|
|
17
18
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
18
19
|
const table = new TableManager(page);
|
|
19
20
|
|
|
20
|
-
// (name, email, salary, manager) * 10 rows.
|
|
21
21
|
await table.grid.ready();
|
|
22
|
-
await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(40);
|
|
23
22
|
await page.close();
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
test('sort', async ({ browser }) => {
|
|
25
|
+
test('sort', async ({ browser, browserName }) => {
|
|
26
|
+
test.skip(browserName === 'webkit');
|
|
27
27
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
28
28
|
const table = new TableManager(page);
|
|
29
29
|
|
|
@@ -38,7 +38,8 @@ test.describe('Table', () => {
|
|
|
38
38
|
await page.close();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('selection', async ({ browser }) => {
|
|
41
|
+
test('selection', async ({ browser, browserName }) => {
|
|
42
|
+
test.skip(browserName === 'webkit');
|
|
42
43
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
43
44
|
const table = new TableManager(page);
|
|
44
45
|
|
|
@@ -58,7 +59,8 @@ test.describe('Table', () => {
|
|
|
58
59
|
await page.close();
|
|
59
60
|
});
|
|
60
61
|
|
|
61
|
-
test('delete row', async ({ browser }) => {
|
|
62
|
+
test('delete row', async ({ browser, browserName }) => {
|
|
63
|
+
test.skip(browserName === 'webkit');
|
|
62
64
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
63
65
|
const table = new TableManager(page);
|
|
64
66
|
|
|
@@ -68,7 +70,8 @@ test.describe('Table', () => {
|
|
|
68
70
|
await page.close();
|
|
69
71
|
});
|
|
70
72
|
|
|
71
|
-
test('delete row--select all', async ({ browser }) => {
|
|
73
|
+
test('delete row--select all', async ({ browser, browserName }) => {
|
|
74
|
+
test.skip(browserName === 'webkit');
|
|
72
75
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
73
76
|
const table = new TableManager(page);
|
|
74
77
|
|
|
@@ -84,7 +87,8 @@ test.describe('Table', () => {
|
|
|
84
87
|
await page.close();
|
|
85
88
|
});
|
|
86
89
|
|
|
87
|
-
test('delete column', async ({ browser }) => {
|
|
90
|
+
test('delete column', async ({ browser, browserName }) => {
|
|
91
|
+
test.skip(browserName === 'webkit');
|
|
88
92
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
89
93
|
const table = new TableManager(page);
|
|
90
94
|
|
|
@@ -110,7 +114,8 @@ test.describe('Table', () => {
|
|
|
110
114
|
|
|
111
115
|
// Rest of add column test remains the same as it's a more complex flow.
|
|
112
116
|
// TODO(ZaymonFC): Restore this after fixing format selection.
|
|
113
|
-
test('add column', async ({ browser }) => {
|
|
117
|
+
test('add column', async ({ browser, browserName }) => {
|
|
118
|
+
test.skip(browserName === 'webkit');
|
|
114
119
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
115
120
|
const table = new TableManager(page);
|
|
116
121
|
|
|
@@ -123,7 +128,8 @@ test.describe('Table', () => {
|
|
|
123
128
|
await page.close();
|
|
124
129
|
});
|
|
125
130
|
|
|
126
|
-
test('reference > reference / create new object', async ({ browser }) => {
|
|
131
|
+
test('reference > reference / create new object', async ({ browser, browserName }) => {
|
|
132
|
+
test.skip(browserName === 'webkit');
|
|
127
133
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
128
134
|
const table = new TableManager(page);
|
|
129
135
|
|
|
@@ -156,7 +162,8 @@ test.describe('Table', () => {
|
|
|
156
162
|
await page.close();
|
|
157
163
|
});
|
|
158
164
|
|
|
159
|
-
test('test toggles', async ({ browser }) => {
|
|
165
|
+
test('test toggles', async ({ browser, browserName }) => {
|
|
166
|
+
test.skip(browserName === 'webkit');
|
|
160
167
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
161
168
|
const table = new TableManager(page);
|
|
162
169
|
|
package/src/translations.ts
CHANGED
|
@@ -16,7 +16,7 @@ export default [
|
|
|
16
16
|
'new schema': 'New schema',
|
|
17
17
|
'continue label': 'Continue',
|
|
18
18
|
'create stack section label': 'Create table',
|
|
19
|
-
'add row': 'Add row',
|
|
19
|
+
'add row label': 'Add row',
|
|
20
20
|
'create comment': 'Create thread',
|
|
21
21
|
'column action sort descending': 'Sort Descending',
|
|
22
22
|
'column action sort ascending': 'Sort Ascending',
|
|
@@ -28,6 +28,7 @@ export default [
|
|
|
28
28
|
'column deleted label': 'Column deleted',
|
|
29
29
|
'new column button label': 'Create column',
|
|
30
30
|
'create new object label': 'Create new object “{{text}}”',
|
|
31
|
+
'save view label': 'Save changes to view',
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
},
|
package/src/types/table.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';
|
|
6
|
-
import { ThreadType } from '@dxos/plugin-space';
|
|
6
|
+
import { ThreadType } from '@dxos/plugin-space/types';
|
|
7
7
|
import { ViewType } from '@dxos/schema';
|
|
8
8
|
|
|
9
9
|
export const TableSchema = S.Struct({
|