@dxos/react-ui-table 0.7.4 → 0.7.5-labs.a279d8c
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 +1405 -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 +1409 -1051
- 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 +1405 -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.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 +30 -29
- 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 +67 -38
- 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 +223 -0
- package/src/model/table-sorting.ts +201 -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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { signal } from '@preact/signals-core';
|
|
6
|
+
|
|
7
|
+
import { FormatEnum, getValue } from '@dxos/echo-schema';
|
|
8
|
+
import { cellClassesForFieldType, cellClassesForRowSelection, formatForDisplay } from '@dxos/react-ui-form';
|
|
9
|
+
import {
|
|
10
|
+
type DxGridPlane,
|
|
11
|
+
type DxGridPlaneCells,
|
|
12
|
+
toPlaneCellIndex,
|
|
13
|
+
type DxGridPlaneRange,
|
|
14
|
+
type DxGridCellValue,
|
|
15
|
+
} from '@dxos/react-ui-grid';
|
|
16
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
17
|
+
import { VIEW_FIELD_LIMIT, type FieldType } from '@dxos/schema';
|
|
18
|
+
|
|
19
|
+
import { type BaseTableRow, type TableModel } from './table-model';
|
|
20
|
+
import { tableButtons, tableControls } from '../util';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Presentation layer for a table component, handling cell rendering and grid display logic.
|
|
24
|
+
* Manages rendering of table cells, headers, selection columns, and action columns across
|
|
25
|
+
* different grid planes.
|
|
26
|
+
*/
|
|
27
|
+
export class TablePresentation<T extends BaseTableRow = { id: string }> {
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly model: TableModel<T>,
|
|
30
|
+
private readonly _visibleRange = signal<DxGridPlaneRange>({
|
|
31
|
+
start: { row: 0, col: 0 },
|
|
32
|
+
end: { row: 0, col: 0 },
|
|
33
|
+
}),
|
|
34
|
+
) {}
|
|
35
|
+
|
|
36
|
+
public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
|
|
37
|
+
switch (plane) {
|
|
38
|
+
case 'grid':
|
|
39
|
+
this._visibleRange.value = range;
|
|
40
|
+
return this.getMainGridCells(range);
|
|
41
|
+
case 'frozenRowsStart':
|
|
42
|
+
return this.getHeaderCells(range);
|
|
43
|
+
case 'frozenColsStart':
|
|
44
|
+
return this.getSelectionColumnCells(range);
|
|
45
|
+
case 'frozenColsEnd':
|
|
46
|
+
return this.getActionColumnCells(range);
|
|
47
|
+
case 'fixedStartStart':
|
|
48
|
+
return this.getSelectAllCell();
|
|
49
|
+
case 'fixedStartEnd':
|
|
50
|
+
return this.getNewColumnCell();
|
|
51
|
+
default:
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
57
|
+
const cells: DxGridPlaneCells = {};
|
|
58
|
+
const fields = this.model.table.view?.target?.fields ?? [];
|
|
59
|
+
|
|
60
|
+
const addCell = (obj: T, field: FieldType, colIndex: number, displayIndex: number): void => {
|
|
61
|
+
const { props } = this.model.projection.getFieldProjection(field.id);
|
|
62
|
+
|
|
63
|
+
const cell: DxGridCellValue = {
|
|
64
|
+
get value() {
|
|
65
|
+
const value = getValue(obj, field.path);
|
|
66
|
+
if (value == null) {
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
switch (props.format) {
|
|
71
|
+
case FormatEnum.Boolean: {
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
case FormatEnum.Ref: {
|
|
75
|
+
if (!field.referencePath) {
|
|
76
|
+
return ''; // TODO(burdon): Show error.
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return getValue(value.target, field.referencePath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
default: {
|
|
83
|
+
return formatForDisplay({ type: props.type, format: props.format, value });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const classes = [];
|
|
90
|
+
const formatClasses = cellClassesForFieldType({ type: props.type, format: props.format });
|
|
91
|
+
if (formatClasses) {
|
|
92
|
+
classes.push(formatClasses);
|
|
93
|
+
}
|
|
94
|
+
const rowSelectionClasses = cellClassesForRowSelection(this.model.selection.isObjectSelected(obj));
|
|
95
|
+
if (rowSelectionClasses) {
|
|
96
|
+
classes.push(rowSelectionClasses);
|
|
97
|
+
}
|
|
98
|
+
if (classes.length > 0) {
|
|
99
|
+
cell.className = mx(classes.flat());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (props.format === FormatEnum.Ref && props.referenceSchema) {
|
|
103
|
+
const targetObj = getValue(obj, field.path)?.target;
|
|
104
|
+
if (targetObj) {
|
|
105
|
+
cell.accessoryHtml = tableButtons.referencedCell.render({
|
|
106
|
+
targetId: targetObj.id as any as string,
|
|
107
|
+
schemaId: props.referenceSchema,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (props.format === FormatEnum.Boolean) {
|
|
113
|
+
const value = getValue(obj, field.path);
|
|
114
|
+
cell.accessoryHtml = tableControls.switch.render({
|
|
115
|
+
colIndex,
|
|
116
|
+
rowIndex: displayIndex,
|
|
117
|
+
checked: value ?? false,
|
|
118
|
+
});
|
|
119
|
+
cell.readonly = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const idx = toPlaneCellIndex({ col: colIndex, row: displayIndex });
|
|
123
|
+
cells[idx] = cell;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
|
|
127
|
+
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
128
|
+
const field = fields[col];
|
|
129
|
+
if (!field) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
addCell(this.model.rows.value[row], field, col, row);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return cells;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
141
|
+
const cells: DxGridPlaneCells = {};
|
|
142
|
+
const fields = this.model.table.view?.target?.fields ?? [];
|
|
143
|
+
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
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
|
+
|
|
148
|
+
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
149
|
+
// TODO(burdon): Use same logic as form for fallback title.
|
|
150
|
+
value: props.title ?? field.path,
|
|
151
|
+
readonly: true,
|
|
152
|
+
resizeHandle: 'col',
|
|
153
|
+
accessoryHtml: `
|
|
154
|
+
${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
|
|
155
|
+
${tableButtons.columnSettings.render({ fieldId: field.id })}
|
|
156
|
+
`,
|
|
157
|
+
className: '!bg-gridHeader',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return cells;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getSelectionColumnCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
165
|
+
const cells: DxGridPlaneCells = {};
|
|
166
|
+
for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
|
|
167
|
+
const isSelected = this.model.selection.isRowIndexSelected(row);
|
|
168
|
+
const classes = cellClassesForRowSelection(isSelected);
|
|
169
|
+
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
170
|
+
value: '',
|
|
171
|
+
readonly: true,
|
|
172
|
+
className: classes ? mx(classes) : undefined,
|
|
173
|
+
accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return cells;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private getActionColumnCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
181
|
+
const cells: DxGridPlaneCells = {};
|
|
182
|
+
for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
|
|
183
|
+
const isSelected = this.model.selection.isRowIndexSelected(row);
|
|
184
|
+
const classes = cellClassesForRowSelection(isSelected);
|
|
185
|
+
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
186
|
+
value: '',
|
|
187
|
+
readonly: true,
|
|
188
|
+
className: classes ? mx(classes) : undefined,
|
|
189
|
+
accessoryHtml: tableButtons.rowMenu.render({ rowIndex: row }),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return cells;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private getSelectAllCell(): DxGridPlaneCells {
|
|
197
|
+
return {
|
|
198
|
+
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
199
|
+
accessoryHtml: tableControls.checkbox.render({
|
|
200
|
+
rowIndex: 0,
|
|
201
|
+
header: true,
|
|
202
|
+
checked: this.model.selection.allRowsSeleted.value,
|
|
203
|
+
}),
|
|
204
|
+
className: '!bg-gridHeader',
|
|
205
|
+
readonly: true,
|
|
206
|
+
value: '',
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private getNewColumnCell(): DxGridPlaneCells {
|
|
212
|
+
return {
|
|
213
|
+
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
214
|
+
accessoryHtml: tableButtons.addColumn.render({
|
|
215
|
+
disabled: (this.model.table.view?.target?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
|
|
216
|
+
}),
|
|
217
|
+
className: '!bg-gridHeader',
|
|
218
|
+
readonly: true,
|
|
219
|
+
value: '',
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Page } from '@playwright/test';
|
|
5
|
+
import { type Locator, type Page } from '@playwright/test';
|
|
6
6
|
|
|
7
7
|
import { DxGridManager } from '@dxos/lit-grid/testing';
|
|
8
8
|
|
|
@@ -44,6 +44,10 @@ export class TableManager {
|
|
|
44
44
|
await this.page.getByTestId(TABLE_SELECTORS.selection).nth(0).click();
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
public selection(index: number): Locator {
|
|
48
|
+
return this.page.getByTestId(TABLE_SELECTORS.selection).nth(index + 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
47
51
|
public async sortColumn(index: number, direction: 'ascending' | 'descending'): Promise<void> {
|
|
48
52
|
await this.page.getByTestId(TABLE_SELECTORS.columnSettingsButton).nth(index).click();
|
|
49
53
|
await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
|
|
@@ -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,29 @@ test.describe('Table', () => {
|
|
|
38
38
|
await page.close();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
test('
|
|
41
|
+
test('selection', async ({ browser, browserName }) => {
|
|
42
|
+
test.skip(browserName === 'webkit');
|
|
43
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
44
|
+
const table = new TableManager(page);
|
|
45
|
+
|
|
46
|
+
await table.grid.ready();
|
|
47
|
+
// Select All
|
|
48
|
+
await table.toggleSelectAll();
|
|
49
|
+
await expect(table.selection(0)).toBeChecked();
|
|
50
|
+
await expect(table.selection(1)).toBeChecked();
|
|
51
|
+
await expect(table.selection(2)).toBeChecked();
|
|
52
|
+
|
|
53
|
+
// Deselect All
|
|
54
|
+
await table.toggleSelectAll();
|
|
55
|
+
await expect(table.selection(0)).not.toBeChecked();
|
|
56
|
+
await expect(table.selection(1)).not.toBeChecked();
|
|
57
|
+
await expect(table.selection(2)).not.toBeChecked();
|
|
58
|
+
|
|
59
|
+
await page.close();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('delete row', async ({ browser, browserName }) => {
|
|
63
|
+
test.skip(browserName === 'webkit');
|
|
42
64
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
43
65
|
const table = new TableManager(page);
|
|
44
66
|
|
|
@@ -48,7 +70,8 @@ test.describe('Table', () => {
|
|
|
48
70
|
await page.close();
|
|
49
71
|
});
|
|
50
72
|
|
|
51
|
-
test('delete row--select all', async ({ browser }) => {
|
|
73
|
+
test('delete row--select all', async ({ browser, browserName }) => {
|
|
74
|
+
test.skip(browserName === 'webkit');
|
|
52
75
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
53
76
|
const table = new TableManager(page);
|
|
54
77
|
|
|
@@ -64,11 +87,15 @@ test.describe('Table', () => {
|
|
|
64
87
|
await page.close();
|
|
65
88
|
});
|
|
66
89
|
|
|
67
|
-
test('delete column', async ({ browser }) => {
|
|
90
|
+
test('delete column', async ({ browser, browserName }) => {
|
|
91
|
+
test.skip(browserName === 'webkit');
|
|
68
92
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
69
93
|
const table = new TableManager(page);
|
|
70
94
|
|
|
71
95
|
await table.grid.ready();
|
|
96
|
+
await table.deleteColumn(0);
|
|
97
|
+
await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(40);
|
|
98
|
+
|
|
72
99
|
await table.deleteColumn(0);
|
|
73
100
|
await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(30);
|
|
74
101
|
|
|
@@ -85,8 +112,10 @@ test.describe('Table', () => {
|
|
|
85
112
|
await page.close();
|
|
86
113
|
});
|
|
87
114
|
|
|
88
|
-
// Rest of add column test remains the same as it's a more complex flow
|
|
89
|
-
|
|
115
|
+
// Rest of add column test remains the same as it's a more complex flow.
|
|
116
|
+
// TODO(ZaymonFC): Restore this after fixing format selection.
|
|
117
|
+
test('add column', async ({ browser, browserName }) => {
|
|
118
|
+
test.skip(browserName === 'webkit');
|
|
90
119
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
91
120
|
const table = new TableManager(page);
|
|
92
121
|
|
|
@@ -98,4 +127,60 @@ test.describe('Table', () => {
|
|
|
98
127
|
await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
|
|
99
128
|
await page.close();
|
|
100
129
|
});
|
|
130
|
+
|
|
131
|
+
test('reference > reference / create new object', async ({ browser, browserName }) => {
|
|
132
|
+
test.skip(browserName === 'webkit');
|
|
133
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
134
|
+
const table = new TableManager(page);
|
|
135
|
+
|
|
136
|
+
await table.grid.ready();
|
|
137
|
+
|
|
138
|
+
// Reference existing object.
|
|
139
|
+
// Scroll over to the manager column.
|
|
140
|
+
await table.grid.panByWheel(10000, 0);
|
|
141
|
+
|
|
142
|
+
await table.grid.cell(4, 0, 'grid').click();
|
|
143
|
+
await page.keyboard.press('A');
|
|
144
|
+
await page.keyboard.press('N');
|
|
145
|
+
await page.getByRole('option', { name: 'Anita Mayer' }).click();
|
|
146
|
+
|
|
147
|
+
// Assert that the value is shown in the cell.
|
|
148
|
+
await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toBeVisible();
|
|
149
|
+
|
|
150
|
+
// Create new object.
|
|
151
|
+
await table.grid.cell(4, 1, 'grid').click();
|
|
152
|
+
await page.keyboard.type('t');
|
|
153
|
+
await page.keyboard.type('e');
|
|
154
|
+
await page.keyboard.type('s');
|
|
155
|
+
await page.keyboard.type('t');
|
|
156
|
+
await page.getByText('Create new object', { exact: false }).click();
|
|
157
|
+
await page.getByTestId('save-button').click();
|
|
158
|
+
|
|
159
|
+
// Scroll to the left.
|
|
160
|
+
await page.getByRole('gridcell', { name: 'test' }).click();
|
|
161
|
+
|
|
162
|
+
await page.close();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('test toggles', async ({ browser, browserName }) => {
|
|
166
|
+
test.skip(browserName === 'webkit');
|
|
167
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
168
|
+
const table = new TableManager(page);
|
|
169
|
+
|
|
170
|
+
await table.grid.ready();
|
|
171
|
+
|
|
172
|
+
await page.getByTestId('table-switch').first().click();
|
|
173
|
+
await page.getByTestId('table-switch').nth(7).click();
|
|
174
|
+
|
|
175
|
+
// Test that checks are durable in the data model by sorting.
|
|
176
|
+
await table.sortColumn(1, 'descending');
|
|
177
|
+
|
|
178
|
+
// Assert the first two switch checkboxes are checked.
|
|
179
|
+
await expect(page.getByTestId('table-switch').first()).toBeChecked();
|
|
180
|
+
await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
|
|
181
|
+
await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Anita Mayer');
|
|
182
|
+
await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Uwe Øvergård');
|
|
183
|
+
|
|
184
|
+
await page.close();
|
|
185
|
+
});
|
|
101
186
|
});
|
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
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { ThreadType } from '@dxos/plugin-space';
|
|
5
|
+
import { Ref, ObjectId, S, TypedObject, Expando } from '@dxos/echo-schema';
|
|
6
|
+
// import { ThreadType } from '@dxos/plugin-space/types';
|
|
7
7
|
import { ViewType } from '@dxos/schema';
|
|
8
8
|
|
|
9
9
|
export const TableSchema = S.Struct({
|
|
10
10
|
id: ObjectId, // TODO(burdon): Where should this be?
|
|
11
11
|
name: S.optional(S.String),
|
|
12
|
-
view: S.optional(
|
|
12
|
+
view: S.optional(Ref(ViewType)),
|
|
13
13
|
// TODO(burdon): Should not import from plugin. Either factor out type or use reverse deps when supported.
|
|
14
|
-
threads: S.optional(S.Array(
|
|
14
|
+
threads: S.optional(S.Array(Ref(Expando /* ThreadType */))), // TODO(dmaretskyi): Breaks edge because plugin-space depends on react-client.
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
// type TableType = S.Schema.Type<typeof TableSchema>;
|
package/src/util/index.ts
CHANGED