@dxos/react-ui-table 0.8.4-main.7ace549 → 0.8.4-main.8360d9e660
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-5XWSB3JP.mjs → chunk-BF54SSNL.mjs} +27 -28
- package/dist/lib/browser/chunk-BF54SSNL.mjs.map +7 -0
- package/dist/lib/browser/{chunk-KNC7XYT6.mjs → chunk-PTHMYAAM.mjs} +620 -652
- package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1154 -1036
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +20 -19
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-CWSGS3Y2.mjs → chunk-GI72MQSG.mjs} +620 -651
- package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-MKTMOY46.mjs → chunk-SIRDIPHZ.mjs} +27 -28
- package/dist/lib/node-esm/chunk-SIRDIPHZ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1154 -1036
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +20 -19
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +2 -16
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +6 -3
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +11 -11
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +6 -3
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +2 -2
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +3 -3
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +22 -13
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +5 -6
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +2 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +6 -5
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +5 -1
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +17 -8
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +100 -18
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +3 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- 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/testing/useTestTableModel.d.ts +5 -8
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +13 -15
- package/dist/types/src/types/Table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +12 -10
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
- package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/query-ast.d.ts +7 -0
- package/dist/types/src/util/query-ast.d.ts.map +1 -0
- package/dist/types/src/util/sort.d.ts +12 -0
- package/dist/types/src/util/sort.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +54 -55
- package/src/components/Table/ColumnActionsMenu.tsx +10 -6
- package/src/components/Table/ColumnSettings.tsx +8 -9
- package/src/components/Table/DynamicTable.stories.tsx +11 -11
- package/src/components/Table/DynamicTable.tsx +30 -30
- package/src/components/Table/Relations.stories.tsx +34 -31
- package/src/components/Table/RowActionsMenu.tsx +3 -2
- package/src/components/Table/Table.stories.tsx +80 -67
- package/src/components/Table/Table.tsx +324 -299
- package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -5
- package/src/components/TableCellEditor/FormCellEditor.tsx +118 -135
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +10 -7
- package/src/components/TableCellEditor/TableCellEditor.tsx +27 -27
- package/src/components/TableToolbar/TableToolbar.tsx +24 -18
- package/src/components/testing.ts +37 -11
- package/src/hooks/useAddRow.ts +8 -10
- package/src/hooks/useProjectionModel.ts +29 -9
- package/src/hooks/useTableModel.ts +27 -23
- package/src/model/modal-controller.ts +23 -12
- package/src/model/selection-model.ts +75 -44
- package/src/model/table-model.test.ts +81 -53
- package/src/model/table-model.ts +410 -138
- package/src/model/table-presentation.test.ts +58 -48
- package/src/model/table-presentation.ts +34 -28
- package/src/playwright/TableManager.ts +12 -5
- package/src/playwright/playwright.config.ts +1 -1
- package/src/playwright/smoke.spec.ts +48 -4
- package/src/testing/useTestTableModel.ts +24 -25
- package/src/translations.ts +1 -1
- package/src/types/Table.ts +28 -38
- package/src/util/dynamic-table.test.ts +70 -0
- package/src/util/dynamic-table.ts +36 -19
- package/src/util/index.ts +2 -0
- package/src/util/query-ast.ts +22 -0
- package/src/util/sort.ts +90 -0
- package/src/util/table-buttons.ts +1 -1
- package/src/util/table-controls.ts +2 -2
- package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +0 -7
- package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +0 -7
- package/dist/types/src/model/table-sorting.d.ts +0 -56
- package/dist/types/src/model/table-sorting.d.ts.map +0 -1
- package/src/model/table-sorting.ts +0 -234
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { Registry } from '@effect-atom/atom-react';
|
|
5
6
|
import * as Schema from 'effect/Schema';
|
|
6
7
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
8
|
|
|
8
|
-
import { Filter, Query } from '@dxos/echo';
|
|
9
|
-
import { TypedObject } from '@dxos/echo/internal';
|
|
9
|
+
import { Filter, JsonSchema, Query, Type } from '@dxos/echo';
|
|
10
10
|
import { createEchoSchema } from '@dxos/echo/testing';
|
|
11
|
-
import {
|
|
12
|
-
import { ProjectionModel, View } from '@dxos/schema';
|
|
11
|
+
import { ProjectionModel, ViewModel, createDirectChangeCallback } from '@dxos/schema';
|
|
13
12
|
|
|
14
13
|
import { Table } from '../types';
|
|
15
14
|
|
|
@@ -17,48 +16,53 @@ import { TableModel, type TableModelProps } from './table-model';
|
|
|
17
16
|
import { TablePresentation } from './table-presentation';
|
|
18
17
|
|
|
19
18
|
describe('TablePresentation', () => {
|
|
20
|
-
describe('
|
|
19
|
+
describe('getCells', () => {
|
|
20
|
+
let registry: Registry.Registry;
|
|
21
21
|
let data: any[];
|
|
22
|
-
let updateCount: number;
|
|
23
22
|
let model: any;
|
|
24
23
|
let presentation: TablePresentation;
|
|
25
24
|
|
|
26
25
|
beforeEach(async () => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
model = createTableModel({
|
|
36
|
-
onCellUpdate: () => updateCount++,
|
|
37
|
-
});
|
|
38
|
-
model.setRows(data);
|
|
26
|
+
registry = Registry.make();
|
|
27
|
+
data = [
|
|
28
|
+
{ id: '1', title: 'A', count: 1 },
|
|
29
|
+
{ id: '2', title: 'B', count: 2 },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
model = createTableModel(registry, {});
|
|
39
33
|
await model.open();
|
|
40
|
-
|
|
34
|
+
model.setRows(data);
|
|
35
|
+
presentation = new TablePresentation(registry, model);
|
|
41
36
|
});
|
|
42
37
|
|
|
43
|
-
it('should
|
|
44
|
-
|
|
45
|
-
|
|
38
|
+
it('should return cells for the visible range', () => {
|
|
39
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
|
|
40
|
+
|
|
41
|
+
// Verify that cells are returned for the range. The exact column indices depend on the projection.
|
|
42
|
+
const cellKeys = Object.keys(cells);
|
|
43
|
+
expect(cellKeys.length).toBeGreaterThan(0);
|
|
44
|
+
|
|
45
|
+
// Check that values from the data are present in the cells.
|
|
46
|
+
const values = Object.values(cells).map((c) => c.value);
|
|
47
|
+
expect(values).toContain('A');
|
|
48
|
+
expect(values).toContain('B');
|
|
49
|
+
});
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
it('should reflect row data changes', () => {
|
|
52
|
+
// Update the row data and refresh.
|
|
48
53
|
data[0].title = 'New Value';
|
|
49
|
-
|
|
54
|
+
model.setRows(data);
|
|
50
55
|
|
|
51
56
|
// Verify the new value through getCells.
|
|
52
|
-
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col:
|
|
53
|
-
|
|
57
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 2 } }, 'grid');
|
|
58
|
+
const values = Object.values(cells).map((c) => c.value);
|
|
59
|
+
expect(values).toContain('New Value');
|
|
54
60
|
});
|
|
55
61
|
|
|
56
|
-
it('should
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
data.push({ title: 'C', count: 3 });
|
|
61
|
-
expect(updateCount).toBe(2);
|
|
62
|
+
it('should reflect new rows', () => {
|
|
63
|
+
// Add a new row.
|
|
64
|
+
data.push({ id: '3', title: 'C', count: 3 });
|
|
65
|
+
model.setRows(data);
|
|
62
66
|
|
|
63
67
|
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
|
|
64
68
|
|
|
@@ -67,17 +71,12 @@ describe('TablePresentation', () => {
|
|
|
67
71
|
expect(cells['1,2']?.value).toBe('3');
|
|
68
72
|
});
|
|
69
73
|
|
|
70
|
-
it('should handle
|
|
71
|
-
//
|
|
72
|
-
presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 2, col: 2 } }, 'grid');
|
|
73
|
-
|
|
74
|
-
// Multiple operations.
|
|
74
|
+
it('should handle row modifications', () => {
|
|
75
|
+
// Modify the data.
|
|
75
76
|
data[0].title = 'Updated A';
|
|
76
|
-
data.push({ title: 'C', count: 3
|
|
77
|
+
data.push({ id: '3', title: 'C', count: 3 });
|
|
77
78
|
data.splice(1, 1);
|
|
78
|
-
|
|
79
|
-
// We expect one update per operation affecting visible rows.
|
|
80
|
-
expect(updateCount).toBe(4);
|
|
79
|
+
model.setRows(data);
|
|
81
80
|
|
|
82
81
|
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 2 } }, 'grid');
|
|
83
82
|
|
|
@@ -88,18 +87,29 @@ describe('TablePresentation', () => {
|
|
|
88
87
|
});
|
|
89
88
|
});
|
|
90
89
|
|
|
91
|
-
|
|
90
|
+
const Test = Schema.Struct({
|
|
92
91
|
title: Schema.String,
|
|
93
92
|
count: Schema.Number,
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
}).pipe(
|
|
94
|
+
Type.object({
|
|
95
|
+
typename: 'com.example.type.test',
|
|
96
|
+
version: '0.1.0',
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const createTableModel = (registry: Registry.Registry, props: Partial<TableModelProps> = {}): TableModel => {
|
|
97
101
|
const schema = createEchoSchema(Test);
|
|
98
|
-
const view =
|
|
102
|
+
const view = ViewModel.make({
|
|
99
103
|
query: Query.select(Filter.type(schema)),
|
|
100
104
|
jsonSchema: schema.jsonSchema,
|
|
101
105
|
});
|
|
102
106
|
const object = Table.make({ view });
|
|
103
|
-
const projection = new ProjectionModel(
|
|
104
|
-
|
|
107
|
+
const projection = new ProjectionModel({
|
|
108
|
+
registry,
|
|
109
|
+
view,
|
|
110
|
+
baseSchema: schema.jsonSchema,
|
|
111
|
+
change: createDirectChangeCallback(view.projection, JsonSchema.toJsonSchema(schema)),
|
|
112
|
+
});
|
|
113
|
+
projection.normalizeView();
|
|
114
|
+
return new TableModel({ registry, object, projection, ...props });
|
|
105
115
|
};
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom-react';
|
|
6
6
|
import * as Predicate from 'effect/Predicate';
|
|
7
7
|
|
|
8
8
|
import { Obj } from '@dxos/echo';
|
|
9
|
+
import { type View } from '@dxos/echo';
|
|
9
10
|
import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
|
|
10
11
|
import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
|
|
11
12
|
import {
|
|
@@ -15,8 +16,8 @@ import {
|
|
|
15
16
|
type DxGridPlaneRange,
|
|
16
17
|
toPlaneCellIndex,
|
|
17
18
|
} from '@dxos/react-ui-grid';
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { VIEW_FIELD_LIMIT } from '@dxos/schema';
|
|
20
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
21
|
|
|
21
22
|
import { tableButtons, tableControls } from '../util';
|
|
22
23
|
|
|
@@ -29,15 +30,20 @@ import { type TableModel, type TableRow } from './table-model';
|
|
|
29
30
|
* different grid planes.
|
|
30
31
|
*/
|
|
31
32
|
export class TablePresentation<T extends TableRow = TableRow> {
|
|
33
|
+
private readonly _registry: Registry.Registry;
|
|
34
|
+
private readonly _visibleRange: Atom.Writable<DxGridPlaneRange>;
|
|
32
35
|
private fieldProjectionCache = new Map<string, ReturnType<typeof this.model.projection.getFieldProjection>>();
|
|
33
36
|
|
|
34
37
|
constructor(
|
|
38
|
+
registry: Registry.Registry,
|
|
35
39
|
private readonly model: TableModel<T>,
|
|
36
|
-
|
|
40
|
+
) {
|
|
41
|
+
this._registry = registry;
|
|
42
|
+
this._visibleRange = Atom.make<DxGridPlaneRange>({
|
|
37
43
|
start: { row: 0, col: 0 },
|
|
38
44
|
end: { row: 0, col: 0 },
|
|
39
|
-
})
|
|
40
|
-
|
|
45
|
+
});
|
|
46
|
+
}
|
|
41
47
|
|
|
42
48
|
public getCells(range: DxGridPlaneRange, plane: DxGridPlane): DxGridPlaneCells {
|
|
43
49
|
// Clear cache at the start of each render pass
|
|
@@ -47,7 +53,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
47
53
|
|
|
48
54
|
switch (plane) {
|
|
49
55
|
case 'grid':
|
|
50
|
-
this._visibleRange
|
|
56
|
+
this._registry.set(this._visibleRange, range);
|
|
51
57
|
cells = this.getMainGridCells(range);
|
|
52
58
|
break;
|
|
53
59
|
case 'frozenRowsStart':
|
|
@@ -102,7 +108,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
102
108
|
private createDataCell(
|
|
103
109
|
cells: DxGridPlaneCells,
|
|
104
110
|
obj: T,
|
|
105
|
-
field: FieldType,
|
|
111
|
+
field: View.FieldType,
|
|
106
112
|
colIndex: number,
|
|
107
113
|
displayIndex: number,
|
|
108
114
|
): void {
|
|
@@ -205,7 +211,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
205
211
|
const targetObj = getValue(obj, field.path)?.target;
|
|
206
212
|
if (targetObj) {
|
|
207
213
|
const dxn = Obj.getDXN(targetObj)?.toString();
|
|
208
|
-
cell.accessoryHtml = `<div role="none" class="absolute
|
|
214
|
+
cell.accessoryHtml = `<div role="none" class="absolute end-0 inset-y-0 p-(--dx-grid-cell-content-padding-block)"><dx-anchor dxn=${dxn} class="dx-button w-6 aspect-square min-h-0" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-anchor></div>`;
|
|
209
215
|
}
|
|
210
216
|
}
|
|
211
217
|
|
|
@@ -260,7 +266,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
260
266
|
|
|
261
267
|
private getMainGridCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
262
268
|
const cells: DxGridPlaneCells = {};
|
|
263
|
-
const fields = this.model.projection?.
|
|
269
|
+
const fields = this.model.projection?.getFields() ?? [];
|
|
264
270
|
|
|
265
271
|
for (let row = range.start.row; row <= range.end.row && row < this.model.getRowCount(); row++) {
|
|
266
272
|
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
@@ -269,7 +275,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
269
275
|
continue;
|
|
270
276
|
}
|
|
271
277
|
|
|
272
|
-
this.createDataCell(cells, this.model.
|
|
278
|
+
this.createDataCell(cells, this.model.getRows()[row], field, col, row);
|
|
273
279
|
}
|
|
274
280
|
}
|
|
275
281
|
|
|
@@ -278,8 +284,8 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
278
284
|
|
|
279
285
|
private getDraftRowCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
280
286
|
const cells: DxGridPlaneCells = {};
|
|
281
|
-
const fields = this.model.projection?.
|
|
282
|
-
const draftRows = this.model.
|
|
287
|
+
const fields = this.model.projection?.getFields() ?? [];
|
|
288
|
+
const draftRows = this.model.getDraftRows();
|
|
283
289
|
|
|
284
290
|
// Return cells of the CTA row if no draft row is active
|
|
285
291
|
if (draftRows.length === 0) {
|
|
@@ -319,22 +325,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
319
325
|
|
|
320
326
|
private getHeaderCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
321
327
|
const cells: DxGridPlaneCells = {};
|
|
322
|
-
const fields = this.model.projection?.
|
|
328
|
+
const fields = this.model.projection?.getFields() ?? [];
|
|
329
|
+
const currentSort = this.model.getSorting();
|
|
323
330
|
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
324
331
|
const { field, props } = this.model.projection.getFieldProjection(fields[col].id);
|
|
325
|
-
const
|
|
326
|
-
const direction = sorting?.fieldId === field.id ? sorting.direction : undefined;
|
|
332
|
+
const direction = currentSort?.fieldId === field.id ? currentSort.direction : undefined;
|
|
327
333
|
|
|
328
334
|
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
329
335
|
// TODO(burdon): Use same logic as form for fallback title.
|
|
330
336
|
value: '',
|
|
331
337
|
resizeHandle: 'col',
|
|
332
338
|
accessoryHtml: `
|
|
333
|
-
<span class="grow min-
|
|
339
|
+
<span class="grow min-w-0 truncate">${props.title ?? field.path}</span>
|
|
334
340
|
${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
|
|
335
341
|
${tableButtons.columnSettings.render({ fieldId: field.id })}
|
|
336
342
|
`,
|
|
337
|
-
className: '!bg-
|
|
343
|
+
className: '!bg-toolbar-surface text-description! [&>div]:flex [&>div]:items-stretch',
|
|
338
344
|
};
|
|
339
345
|
}
|
|
340
346
|
|
|
@@ -377,7 +383,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
377
383
|
if (!this.model.features.selection.enabled || this.model.selection.selectionMode === 'single') {
|
|
378
384
|
return {
|
|
379
385
|
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
380
|
-
className: '!bg-
|
|
386
|
+
className: '!bg-toolbar-surface',
|
|
381
387
|
readonly: true,
|
|
382
388
|
value: '',
|
|
383
389
|
},
|
|
@@ -389,9 +395,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
389
395
|
accessoryHtml: tableControls.checkbox.render({
|
|
390
396
|
rowIndex: 0,
|
|
391
397
|
header: true,
|
|
392
|
-
checked: this.model.selection.
|
|
398
|
+
checked: this.model.selection.allRowsSelected,
|
|
393
399
|
}),
|
|
394
|
-
className: '!bg-
|
|
400
|
+
className: '!bg-toolbar-surface',
|
|
395
401
|
readonly: true,
|
|
396
402
|
value: '',
|
|
397
403
|
},
|
|
@@ -403,10 +409,10 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
403
409
|
[toPlaneCellIndex({ col: 0, row: 0 })]: {
|
|
404
410
|
accessoryHtml: this.model.features.schemaEditable
|
|
405
411
|
? tableButtons.addColumn.render({
|
|
406
|
-
disabled: (this.model.projection?.
|
|
412
|
+
disabled: (this.model.projection?.getFields()?.length ?? 0) >= VIEW_FIELD_LIMIT,
|
|
407
413
|
})
|
|
408
414
|
: undefined,
|
|
409
|
-
className: '!bg-
|
|
415
|
+
className: '!bg-toolbar-surface',
|
|
410
416
|
readonly: true,
|
|
411
417
|
value: '',
|
|
412
418
|
},
|
|
@@ -415,7 +421,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
415
421
|
|
|
416
422
|
private getDraftActionCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
417
423
|
const cells: DxGridPlaneCells = {};
|
|
418
|
-
const draftRows = this.model.
|
|
424
|
+
const draftRows = this.model.getDraftRows();
|
|
419
425
|
|
|
420
426
|
// Return cells of the CTA row if no draft row is active
|
|
421
427
|
if (draftRows.length === 0) {
|
|
@@ -442,7 +448,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
442
448
|
|
|
443
449
|
private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
444
450
|
const cells: DxGridPlaneCells = {};
|
|
445
|
-
const draftRows = this.model.
|
|
451
|
+
const draftRows = this.model.getDraftRows();
|
|
446
452
|
|
|
447
453
|
for (let row = range.start.row; row <= range.end.row; row++) {
|
|
448
454
|
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
@@ -460,7 +466,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
460
466
|
export const cellClassesForRowSelection = (selected: boolean, selectionMode: SelectionMode) => {
|
|
461
467
|
if (!selected) {
|
|
462
468
|
if (selectionMode === 'single') {
|
|
463
|
-
return ['
|
|
469
|
+
return ['cursor-pointer!'];
|
|
464
470
|
} else {
|
|
465
471
|
return undefined;
|
|
466
472
|
}
|
|
@@ -468,8 +474,8 @@ export const cellClassesForRowSelection = (selected: boolean, selectionMode: Sel
|
|
|
468
474
|
|
|
469
475
|
switch (selectionMode) {
|
|
470
476
|
case 'single':
|
|
471
|
-
return ['
|
|
477
|
+
return ['dx-grid__cell--no-focus-unfurl !bg-current-surface hover:bg-hover-surface cursor-pointer!'];
|
|
472
478
|
case 'multiple':
|
|
473
|
-
return ['!bg-
|
|
479
|
+
return ['!bg-grid-cell-selected'];
|
|
474
480
|
}
|
|
475
481
|
};
|
|
@@ -17,6 +17,7 @@ const TABLE_SELECTORS = {
|
|
|
17
17
|
},
|
|
18
18
|
columnDelete: 'column-delete',
|
|
19
19
|
rowDelete: 'row-menu-delete',
|
|
20
|
+
addRow: 'table.toolbar.add-row',
|
|
20
21
|
} as const;
|
|
21
22
|
|
|
22
23
|
export class TableManager {
|
|
@@ -53,12 +54,18 @@ export class TableManager {
|
|
|
53
54
|
await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
public async
|
|
57
|
+
public async addRow(): Promise<void> {
|
|
58
|
+
await this.page.getByTestId(TABLE_SELECTORS.addRow).click();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public async addColumn({ label, format }: { label: string; format?: string }): Promise<void> {
|
|
57
62
|
await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
if (format) {
|
|
64
|
+
await this.page.getByRole('combobox').click();
|
|
65
|
+
await this.page.getByLabel(format).click();
|
|
66
|
+
}
|
|
67
|
+
await this.page.getByPlaceholder('Property label').click();
|
|
68
|
+
await this.page.getByPlaceholder('Property label').fill(label);
|
|
62
69
|
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
63
70
|
}
|
|
64
71
|
}
|
|
@@ -10,7 +10,7 @@ export default defineConfig({
|
|
|
10
10
|
...e2ePreset(import.meta.dirname),
|
|
11
11
|
// TODO(wittjosiah): Avoid hard-coding ports.
|
|
12
12
|
webServer: {
|
|
13
|
-
command: '
|
|
13
|
+
command: 'pnpm storybook dev --ci --quiet --port=9004 --config-dir=.storybook',
|
|
14
14
|
port: 9004,
|
|
15
15
|
reuseExistingServer: false,
|
|
16
16
|
},
|
|
@@ -71,6 +71,7 @@ test.describe('Table', () => {
|
|
|
71
71
|
const table = new TableManager(page);
|
|
72
72
|
|
|
73
73
|
await table.grid.ready();
|
|
74
|
+
await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(1);
|
|
74
75
|
await table.deleteRow(0);
|
|
75
76
|
await expect(page.getByRole('gridcell', { name: 'Sapiente.' })).toHaveCount(0);
|
|
76
77
|
await page.close();
|
|
@@ -88,7 +89,7 @@ test.describe('Table', () => {
|
|
|
88
89
|
// Delete action affects all selected rows.
|
|
89
90
|
await table.deleteRow(0);
|
|
90
91
|
|
|
91
|
-
await expect(page.getByRole('gridcell', { name: '
|
|
92
|
+
await expect(page.getByRole('gridcell', { name: 'Aut.' })).toHaveCount(0);
|
|
92
93
|
await expect(page.getByRole('gridcell', { name: 'Beatae.' })).toHaveCount(0);
|
|
93
94
|
await expect(table.grid.cellsWithinPlane('grid')).toHaveCount(0);
|
|
94
95
|
await page.close();
|
|
@@ -120,8 +121,22 @@ test.describe('Table', () => {
|
|
|
120
121
|
await page.close();
|
|
121
122
|
});
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
test('add column without changing format', async ({ browser, browserName }) => {
|
|
125
|
+
test.skip(browserName === 'webkit');
|
|
126
|
+
test.skip(browserName === 'firefox');
|
|
127
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
128
|
+
const table = new TableManager(page);
|
|
129
|
+
|
|
130
|
+
await table.grid.ready();
|
|
131
|
+
const newColumnLabel = 'DEFAULT FORMAT COL';
|
|
132
|
+
|
|
133
|
+
await table.addColumn({ label: newColumnLabel });
|
|
134
|
+
|
|
135
|
+
await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
|
|
136
|
+
await page.close();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('add column with format', async ({ browser, browserName }) => {
|
|
125
140
|
test.skip(browserName === 'webkit');
|
|
126
141
|
test.skip(browserName === 'firefox');
|
|
127
142
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
@@ -153,12 +168,41 @@ test.describe('Table', () => {
|
|
|
153
168
|
// Assert the first two switch checkboxes are checked.
|
|
154
169
|
await expect(page.getByTestId('table-switch').first()).toBeChecked();
|
|
155
170
|
await expect(page.getByTestId('table-switch').nth(1)).toBeChecked();
|
|
156
|
-
await expect(table.grid.cell(0, 0, 'grid')).toHaveText('
|
|
171
|
+
await expect(table.grid.cell(0, 0, 'grid')).toHaveText('Aut.');
|
|
157
172
|
await expect(table.grid.cell(0, 1, 'grid')).toHaveText('Beatae.');
|
|
158
173
|
|
|
159
174
|
await page.close();
|
|
160
175
|
});
|
|
161
176
|
|
|
177
|
+
test('add row and edit cell', async ({ browser, browserName }) => {
|
|
178
|
+
test.skip(browserName === 'webkit');
|
|
179
|
+
test.skip(browserName === 'firefox');
|
|
180
|
+
const { page } = await setupPage(browser, { url: storyUrl });
|
|
181
|
+
const table = new TableManager(page);
|
|
182
|
+
|
|
183
|
+
await table.grid.ready();
|
|
184
|
+
|
|
185
|
+
// Click the CTA row at the bottom to add a new row.
|
|
186
|
+
await table.grid.cell(0, 0, 'frozenRowsEnd').click();
|
|
187
|
+
|
|
188
|
+
// All schema fields are optional so the row is added directly to the grid (not as a draft).
|
|
189
|
+
const newCell = table.grid.cell(0, 10, 'grid');
|
|
190
|
+
await newCell.waitFor({ state: 'visible' });
|
|
191
|
+
|
|
192
|
+
// Focus should already be on the new row; engage edit mode.
|
|
193
|
+
await page.keyboard.press('Enter');
|
|
194
|
+
await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
|
|
195
|
+
|
|
196
|
+
// Type text and confirm.
|
|
197
|
+
const cellContent = 'New row content';
|
|
198
|
+
await page.keyboard.type(cellContent);
|
|
199
|
+
await page.keyboard.press('Enter');
|
|
200
|
+
|
|
201
|
+
// Assert the cell content is saved.
|
|
202
|
+
await expect(newCell).toHaveText(cellContent);
|
|
203
|
+
await page.close();
|
|
204
|
+
});
|
|
205
|
+
|
|
162
206
|
// TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
|
|
163
207
|
test.skip('extant relations work as expected', async ({ browser, browserName }) => {
|
|
164
208
|
test.skip(browserName === 'webkit');
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { type RefObject, useCallback, useMemo, useRef } from 'react';
|
|
5
|
+
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { type RefObject, useCallback, useContext, useMemo, useRef } from 'react';
|
|
7
7
|
|
|
8
|
+
import { type Database, type Type } from '@dxos/echo';
|
|
8
9
|
import { isMutable } from '@dxos/echo/internal';
|
|
9
|
-
import { useGlobalFilteredObjects } from '@dxos/plugin-search';
|
|
10
10
|
import { faker } from '@dxos/random';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
11
|
+
import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
|
|
12
|
+
import { useClientStory } from '@dxos/react-client/testing';
|
|
13
|
+
import { useGlobalFilteredObjects } from '@dxos/react-ui-searchlist';
|
|
14
14
|
import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
|
|
15
15
|
|
|
16
16
|
import { type TableController } from '../components';
|
|
@@ -20,15 +20,14 @@ import { Table } from '../types';
|
|
|
20
20
|
|
|
21
21
|
faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
|
|
22
22
|
|
|
23
|
-
export type TestTableModel = {
|
|
24
|
-
schema:
|
|
23
|
+
export type TestTableModel<T extends Type.AnyObj = Type.AnyObj> = {
|
|
24
|
+
schema: T | undefined;
|
|
25
25
|
table: Table.Table | undefined;
|
|
26
26
|
projection: ProjectionModel | undefined;
|
|
27
27
|
tableRef: RefObject<TableController | null>;
|
|
28
28
|
model: TableModel | undefined;
|
|
29
29
|
presentation: TablePresentation | undefined;
|
|
30
|
-
|
|
31
|
-
client: Client | undefined;
|
|
30
|
+
db: Database.Database | undefined;
|
|
32
31
|
handleInsertRow: () => void;
|
|
33
32
|
handleSaveView: () => void;
|
|
34
33
|
handleDeleteRows: (rowIndex: number, objects: any[]) => void;
|
|
@@ -39,15 +38,16 @@ export type TestTableModel = {
|
|
|
39
38
|
* Custom hook to create and manage a test table model for storybook demonstrations.
|
|
40
39
|
* Provides table data, schema, and handlers for table operations.
|
|
41
40
|
*/
|
|
42
|
-
export const useTestTableModel = (): TestTableModel => {
|
|
43
|
-
const
|
|
44
|
-
const { space } =
|
|
41
|
+
export const useTestTableModel = <T extends Type.AnyObj = Type.AnyObj>(): TestTableModel<T> => {
|
|
42
|
+
const registry = useContext(RegistryContext);
|
|
43
|
+
const { space } = useClientStory();
|
|
44
|
+
const db = space?.db;
|
|
45
45
|
|
|
46
|
-
const tables = useQuery(space, Filter.type(Table.Table));
|
|
46
|
+
const tables = useQuery(space?.db, Filter.type(Table.Table));
|
|
47
47
|
const table = tables.at(0);
|
|
48
48
|
const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
|
|
49
|
-
const schema = useSchema(
|
|
50
|
-
const projection = useProjectionModel(schema, table);
|
|
49
|
+
const schema = useSchema<T>(space?.db, typename);
|
|
50
|
+
const projection = useProjectionModel(schema, table, registry);
|
|
51
51
|
|
|
52
52
|
const features = useMemo(
|
|
53
53
|
() => ({
|
|
@@ -58,7 +58,7 @@ export const useTestTableModel = (): TestTableModel => {
|
|
|
58
58
|
[schema],
|
|
59
59
|
);
|
|
60
60
|
|
|
61
|
-
const objects = useQuery(
|
|
61
|
+
const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
|
|
62
62
|
const filteredObjects = useGlobalFilteredObjects(objects);
|
|
63
63
|
|
|
64
64
|
const tableRef = useRef<TableController>(null);
|
|
@@ -70,15 +70,15 @@ export const useTestTableModel = (): TestTableModel => {
|
|
|
70
70
|
tableRef.current?.update?.();
|
|
71
71
|
}, []);
|
|
72
72
|
|
|
73
|
-
const addRow = useAddRow({
|
|
73
|
+
const addRow = useAddRow({ db, schema });
|
|
74
74
|
|
|
75
75
|
const handleDeleteRows = useCallback(
|
|
76
76
|
(_: number, objects: any[]) => {
|
|
77
77
|
for (const object of objects) {
|
|
78
|
-
|
|
78
|
+
db?.remove(object);
|
|
79
79
|
}
|
|
80
80
|
},
|
|
81
|
-
[
|
|
81
|
+
[db],
|
|
82
82
|
);
|
|
83
83
|
|
|
84
84
|
const handleDeleteColumn = useCallback(
|
|
@@ -97,7 +97,7 @@ export const useTestTableModel = (): TestTableModel => {
|
|
|
97
97
|
rows: filteredObjects,
|
|
98
98
|
onInsertRow: addRow,
|
|
99
99
|
onDeleteRows: handleDeleteRows,
|
|
100
|
-
|
|
100
|
+
onColumnDelete: handleDeleteColumn,
|
|
101
101
|
onCellUpdate: handleCellUpdate,
|
|
102
102
|
onRowOrderChange: handleRowOrderChange,
|
|
103
103
|
});
|
|
@@ -113,9 +113,9 @@ export const useTestTableModel = (): TestTableModel => {
|
|
|
113
113
|
|
|
114
114
|
const presentation = useMemo(() => {
|
|
115
115
|
if (model) {
|
|
116
|
-
return new TablePresentation(model);
|
|
116
|
+
return new TablePresentation(registry, model);
|
|
117
117
|
}
|
|
118
|
-
}, [model]);
|
|
118
|
+
}, [registry, model]);
|
|
119
119
|
|
|
120
120
|
return {
|
|
121
121
|
schema,
|
|
@@ -124,8 +124,7 @@ export const useTestTableModel = (): TestTableModel => {
|
|
|
124
124
|
tableRef,
|
|
125
125
|
model,
|
|
126
126
|
presentation,
|
|
127
|
-
|
|
128
|
-
client,
|
|
127
|
+
db,
|
|
129
128
|
handleInsertRow,
|
|
130
129
|
handleSaveView,
|
|
131
130
|
handleDeleteRows,
|