@dxos/react-ui-table 0.7.4 → 0.7.5-main.9d26e3a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-IYCSJDMB.mjs → chunk-AT7O4RAD.mjs} +4 -4
- package/dist/lib/browser/{chunk-IYCSJDMB.mjs.map → chunk-AT7O4RAD.mjs.map} +2 -2
- package/dist/lib/browser/index.mjs +1404 -1218
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node/{chunk-MHBMVCFZ.cjs → chunk-YSBXNBDJ.cjs} +6 -6
- package/dist/lib/node/{chunk-MHBMVCFZ.cjs.map → chunk-YSBXNBDJ.cjs.map} +2 -2
- package/dist/lib/node/index.cjs +1136 -936
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types/index.cjs +3 -3
- package/dist/lib/node/types/index.cjs.map +1 -1
- package/dist/lib/node-esm/{chunk-BMWGPXPL.mjs → chunk-6OH2NHUX.mjs} +4 -4
- package/dist/lib/node-esm/{chunk-BMWGPXPL.mjs.map → chunk-6OH2NHUX.mjs.map} +2 -2
- package/dist/lib/node-esm/index.mjs +1404 -1218
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/_deprecated/hooks/useColumnSorting.d.ts +1 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +4 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/CreateRefPanel.d.ts +3 -2
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/RefPanel.d.ts +4 -3
- package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts +3 -2
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +2 -1
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -2
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +55 -71
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/model/index.d.ts +1 -0
- package/dist/types/src/model/index.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +6 -2
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +8 -12
- 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/playwright/TableManager.d.ts +2 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/types/table.d.ts +118 -154
- package/dist/types/src/types/table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts +50 -20
- package/dist/types/src/util/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts +32 -9
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/src/util/table.d.ts +2 -2
- package/dist/types/src/util/table.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +29 -29
- package/src/components/Table/ColumnActionsMenu.tsx +8 -8
- package/src/components/Table/ColumnSettings.tsx +11 -11
- package/src/components/Table/CreateRefPanel.tsx +13 -14
- package/src/components/Table/RefPanel.tsx +9 -9
- package/src/components/Table/RowActionsMenu.tsx +6 -7
- package/src/components/Table/Table.stories.tsx +40 -23
- package/src/components/Table/Table.tsx +235 -148
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -12
- package/src/components/TableCellEditor/TableCellEditor.tsx +6 -4
- package/src/components/testing.ts +10 -8
- package/src/index.ts +2 -0
- package/src/model/index.ts +1 -0
- package/src/model/modal-controller.ts +15 -48
- package/src/model/selection-model.ts +12 -1
- package/src/model/table-model.test.ts +4 -71
- package/src/model/table-model.ts +59 -232
- package/src/model/table-presentation.test.ts +102 -0
- package/src/model/table-presentation.ts +214 -0
- package/src/playwright/TableManager.ts +5 -1
- package/src/playwright/smoke.spec.ts +79 -1
- package/src/types/table.ts +3 -3
- package/src/util/index.ts +1 -0
- package/src/util/table-buttons.ts +97 -48
- package/src/util/table-controls.ts +71 -7
- package/src/util/table.ts +21 -13
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
146
|
+
// TODO(burdon): Use same logic as form for fallback title.
|
|
147
|
+
value: props.title ?? field.path,
|
|
148
|
+
readonly: true,
|
|
149
|
+
resizeHandle: 'col',
|
|
150
|
+
accessoryHtml: tableButtons.columnSettings.render({ fieldId: field.id }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return cells;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private getSelectionColumnCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
158
|
+
const cells: DxGridPlaneCells = {};
|
|
159
|
+
for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
|
|
160
|
+
const isSelected = this.model.selection.isRowIndexSelected(row);
|
|
161
|
+
const classes = cellClassesForRowSelection(isSelected);
|
|
162
|
+
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
163
|
+
value: '',
|
|
164
|
+
readonly: true,
|
|
165
|
+
className: classes ? mx(classes) : undefined,
|
|
166
|
+
accessoryHtml: tableControls.checkbox.render({ rowIndex: row, checked: isSelected }),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return cells;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private getActionColumnCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
174
|
+
const cells: DxGridPlaneCells = {};
|
|
175
|
+
for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
|
|
176
|
+
const isSelected = this.model.selection.isRowIndexSelected(row);
|
|
177
|
+
const classes = cellClassesForRowSelection(isSelected);
|
|
178
|
+
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
179
|
+
value: '',
|
|
180
|
+
readonly: true,
|
|
181
|
+
className: classes ? mx(classes) : undefined,
|
|
182
|
+
accessoryHtml: tableButtons.rowMenu.render({ rowIndex: row }),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return cells;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private getSelectAllCell(): DxGridPlaneCells {
|
|
190
|
+
return {
|
|
191
|
+
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
192
|
+
value: '',
|
|
193
|
+
accessoryHtml: tableControls.checkbox.render({
|
|
194
|
+
rowIndex: 0,
|
|
195
|
+
header: true,
|
|
196
|
+
checked: this.model.selection.allRowsSeleted.value,
|
|
197
|
+
}),
|
|
198
|
+
readonly: true,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private getNewColumnCell(): DxGridPlaneCells {
|
|
204
|
+
return {
|
|
205
|
+
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
206
|
+
value: '',
|
|
207
|
+
accessoryHtml: tableButtons.addColumn.render({
|
|
208
|
+
disabled: (this.model.table.view?.target?.fields?.length ?? 0) >= VIEW_FIELD_LIMIT,
|
|
209
|
+
}),
|
|
210
|
+
readonly: true,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -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();
|
|
@@ -38,6 +38,26 @@ test.describe('Table', () => {
|
|
|
38
38
|
await page.close();
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
+
test('selection', async ({ browser }) => {
|
|
42
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
43
|
+
const table = new TableManager(page);
|
|
44
|
+
|
|
45
|
+
await table.grid.ready();
|
|
46
|
+
// Select All
|
|
47
|
+
await table.toggleSelectAll();
|
|
48
|
+
await expect(table.selection(0)).toBeChecked();
|
|
49
|
+
await expect(table.selection(1)).toBeChecked();
|
|
50
|
+
await expect(table.selection(2)).toBeChecked();
|
|
51
|
+
|
|
52
|
+
// Deselect All
|
|
53
|
+
await table.toggleSelectAll();
|
|
54
|
+
await expect(table.selection(0)).not.toBeChecked();
|
|
55
|
+
await expect(table.selection(1)).not.toBeChecked();
|
|
56
|
+
await expect(table.selection(2)).not.toBeChecked();
|
|
57
|
+
|
|
58
|
+
await page.close();
|
|
59
|
+
});
|
|
60
|
+
|
|
41
61
|
test('delete row', async ({ browser }) => {
|
|
42
62
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
43
63
|
const table = new TableManager(page);
|
|
@@ -69,6 +89,9 @@ test.describe('Table', () => {
|
|
|
69
89
|
const table = new TableManager(page);
|
|
70
90
|
|
|
71
91
|
await table.grid.ready();
|
|
92
|
+
await table.deleteColumn(0);
|
|
93
|
+
await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(40);
|
|
94
|
+
|
|
72
95
|
await table.deleteColumn(0);
|
|
73
96
|
await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(30);
|
|
74
97
|
|
|
@@ -85,7 +108,8 @@ test.describe('Table', () => {
|
|
|
85
108
|
await page.close();
|
|
86
109
|
});
|
|
87
110
|
|
|
88
|
-
// Rest of add column test remains the same as it's a more complex flow
|
|
111
|
+
// Rest of add column test remains the same as it's a more complex flow.
|
|
112
|
+
// TODO(ZaymonFC): Restore this after fixing format selection.
|
|
89
113
|
test('add column', async ({ browser }) => {
|
|
90
114
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
91
115
|
const table = new TableManager(page);
|
|
@@ -98,4 +122,58 @@ test.describe('Table', () => {
|
|
|
98
122
|
await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
|
|
99
123
|
await page.close();
|
|
100
124
|
});
|
|
125
|
+
|
|
126
|
+
test('reference > reference / create new object', async ({ browser }) => {
|
|
127
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
128
|
+
const table = new TableManager(page);
|
|
129
|
+
|
|
130
|
+
await table.grid.ready();
|
|
131
|
+
|
|
132
|
+
// Reference existing object.
|
|
133
|
+
// Scroll over to the manager column.
|
|
134
|
+
await table.grid.panByWheel(10000, 0);
|
|
135
|
+
|
|
136
|
+
await table.grid.cell(4, 0, 'grid').click();
|
|
137
|
+
await page.keyboard.press('A');
|
|
138
|
+
await page.keyboard.press('N');
|
|
139
|
+
await page.getByRole('option', { name: 'Anita Mayer' }).click();
|
|
140
|
+
|
|
141
|
+
// Assert that the value is shown in the cell.
|
|
142
|
+
await expect(page.getByRole('gridcell', { name: 'Anita Mayer' })).toBeVisible();
|
|
143
|
+
|
|
144
|
+
// Create new object.
|
|
145
|
+
await table.grid.cell(4, 1, 'grid').click();
|
|
146
|
+
await page.keyboard.type('t');
|
|
147
|
+
await page.keyboard.type('e');
|
|
148
|
+
await page.keyboard.type('s');
|
|
149
|
+
await page.keyboard.type('t');
|
|
150
|
+
await page.getByText('Create new object', { exact: false }).click();
|
|
151
|
+
await page.getByTestId('save-button').click();
|
|
152
|
+
|
|
153
|
+
// Scroll to the left.
|
|
154
|
+
await page.getByRole('gridcell', { name: 'test' }).click();
|
|
155
|
+
|
|
156
|
+
await page.close();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('test toggles', async ({ browser }) => {
|
|
160
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
161
|
+
const table = new TableManager(page);
|
|
162
|
+
|
|
163
|
+
await table.grid.ready();
|
|
164
|
+
|
|
165
|
+
await page.getByTestId('table-switch').first().click();
|
|
166
|
+
await page.getByTestId('table-switch').nth(7).click();
|
|
167
|
+
|
|
168
|
+
// Test that checks are durable in the data model by sorting.
|
|
169
|
+
await table.sortColumn(1, 'descending');
|
|
170
|
+
|
|
171
|
+
// Assert the first two switch checkboxes are checked.
|
|
172
|
+
await expect(page.getByTestId('table-switch').first()).toBeChecked();
|
|
173
|
+
await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
|
|
174
|
+
await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Anita Mayer');
|
|
175
|
+
await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Uwe Øvergård');
|
|
176
|
+
|
|
177
|
+
await page.close();
|
|
178
|
+
});
|
|
101
179
|
});
|
package/src/types/table.ts
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Ref, ObjectId, S, TypedObject } from '@dxos/echo-schema';
|
|
6
6
|
import { ThreadType } from '@dxos/plugin-space';
|
|
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(ThreadType))),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
// type TableType = S.Schema.Type<typeof TableSchema>;
|
package/src/util/index.ts
CHANGED
|
@@ -2,69 +2,118 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
type TableButton = 'columnSettings' | 'rowMenu' | 'newColumn' | 'referencedCell';
|
|
5
|
+
export type TableButton = 'columnSettings' | 'rowMenu' | 'newColumn' | 'referencedCell';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// TODO(thure): These unique attributes come with more caveats than e.g. applying multiple attributes – one key-value
|
|
10
|
-
// pair for identifying the button type and another for passing the fieldId.
|
|
11
|
-
|
|
12
|
-
const TABLE_ATTRS: { [K in TableButton]: string } = {
|
|
13
|
-
referencedCell: 'data-table-ref-cell-button',
|
|
7
|
+
export const BUTTON_IDENTIFIERS: { [K in TableButton]: string } = {
|
|
14
8
|
columnSettings: 'data-table-column-settings-button',
|
|
15
|
-
rowMenu: 'data-table-row-menu-button',
|
|
16
9
|
newColumn: 'data-table-new-column-button',
|
|
10
|
+
referencedCell: 'data-table-ref-cell-button',
|
|
11
|
+
rowMenu: 'data-table-row-menu-button',
|
|
17
12
|
} as const;
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} as const;
|
|
14
|
+
type ButtonData =
|
|
15
|
+
| { type: 'columnSettings'; fieldId: string }
|
|
16
|
+
| { type: 'newColumn'; disabled?: boolean }
|
|
17
|
+
| { type: 'referencedCell'; schemaId: string; targetId: string }
|
|
18
|
+
| { type: 'rowMenu'; rowIndex: number };
|
|
25
19
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const createButton = ({
|
|
21
|
+
attr,
|
|
22
|
+
data,
|
|
29
23
|
disabled = false,
|
|
24
|
+
icon,
|
|
30
25
|
testId,
|
|
31
26
|
}: {
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
attr: string;
|
|
28
|
+
data: Record<string, string>;
|
|
34
29
|
disabled?: boolean;
|
|
30
|
+
icon: string;
|
|
35
31
|
testId: string;
|
|
36
|
-
})
|
|
37
|
-
const
|
|
38
|
-
|
|
32
|
+
}) => {
|
|
33
|
+
const dataAttrs = Object.entries(data)
|
|
34
|
+
.map(([k, v]) => `${k}="${v}"`)
|
|
35
|
+
.join(' ');
|
|
36
|
+
|
|
37
|
+
return `<button ${attr} data-testid="${testId}" class="ch-button is-6 pli-0.5 min-bs-0 absolute inset-block-1 inline-end-2" ${dataAttrs} ${disabled ? 'disabled' : ''}><svg><use href="/icons.svg#${icon}"/></svg></button>`;
|
|
39
38
|
};
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
const addColumnButton = {
|
|
41
|
+
attr: BUTTON_IDENTIFIERS.newColumn,
|
|
42
|
+
icon: 'ph--plus--regular',
|
|
43
|
+
render: ({ disabled }: Omit<Extract<ButtonData, { type: 'newColumn' }>, 'type'>) => {
|
|
44
|
+
return createButton({
|
|
45
|
+
attr: BUTTON_IDENTIFIERS.newColumn,
|
|
46
|
+
icon: addColumnButton.icon,
|
|
47
|
+
disabled,
|
|
48
|
+
data: {},
|
|
49
|
+
testId: 'table-new-column-button',
|
|
50
|
+
});
|
|
51
51
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
getData: (_el: HTMLElement): Extract<ButtonData, { type: 'newColumn' }> => ({ type: 'newColumn' }),
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
const columnSettingsButton = {
|
|
56
|
+
attr: BUTTON_IDENTIFIERS.columnSettings,
|
|
57
|
+
icon: 'ph--caret-down--regular',
|
|
58
|
+
render: ({ fieldId }: Omit<Extract<ButtonData, { type: 'columnSettings' }>, 'type'>) => {
|
|
59
|
+
return createButton({
|
|
60
|
+
attr: BUTTON_IDENTIFIERS.columnSettings,
|
|
61
|
+
icon: columnSettingsButton.icon,
|
|
62
|
+
data: {
|
|
63
|
+
'data-field-id': fieldId,
|
|
64
|
+
},
|
|
65
|
+
testId: 'table-column-settings-button',
|
|
66
|
+
});
|
|
57
67
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
getData: (el: HTMLElement): Extract<ButtonData, { type: 'columnSettings' }> => ({
|
|
69
|
+
type: 'columnSettings',
|
|
70
|
+
fieldId: el.getAttribute('data-field-id')!,
|
|
71
|
+
}),
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
const referencedCellButton = {
|
|
75
|
+
attr: BUTTON_IDENTIFIERS.referencedCell,
|
|
76
|
+
icon: 'ph--link-simple-horizontal--regular',
|
|
77
|
+
render: ({ targetId, schemaId }: Omit<Extract<ButtonData, { type: 'referencedCell' }>, 'type'>) => {
|
|
78
|
+
return createButton({
|
|
79
|
+
attr: BUTTON_IDENTIFIERS.referencedCell,
|
|
80
|
+
icon: referencedCellButton.icon,
|
|
81
|
+
data: {
|
|
82
|
+
'data-target-id': targetId,
|
|
83
|
+
'data-schema-id': schemaId,
|
|
84
|
+
},
|
|
85
|
+
testId: 'table-ref-cell-button',
|
|
86
|
+
});
|
|
63
87
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
88
|
+
getData: (el: HTMLElement): Extract<ButtonData, { type: 'referencedCell' }> => ({
|
|
89
|
+
type: 'referencedCell',
|
|
90
|
+
targetId: el.getAttribute('data-target-id')!,
|
|
91
|
+
schemaId: el.getAttribute('data-schema-id')!,
|
|
92
|
+
}),
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
const rowMenuButton = {
|
|
96
|
+
attr: BUTTON_IDENTIFIERS.rowMenu,
|
|
97
|
+
icon: 'ph--dots-three--regular',
|
|
98
|
+
render: ({ rowIndex }: Omit<Extract<ButtonData, { type: 'rowMenu' }>, 'type'>) => {
|
|
99
|
+
return createButton({
|
|
100
|
+
attr: BUTTON_IDENTIFIERS.rowMenu,
|
|
101
|
+
icon: rowMenuButton.icon,
|
|
102
|
+
data: {
|
|
103
|
+
'data-row-index': rowIndex.toString(),
|
|
104
|
+
},
|
|
105
|
+
testId: 'table-row-menu-button',
|
|
106
|
+
});
|
|
69
107
|
},
|
|
108
|
+
getData: (el: HTMLElement): Extract<ButtonData, { type: 'rowMenu' }> => ({
|
|
109
|
+
type: 'rowMenu',
|
|
110
|
+
rowIndex: Number(el.getAttribute('data-row-index')!),
|
|
111
|
+
}),
|
|
112
|
+
} as const;
|
|
113
|
+
|
|
114
|
+
export const tableButtons = {
|
|
115
|
+
addColumn: addColumnButton,
|
|
116
|
+
columnSettings: columnSettingsButton,
|
|
117
|
+
referencedCell: referencedCellButton,
|
|
118
|
+
rowMenu: rowMenuButton,
|
|
70
119
|
} as const;
|