@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.70d3990
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 +71 -0
- package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-KNC7XYT6.mjs} +573 -541
- package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +625 -698
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +16 -24
- package/dist/lib/browser/testing/index.mjs.map +3 -3
- package/dist/lib/browser/types/index.mjs +3 -3
- package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-CWSGS3Y2.mjs} +573 -541
- package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-MKTMOY46.mjs +72 -0
- package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +625 -698
- 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 +16 -24
- package/dist/lib/node-esm/testing/index.mjs.map +3 -3
- package/dist/lib/node-esm/types/index.mjs +3 -3
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
- package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts +1 -1
- package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +108 -0
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
- package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
- package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +8 -4
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +105 -4
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
- package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +3 -5
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -1
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +3 -2
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
- package/dist/types/src/hooks/useTableModel.d.ts +5 -6
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +0 -7
- 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 +14 -14
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +2 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/model/table-sorting.d.ts +9 -6
- package/dist/types/src/model/table-sorting.d.ts.map +1 -1
- package/dist/types/src/playwright/TableManager.d.ts +1 -1
- package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +21 -34
- package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/types/Table.d.ts +28 -0
- package/dist/types/src/types/Table.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +9 -9
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +0 -1
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/schema.d.ts +1 -1
- package/dist/types/src/util/schema.d.ts.map +1 -1
- package/dist/types/src/util/table-buttons.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +61 -59
- package/src/components/Table/ColumnActionsMenu.tsx +1 -1
- package/src/components/Table/ColumnSettings.tsx +1 -1
- package/src/components/Table/DynamicTable.stories.tsx +21 -17
- package/src/components/Table/DynamicTable.tsx +19 -13
- package/src/components/Table/Relations.stories.tsx +237 -0
- package/src/components/Table/RowActionsMenu.tsx +23 -15
- package/src/components/Table/Table.stories.tsx +103 -86
- package/src/components/Table/Table.tsx +140 -115
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +28 -66
- package/src/components/TableCellEditor/TableCellEditor.tsx +75 -225
- package/src/components/TableCellEditor/index.ts +1 -0
- package/src/components/TableToolbar/TableToolbar.tsx +34 -24
- package/src/components/testing.ts +4 -4
- package/src/hooks/index.ts +2 -1
- package/src/hooks/useAddRow.ts +7 -5
- package/src/hooks/useProjectionModel.ts +19 -0
- package/src/hooks/useTableModel.ts +12 -14
- package/src/model/modal-controller.ts +1 -29
- package/src/model/selection-model.ts +3 -2
- package/src/model/table-model.test.ts +21 -12
- package/src/model/table-model.ts +62 -56
- package/src/model/table-presentation.test.ts +25 -23
- package/src/model/table-presentation.ts +82 -41
- package/src/model/table-sorting.ts +32 -14
- package/src/playwright/TableManager.ts +3 -3
- package/src/playwright/smoke.spec.ts +132 -38
- package/src/testing/useTestTableModel.ts +34 -22
- package/src/translations.ts +2 -2
- package/src/types/Table.ts +89 -0
- package/src/types/index.ts +1 -1
- package/src/util/dynamic-table.ts +20 -24
- package/src/util/index.ts +0 -1
- package/src/util/schema.ts +4 -4
- package/src/util/table-buttons.ts +4 -3
- package/src/util/table-controls.ts +4 -1
- package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
- package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
- package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
- package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
- package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
- package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
- package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
- package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
- package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
- package/dist/types/src/types/schema.d.ts +0 -8
- package/dist/types/src/types/schema.d.ts.map +0 -1
- package/dist/types/src/util/table.d.ts +0 -11
- package/dist/types/src/util/table.d.ts.map +0 -1
- package/src/components/Table/CreateRefPanel.tsx +0 -86
- package/src/components/Table/relations.stories.tsx +0 -116
- package/src/components/TableCellEditor/extension.ts +0 -61
- package/src/types/schema.ts +0 -16
- package/src/util/table.ts +0 -49
|
@@ -2,15 +2,24 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type ReadonlySignal, type Signal, computed, signal } from '@preact/signals-core';
|
|
6
6
|
import orderBy from 'lodash.orderby';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Format, TypeEnum, getValue } from '@dxos/echo/internal';
|
|
9
9
|
import { formatForDisplay } from '@dxos/react-ui-form';
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
type FieldSortType,
|
|
12
|
+
type FieldType,
|
|
13
|
+
type ProjectionModel,
|
|
14
|
+
type PropertyType,
|
|
15
|
+
type SortDirectionType,
|
|
16
|
+
type View,
|
|
17
|
+
} from '@dxos/schema';
|
|
11
18
|
|
|
12
19
|
import { type TableRow } from './table-model';
|
|
13
20
|
|
|
21
|
+
// TODO(wittjosiah): Remove. Use ordering in queries instead.
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Represents the local sort state.
|
|
16
25
|
* - undefined: No local sort set, fallback to view sort.
|
|
@@ -43,19 +52,27 @@ type LocalSort = { type: 'cleared' } | { type: 'active'; sort: FieldSortType } |
|
|
|
43
52
|
export class TableSorting<T extends TableRow> {
|
|
44
53
|
private readonly _displayToDataIndex = new Map<number, number>();
|
|
45
54
|
private readonly _rows: Signal<T[]>;
|
|
55
|
+
private readonly _getView: () => View.View;
|
|
56
|
+
private readonly _projection: ProjectionModel;
|
|
46
57
|
private readonly _localSort = signal<LocalSort>(undefined);
|
|
47
58
|
private readonly _isDirty: ReadonlySignal<boolean>;
|
|
48
59
|
public readonly sortedRows: ReadonlySignal<T[]>;
|
|
49
60
|
|
|
50
|
-
constructor(
|
|
51
|
-
rows
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
constructor({
|
|
62
|
+
rows,
|
|
63
|
+
projection,
|
|
64
|
+
getView,
|
|
65
|
+
}: {
|
|
66
|
+
rows: Signal<T[]>;
|
|
67
|
+
getView: () => View.View;
|
|
68
|
+
projection: ProjectionModel;
|
|
69
|
+
}) {
|
|
55
70
|
this._rows = rows;
|
|
71
|
+
this._getView = getView;
|
|
72
|
+
this._projection = projection;
|
|
56
73
|
this._isDirty = computed(() => {
|
|
57
74
|
const local = this._localSort.value;
|
|
58
|
-
const viewSort = this.
|
|
75
|
+
const viewSort = this._getView().sort?.[0];
|
|
59
76
|
if (local?.type === 'cleared') {
|
|
60
77
|
return viewSort !== undefined;
|
|
61
78
|
}
|
|
@@ -83,7 +100,7 @@ export class TableSorting<T extends TableRow> {
|
|
|
83
100
|
return local.sort;
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
return this.
|
|
103
|
+
return this._getView().sort?.[0];
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/**
|
|
@@ -128,10 +145,11 @@ export class TableSorting<T extends TableRow> {
|
|
|
128
145
|
|
|
129
146
|
public save(): void {
|
|
130
147
|
if (this._localSort.value !== undefined) {
|
|
148
|
+
const view = this._getView();
|
|
131
149
|
if (this._localSort.value.type === 'active') {
|
|
132
|
-
|
|
150
|
+
view.sort = [this._localSort.value.sort];
|
|
133
151
|
} else {
|
|
134
|
-
|
|
152
|
+
view.sort = [];
|
|
135
153
|
}
|
|
136
154
|
this._localSort.value = undefined;
|
|
137
155
|
}
|
|
@@ -193,7 +211,7 @@ export class TableSorting<T extends TableRow> {
|
|
|
193
211
|
* For single select fields, the sort value is determined by the index of the option in the
|
|
194
212
|
* options array. This ensures sort order matches the configured option order.
|
|
195
213
|
*/
|
|
196
|
-
if (format ===
|
|
214
|
+
if (format === Format.TypeFormat.SingleSelect) {
|
|
197
215
|
const options = props.options;
|
|
198
216
|
if (!options) {
|
|
199
217
|
return undefined;
|
|
@@ -206,7 +224,7 @@ export class TableSorting<T extends TableRow> {
|
|
|
206
224
|
// TODO(ZaymonFC): Should we format this returned value for display based on the referenced prop format?
|
|
207
225
|
// Right now we're going to be sorting based on the raw values.
|
|
208
226
|
// Maybe we need to recurse this function with the referenced prop format and field?
|
|
209
|
-
if (format ===
|
|
227
|
+
if (format === Format.TypeFormat.Ref && field.referencePath) {
|
|
210
228
|
const refValue = getValue(value.target, field.referencePath);
|
|
211
229
|
return refValue;
|
|
212
230
|
}
|
|
@@ -53,12 +53,12 @@ export class TableManager {
|
|
|
53
53
|
await this.page.getByTestId(TABLE_SELECTORS.columnSort[direction]).click();
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
public async addColumn({ label, format }: { label: string; format:
|
|
56
|
+
public async addColumn({ label, format }: { label: string; format: string }): Promise<void> {
|
|
57
57
|
await this.page.getByTestId(TABLE_SELECTORS.newColumnButton).click();
|
|
58
58
|
await this.page.getByRole('combobox').click();
|
|
59
|
-
await this.page.getByLabel(
|
|
59
|
+
await this.page.getByLabel(format).click();
|
|
60
60
|
await this.page.getByPlaceholder('Property label.').click();
|
|
61
61
|
await this.page.getByPlaceholder('Property label.').fill(label);
|
|
62
|
-
await this.page.getByRole('button', { name: '
|
|
62
|
+
await this.page.getByRole('button', { name: 'Save' }).click();
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
import { expect, test } from '@playwright/test';
|
|
6
6
|
|
|
7
|
+
import { type DxGrid } from '@dxos/lit-grid';
|
|
8
|
+
import { faker } from '@dxos/random';
|
|
7
9
|
import { setupPage, storybookUrl } from '@dxos/test-utils/playwright';
|
|
8
10
|
|
|
9
11
|
import { TableManager } from './TableManager';
|
|
10
12
|
|
|
11
13
|
const storyUrl = storybookUrl('ui-react-ui-table-table--default', 9004);
|
|
14
|
+
const relationsStoryUrl = storybookUrl('ui-react-ui-table-relations--default', 9004);
|
|
12
15
|
|
|
13
16
|
// NOTE(ZaymonFC): This test suite relies on the faker seed being set to 0 in the story.
|
|
14
17
|
test.describe('Table', () => {
|
|
15
18
|
test('Loads', async ({ browser, browserName }) => {
|
|
16
19
|
test.skip(browserName === 'webkit');
|
|
20
|
+
test.skip(browserName === 'firefox');
|
|
17
21
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
18
22
|
const table = new TableManager(page);
|
|
19
23
|
|
|
@@ -23,6 +27,7 @@ test.describe('Table', () => {
|
|
|
23
27
|
|
|
24
28
|
test('sort', async ({ browser, browserName }) => {
|
|
25
29
|
test.skip(browserName === 'webkit');
|
|
30
|
+
test.skip(browserName === 'firefox');
|
|
26
31
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
27
32
|
const table = new TableManager(page);
|
|
28
33
|
|
|
@@ -39,6 +44,7 @@ test.describe('Table', () => {
|
|
|
39
44
|
|
|
40
45
|
test('selection', async ({ browser, browserName }) => {
|
|
41
46
|
test.skip(browserName === 'webkit');
|
|
47
|
+
test.skip(browserName === 'firefox');
|
|
42
48
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
43
49
|
const table = new TableManager(page);
|
|
44
50
|
|
|
@@ -60,6 +66,7 @@ test.describe('Table', () => {
|
|
|
60
66
|
|
|
61
67
|
test('delete row', async ({ browser, browserName }) => {
|
|
62
68
|
test.skip(browserName === 'webkit');
|
|
69
|
+
test.skip(browserName === 'firefox');
|
|
63
70
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
64
71
|
const table = new TableManager(page);
|
|
65
72
|
|
|
@@ -71,6 +78,7 @@ test.describe('Table', () => {
|
|
|
71
78
|
|
|
72
79
|
test('delete row--select all', async ({ browser, browserName }) => {
|
|
73
80
|
test.skip(browserName === 'webkit');
|
|
81
|
+
test.skip(browserName === 'firefox');
|
|
74
82
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
75
83
|
const table = new TableManager(page);
|
|
76
84
|
|
|
@@ -88,6 +96,7 @@ test.describe('Table', () => {
|
|
|
88
96
|
|
|
89
97
|
test('delete column', async ({ browser, browserName }) => {
|
|
90
98
|
test.skip(browserName === 'webkit');
|
|
99
|
+
test.skip(browserName === 'firefox');
|
|
91
100
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
92
101
|
const table = new TableManager(page);
|
|
93
102
|
|
|
@@ -114,56 +123,22 @@ test.describe('Table', () => {
|
|
|
114
123
|
// Rest of add column test remains the same as it's a more complex flow.
|
|
115
124
|
test('add column', async ({ browser, browserName }) => {
|
|
116
125
|
test.skip(browserName === 'webkit');
|
|
126
|
+
test.skip(browserName === 'firefox');
|
|
117
127
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
118
128
|
const table = new TableManager(page);
|
|
119
129
|
|
|
120
130
|
await table.grid.ready();
|
|
121
131
|
const newColumnLabel = 'TEST LABEL';
|
|
122
132
|
|
|
123
|
-
await table.addColumn({ label: newColumnLabel, format: '
|
|
133
|
+
await table.addColumn({ label: newColumnLabel, format: 'Number' });
|
|
124
134
|
|
|
125
135
|
await expect(page.getByRole('gridcell', { name: newColumnLabel })).toBeVisible();
|
|
126
136
|
await page.close();
|
|
127
137
|
});
|
|
128
138
|
|
|
129
|
-
test('
|
|
130
|
-
test.skip(browserName === 'webkit');
|
|
131
|
-
const { page } = await setupPage(browser, { url: storyUrl });
|
|
132
|
-
const table = new TableManager(page);
|
|
133
|
-
|
|
134
|
-
await table.grid.ready();
|
|
135
|
-
|
|
136
|
-
// Reference existing object.
|
|
137
|
-
// Scroll over to the manager column.
|
|
138
|
-
await table.grid.panByWheel(10000, 0);
|
|
139
|
-
|
|
140
|
-
await table.grid.cell(4, 0, 'grid').click();
|
|
141
|
-
// TODO(wittjosiah): Surprisingly long delay needed here.
|
|
142
|
-
await page.keyboard.press('A', { delay: 1_000 });
|
|
143
|
-
await page.keyboard.press('u', { delay: 1_000 });
|
|
144
|
-
await page.getByRole('option', { name: 'Aut.' }).click();
|
|
145
|
-
|
|
146
|
-
// Assert that the value is shown in the cell.
|
|
147
|
-
await expect(page.getByRole('gridcell', { name: 'Aut.' }).first()).toBeVisible();
|
|
148
|
-
|
|
149
|
-
// Create new object.
|
|
150
|
-
await table.grid.cell(4, 1, 'grid').click();
|
|
151
|
-
// TODO(wittjosiah): Surprisingly long delay needed here.
|
|
152
|
-
await page.keyboard.type('t', { delay: 1_000 });
|
|
153
|
-
await page.keyboard.type('e', { delay: 1_000 });
|
|
154
|
-
await page.keyboard.type('s', { delay: 1_000 });
|
|
155
|
-
await page.keyboard.type('t', { delay: 1_000 });
|
|
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' }).first().click();
|
|
161
|
-
|
|
162
|
-
await page.close();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('test toggles', async ({ browser, browserName }) => {
|
|
139
|
+
test.skip('test toggles', async ({ browser, browserName }) => {
|
|
166
140
|
test.skip(browserName === 'webkit');
|
|
141
|
+
test.skip(browserName === 'firefox');
|
|
167
142
|
const { page } = await setupPage(browser, { url: storyUrl });
|
|
168
143
|
const table = new TableManager(page);
|
|
169
144
|
|
|
@@ -183,4 +158,123 @@ test.describe('Table', () => {
|
|
|
183
158
|
|
|
184
159
|
await page.close();
|
|
185
160
|
});
|
|
161
|
+
|
|
162
|
+
// TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
|
|
163
|
+
test.skip('extant relations work as expected', async ({ browser, browserName }) => {
|
|
164
|
+
test.skip(browserName === 'webkit');
|
|
165
|
+
test.skip(browserName === 'firefox');
|
|
166
|
+
const { page } = await setupPage(browser, { url: relationsStoryUrl });
|
|
167
|
+
|
|
168
|
+
// Wait for the page to load
|
|
169
|
+
await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
|
|
170
|
+
|
|
171
|
+
// Find the dx-grid element for the contactModel (second table)
|
|
172
|
+
// The contactModel is used in the second Table.Main component in the story
|
|
173
|
+
const dxGrid = page.locator('dx-grid').nth(1);
|
|
174
|
+
await dxGrid.waitFor({ state: 'visible' });
|
|
175
|
+
|
|
176
|
+
// Scroll to the last column (column 4)
|
|
177
|
+
await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
|
|
178
|
+
dxGridElement.scrollToColumn(4);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Click on the cell at aria-rowindex=0 aria-colindex=4 to focus it
|
|
182
|
+
const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
|
|
183
|
+
await targetCell.click();
|
|
184
|
+
|
|
185
|
+
// Click again to engage edit mode
|
|
186
|
+
await page.keyboard.press('Enter');
|
|
187
|
+
|
|
188
|
+
// Click on the combobox to open options
|
|
189
|
+
await page.getByRole('combobox').click();
|
|
190
|
+
await page.pause();
|
|
191
|
+
await page.getByPlaceholder('Search…').focus();
|
|
192
|
+
|
|
193
|
+
// Type the first few letters of an org name.
|
|
194
|
+
const orgName =
|
|
195
|
+
(await page
|
|
196
|
+
.locator('dx-grid')
|
|
197
|
+
.nth(0)
|
|
198
|
+
.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="0"] .dx-grid__cell__content')
|
|
199
|
+
.textContent()) ?? 'never';
|
|
200
|
+
await page.keyboard.type(orgName.substring(0, 4), { delay: 500 });
|
|
201
|
+
|
|
202
|
+
// Assert that there is an element with aria-selected on the page
|
|
203
|
+
await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
|
|
204
|
+
|
|
205
|
+
// Type the enter key
|
|
206
|
+
await page.keyboard.press('Enter');
|
|
207
|
+
|
|
208
|
+
// Save the result
|
|
209
|
+
await page.getByTestId('save-button').click();
|
|
210
|
+
|
|
211
|
+
// Assert that the cell element has the org name
|
|
212
|
+
await expect(targetCell).toHaveText(orgName);
|
|
213
|
+
|
|
214
|
+
await page.close();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// TODO(wittjosiah): Remove? Conflicts with story play function which is run as a unit test anyways.
|
|
218
|
+
test.skip('new relations work as expected', async ({ browser, browserName }) => {
|
|
219
|
+
test.skip(browserName === 'webkit');
|
|
220
|
+
test.skip(browserName === 'firefox');
|
|
221
|
+
const { page } = await setupPage(browser, { url: relationsStoryUrl });
|
|
222
|
+
|
|
223
|
+
// Wait for the page to load
|
|
224
|
+
await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
|
|
225
|
+
|
|
226
|
+
// Find the dx-grid element for the contactModel (second table)
|
|
227
|
+
// The contactModel is used in the second Table.Main component in the story
|
|
228
|
+
const dxGrid = page.locator('dx-grid').nth(1);
|
|
229
|
+
await dxGrid.waitFor({ state: 'visible' });
|
|
230
|
+
|
|
231
|
+
// Scroll to the last column (column 4)
|
|
232
|
+
await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
|
|
233
|
+
dxGridElement.scrollToColumn(4);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Click on the cell at aria-rowindex=0 aria-colindex=8 to focus it
|
|
237
|
+
const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
|
|
238
|
+
await targetCell.click();
|
|
239
|
+
|
|
240
|
+
// Click again to engage edit mode
|
|
241
|
+
await page.keyboard.press('Enter');
|
|
242
|
+
|
|
243
|
+
// Click on the combobox to open options
|
|
244
|
+
await page.getByRole('combobox').click();
|
|
245
|
+
await page.getByPlaceholder('Search…').focus();
|
|
246
|
+
|
|
247
|
+
// Type the first few letters of an org name.
|
|
248
|
+
const orgName = 'Sally';
|
|
249
|
+
await page.keyboard.type(orgName, { delay: 500 });
|
|
250
|
+
|
|
251
|
+
// Assert that there is an element with aria-selected on the page
|
|
252
|
+
await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
|
|
253
|
+
|
|
254
|
+
// Type the enter key
|
|
255
|
+
await page.keyboard.press('Enter');
|
|
256
|
+
await page.pause();
|
|
257
|
+
|
|
258
|
+
// Click the save button in the popover
|
|
259
|
+
await page.getByTestId('create-referenced-object-form').getByTestId('save-button').click();
|
|
260
|
+
|
|
261
|
+
// Assert that the cell element has the org name
|
|
262
|
+
await expect(targetCell).toHaveText(orgName);
|
|
263
|
+
|
|
264
|
+
// Make a change to a non-ref cell to check that populated refs don’t cause problems with snapshots or schemas.
|
|
265
|
+
await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
|
|
266
|
+
dxGridElement.scrollToColumn(6);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const nonRefContent = faker.lorem.words(3);
|
|
270
|
+
const nonRefCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="6"]');
|
|
271
|
+
await nonRefCell.click();
|
|
272
|
+
await page.keyboard.press('Enter');
|
|
273
|
+
await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
|
|
274
|
+
await page.keyboard.type(nonRefContent, { delay: 500 });
|
|
275
|
+
await page.keyboard.press('Enter');
|
|
276
|
+
await expect(nonRefCell).toHaveText(nonRefContent);
|
|
277
|
+
|
|
278
|
+
await page.close();
|
|
279
|
+
});
|
|
186
280
|
});
|
|
@@ -2,40 +2,52 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
import { type RefObject, useCallback, useMemo, useRef } from 'react';
|
|
6
7
|
|
|
7
|
-
import { isMutable
|
|
8
|
+
import { isMutable } from '@dxos/echo/internal';
|
|
8
9
|
import { useGlobalFilteredObjects } from '@dxos/plugin-search';
|
|
9
10
|
import { faker } from '@dxos/random';
|
|
10
|
-
import { useClient } from '@dxos/react-client';
|
|
11
|
-
import { Filter, useQuery, useSchema } from '@dxos/react-client/echo';
|
|
11
|
+
import { type Client, useClient } from '@dxos/react-client';
|
|
12
|
+
import { Filter, type Space, useQuery, useSchema } from '@dxos/react-client/echo';
|
|
12
13
|
import { useClientProvider } from '@dxos/react-client/testing';
|
|
13
|
-
import {
|
|
14
|
+
import { type ProjectionModel, getTypenameFromQuery } from '@dxos/schema';
|
|
14
15
|
|
|
15
16
|
import { type TableController } from '../components';
|
|
16
|
-
import {
|
|
17
|
-
import { TablePresentation } from '../model';
|
|
17
|
+
import { useAddRow, useProjectionModel, useTableModel } from '../hooks';
|
|
18
|
+
import { type TableModel, TablePresentation } from '../model';
|
|
19
|
+
import { Table } from '../types';
|
|
18
20
|
|
|
19
21
|
faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
|
|
20
22
|
|
|
23
|
+
export type TestTableModel = {
|
|
24
|
+
schema: Schema.Schema.AnyNoContext | undefined;
|
|
25
|
+
table: Table.Table | undefined;
|
|
26
|
+
projection: ProjectionModel | undefined;
|
|
27
|
+
tableRef: RefObject<TableController | null>;
|
|
28
|
+
model: TableModel | undefined;
|
|
29
|
+
presentation: TablePresentation | undefined;
|
|
30
|
+
space: Space | undefined;
|
|
31
|
+
client: Client | undefined;
|
|
32
|
+
handleInsertRow: () => void;
|
|
33
|
+
handleSaveView: () => void;
|
|
34
|
+
handleDeleteRows: (rowIndex: number, objects: any[]) => void;
|
|
35
|
+
handleDeleteColumn: (fieldId: string) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
21
38
|
/**
|
|
22
39
|
* Custom hook to create and manage a test table model for storybook demonstrations.
|
|
23
40
|
* Provides table data, schema, and handlers for table operations.
|
|
24
41
|
*/
|
|
25
|
-
export const useTestTableModel = () => {
|
|
42
|
+
export const useTestTableModel = (): TestTableModel => {
|
|
26
43
|
const client = useClient();
|
|
27
44
|
const { space } = useClientProvider();
|
|
28
45
|
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const projection = useMemo(() => {
|
|
35
|
-
if (schema && view?.projection) {
|
|
36
|
-
return new ProjectionModel(toJsonSchema(schema), view.projection);
|
|
37
|
-
}
|
|
38
|
-
}, [schema, view?.projection]);
|
|
46
|
+
const tables = useQuery(space, Filter.type(Table.Table));
|
|
47
|
+
const table = tables.at(0);
|
|
48
|
+
const typename = table?.view.target?.query ? getTypenameFromQuery(table.view.target.query.ast) : undefined;
|
|
49
|
+
const schema = useSchema(client, space, typename);
|
|
50
|
+
const projection = useProjectionModel(schema, table);
|
|
39
51
|
|
|
40
52
|
const features = useMemo(
|
|
41
53
|
() => ({
|
|
@@ -79,8 +91,7 @@ export const useTestTableModel = () => {
|
|
|
79
91
|
);
|
|
80
92
|
|
|
81
93
|
const model = useTableModel({
|
|
82
|
-
|
|
83
|
-
schema: jsonSchema,
|
|
94
|
+
object: table,
|
|
84
95
|
projection,
|
|
85
96
|
features,
|
|
86
97
|
rows: filteredObjects,
|
|
@@ -92,7 +103,8 @@ export const useTestTableModel = () => {
|
|
|
92
103
|
});
|
|
93
104
|
|
|
94
105
|
const handleInsertRow = useCallback(() => {
|
|
95
|
-
model?.insertRow();
|
|
106
|
+
const insertResult = model?.insertRow();
|
|
107
|
+
tableRef.current?.handleInsertRowResult?.(insertResult);
|
|
96
108
|
}, [model]);
|
|
97
109
|
|
|
98
110
|
const handleSaveView = useCallback(() => {
|
|
@@ -107,7 +119,7 @@ export const useTestTableModel = () => {
|
|
|
107
119
|
|
|
108
120
|
return {
|
|
109
121
|
schema,
|
|
110
|
-
|
|
122
|
+
table,
|
|
111
123
|
projection,
|
|
112
124
|
tableRef,
|
|
113
125
|
model,
|
package/src/translations.ts
CHANGED
|
@@ -14,8 +14,8 @@ export const translations = [
|
|
|
14
14
|
'create object label': 'Create table',
|
|
15
15
|
'table name placeholder': 'Table name',
|
|
16
16
|
'settings title': 'Table settings',
|
|
17
|
-
'table schema label': '
|
|
18
|
-
'new schema': 'New
|
|
17
|
+
'table schema label': 'Type',
|
|
18
|
+
'new schema': 'New type',
|
|
19
19
|
'continue label': 'Continue',
|
|
20
20
|
'create stack section label': 'Create table',
|
|
21
21
|
'add row label': 'Add row',
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Match from 'effect/Match';
|
|
6
|
+
import * as Schema from 'effect/Schema';
|
|
7
|
+
|
|
8
|
+
import { Obj, Ref, Type } from '@dxos/echo';
|
|
9
|
+
import { FormInputAnnotation, type JsonPath, type JsonSchemaType, LabelAnnotation } from '@dxos/echo/internal';
|
|
10
|
+
import { type SimpleType } from '@dxos/effect';
|
|
11
|
+
import { View, ViewAnnotation, getSchemaProperties } from '@dxos/schema';
|
|
12
|
+
|
|
13
|
+
const TableSchema = Schema.Struct({
|
|
14
|
+
name: Schema.String.pipe(Schema.optional),
|
|
15
|
+
|
|
16
|
+
view: Type.Ref(View.View).pipe(FormInputAnnotation.set(false)),
|
|
17
|
+
|
|
18
|
+
sizes: Schema.Record({
|
|
19
|
+
// TODO(wittjosiah): Should be JsonPath.
|
|
20
|
+
key: Schema.String,
|
|
21
|
+
value: Schema.Number,
|
|
22
|
+
}).pipe(Schema.mutable, FormInputAnnotation.set(false)),
|
|
23
|
+
}).pipe(
|
|
24
|
+
Type.Obj({
|
|
25
|
+
typename: 'dxos.org/type/Table',
|
|
26
|
+
version: '0.2.0',
|
|
27
|
+
}),
|
|
28
|
+
LabelAnnotation.set(['name']),
|
|
29
|
+
ViewAnnotation.set(true),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export interface Table extends Schema.Schema.Type<typeof TableSchema> {}
|
|
33
|
+
export interface TableEncoded extends Schema.Schema.Encoded<typeof TableSchema> {}
|
|
34
|
+
export const Table: Schema.Schema<Table, TableEncoded> = TableSchema;
|
|
35
|
+
|
|
36
|
+
type MakeProps = Omit<Partial<Obj.MakeProps<typeof Table>>, 'view'> & {
|
|
37
|
+
view: View.View;
|
|
38
|
+
/** Required to auto-size columns. */
|
|
39
|
+
jsonSchema?: JsonSchemaType;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Make a table as a view of a data set.
|
|
44
|
+
*/
|
|
45
|
+
export const make = ({ name, sizes = {}, view, jsonSchema }: MakeProps): Table => {
|
|
46
|
+
const table = Obj.make(Table, { name, view: Ref.make(view), sizes });
|
|
47
|
+
|
|
48
|
+
// Preset sizes.
|
|
49
|
+
if (jsonSchema) {
|
|
50
|
+
const schema = Type.toEffectSchema(jsonSchema);
|
|
51
|
+
const properties = getSchemaProperties(schema.ast, {});
|
|
52
|
+
for (const property of properties) {
|
|
53
|
+
if (sizes?.[property.name]) {
|
|
54
|
+
table.sizes[property.name] = sizes[property.name];
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
Match.type<SimpleType>().pipe(
|
|
59
|
+
Match.when('boolean', () => {
|
|
60
|
+
table.sizes[property.name as JsonPath] = 100;
|
|
61
|
+
}),
|
|
62
|
+
Match.when('number', () => {
|
|
63
|
+
table.sizes[property.name as JsonPath] = 100;
|
|
64
|
+
}),
|
|
65
|
+
Match.orElse(() => {
|
|
66
|
+
// Noop.
|
|
67
|
+
}),
|
|
68
|
+
)(property.type);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return table;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//
|
|
76
|
+
// V1
|
|
77
|
+
//
|
|
78
|
+
|
|
79
|
+
export const TableV1 = Schema.Struct({
|
|
80
|
+
sizes: Schema.Record({
|
|
81
|
+
key: Schema.String,
|
|
82
|
+
value: Schema.Number,
|
|
83
|
+
}).pipe(Schema.mutable),
|
|
84
|
+
}).pipe(
|
|
85
|
+
Type.Obj({
|
|
86
|
+
typename: 'dxos.org/type/Table',
|
|
87
|
+
version: '0.1.0',
|
|
88
|
+
}),
|
|
89
|
+
);
|
package/src/types/index.ts
CHANGED
|
@@ -2,20 +2,18 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
6
|
|
|
7
|
-
import { Obj } from '@dxos/echo';
|
|
8
|
-
import { getTypename, toJsonSchema } from '@dxos/echo-schema';
|
|
9
|
-
import type { JsonSchemaType, SortDirectionType } from '@dxos/echo-schema';
|
|
7
|
+
import { Filter, type JsonSchema, Obj, Query, Ref, Type } from '@dxos/echo';
|
|
10
8
|
import {
|
|
11
|
-
createView,
|
|
12
|
-
type DataType,
|
|
13
|
-
getSchemaFromPropertyDefinitions,
|
|
14
9
|
ProjectionModel,
|
|
15
10
|
type SchemaPropertyDefinition,
|
|
11
|
+
type SortDirectionType,
|
|
12
|
+
View,
|
|
13
|
+
getSchemaFromPropertyDefinitions,
|
|
16
14
|
} from '@dxos/schema';
|
|
17
15
|
|
|
18
|
-
import {
|
|
16
|
+
import { Table } from '../types';
|
|
19
17
|
|
|
20
18
|
// TODO(ZaymonFC): Upstream these extra fields to SchemaPropertyDefinition to enhance schema-tools schema creation.
|
|
21
19
|
type PropertyDisplayProps = {
|
|
@@ -38,14 +36,14 @@ export const getBaseSchema = ({
|
|
|
38
36
|
}: {
|
|
39
37
|
typename?: string;
|
|
40
38
|
properties?: TablePropertyDefinition[];
|
|
41
|
-
jsonSchema?:
|
|
39
|
+
jsonSchema?: JsonSchema.JsonSchema;
|
|
42
40
|
schema?: Schema.Schema.AnyNoContext;
|
|
43
|
-
}): { typename: string; jsonSchema:
|
|
41
|
+
}): { typename: string; jsonSchema: JsonSchema.JsonSchema } => {
|
|
44
42
|
if (typename && properties) {
|
|
45
43
|
const schema = getSchemaFromPropertyDefinitions(typename, properties);
|
|
46
44
|
return { typename: schema.typename, jsonSchema: schema.jsonSchema };
|
|
47
45
|
} else if (schema) {
|
|
48
|
-
return { typename: getTypename(schema)!, jsonSchema: toJsonSchema(schema) };
|
|
46
|
+
return { typename: Type.getTypename(schema)!, jsonSchema: Type.toJsonSchema(schema) };
|
|
49
47
|
} else if (typename && jsonSchema) {
|
|
50
48
|
return { typename, jsonSchema };
|
|
51
49
|
} else {
|
|
@@ -54,34 +52,32 @@ export const getBaseSchema = ({
|
|
|
54
52
|
};
|
|
55
53
|
|
|
56
54
|
export const makeDynamicTable = ({
|
|
57
|
-
typename,
|
|
58
55
|
jsonSchema,
|
|
59
56
|
properties,
|
|
60
57
|
}: {
|
|
61
|
-
|
|
62
|
-
jsonSchema: JsonSchemaType;
|
|
58
|
+
jsonSchema: JsonSchema.JsonSchema;
|
|
63
59
|
properties?: TablePropertyDefinition[];
|
|
64
|
-
}): { projection: ProjectionModel;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
typename,
|
|
60
|
+
}): { projection: ProjectionModel; object: Table.Table } => {
|
|
61
|
+
const view = View.make({
|
|
62
|
+
query: Query.select(Filter.everything()),
|
|
68
63
|
jsonSchema,
|
|
69
|
-
presentation: table,
|
|
70
64
|
...(properties && { fields: properties.map((property) => property.name) }),
|
|
71
65
|
});
|
|
66
|
+
const object = Obj.make(Table.Table, { view: Ref.make(view), sizes: {} });
|
|
72
67
|
|
|
73
68
|
const projection = new ProjectionModel(jsonSchema, view.projection);
|
|
69
|
+
projection.normalizeView();
|
|
74
70
|
if (properties && projection.fields) {
|
|
75
|
-
setProperties(view, projection,
|
|
71
|
+
setProperties(view, projection, object, properties);
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
return { projection,
|
|
74
|
+
return { projection, object };
|
|
79
75
|
};
|
|
80
76
|
|
|
81
77
|
const setProperties = (
|
|
82
|
-
view:
|
|
78
|
+
view: View.View,
|
|
83
79
|
projection: ProjectionModel,
|
|
84
|
-
table:
|
|
80
|
+
table: Table.Table,
|
|
85
81
|
properties: TablePropertyDefinition[],
|
|
86
82
|
) => {
|
|
87
83
|
for (const property of properties) {
|
|
@@ -101,7 +97,7 @@ const setProperties = (
|
|
|
101
97
|
|
|
102
98
|
if (property.sort) {
|
|
103
99
|
const fieldId = field.id;
|
|
104
|
-
view.
|
|
100
|
+
view.sort = [{ fieldId, direction: property.sort }];
|
|
105
101
|
}
|
|
106
102
|
}
|
|
107
103
|
}
|