@dxos/react-ui-table 0.8.4-main.2e9d522 → 0.8.4-main.5ea62a8
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-63UUF4IG.mjs +66 -0
- package/dist/lib/browser/chunk-63UUF4IG.mjs.map +7 -0
- package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-RQYXNWWN.mjs} +531 -460
- package/dist/lib/browser/chunk-RQYXNWWN.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +691 -625
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +6 -4
- 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-K734DJB7.mjs +67 -0
- package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-Y66YLSTC.mjs → chunk-QNDUGIS7.mjs} +531 -460
- package/dist/lib/node-esm/chunk-QNDUGIS7.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +691 -625
- 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 +6 -4
- 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/CreateRefPanel.d.ts +1 -1
- package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +50 -2
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +44 -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 +3 -2
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +42 -3
- 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 +7 -3
- package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -15
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -3
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/hooks/useAddRow.d.ts +2 -1
- package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- 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 +7 -5
- 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 +2 -2
- package/dist/types/src/model/table-sorting.d.ts.map +1 -1
- package/dist/types/src/testing/useTestTableModel.d.ts +4 -9
- 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/index.d.ts +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/types/schema.d.ts +24 -4
- package/dist/types/src/types/schema.d.ts.map +1 -1
- 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/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 +44 -39
- package/src/components/Table/ColumnActionsMenu.tsx +1 -1
- package/src/components/Table/ColumnSettings.tsx +1 -1
- package/src/components/Table/CreateRefPanel.tsx +25 -8
- package/src/components/Table/DynamicTable.stories.tsx +7 -4
- package/src/components/Table/DynamicTable.tsx +4 -3
- package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +40 -23
- package/src/components/Table/RowActionsMenu.tsx +3 -3
- package/src/components/Table/Table.stories.tsx +34 -23
- package/src/components/Table/Table.tsx +138 -112
- package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
- package/src/components/TableCellEditor/FormCellEditor.tsx +143 -42
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +11 -23
- package/src/components/TableCellEditor/TableCellEditor.tsx +64 -119
- package/src/components/TableToolbar/TableToolbar.tsx +2 -2
- package/src/components/testing.ts +1 -1
- package/src/hooks/useAddRow.ts +6 -4
- package/src/hooks/useTableModel.ts +1 -1
- package/src/model/modal-controller.ts +1 -1
- package/src/model/selection-model.ts +3 -2
- package/src/model/table-model.test.ts +3 -4
- package/src/model/table-model.ts +28 -23
- package/src/model/table-presentation.test.ts +4 -4
- package/src/model/table-presentation.ts +72 -30
- package/src/model/table-sorting.ts +3 -3
- package/src/playwright/smoke.spec.ts +129 -37
- package/src/testing/useTestTableModel.ts +4 -3
- package/src/translations.ts +2 -2
- package/src/types/index.ts +1 -1
- package/src/types/schema.ts +62 -5
- package/src/util/dynamic-table.ts +5 -5
- package/src/util/index.ts +0 -1
- package/src/util/schema.ts +1 -1
- 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/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/util/table.d.ts +0 -11
- package/dist/types/src/util/table.d.ts.map +0 -1
- package/src/components/TableCellEditor/extension.ts +0 -61
- package/src/util/table.ts +0 -49
package/src/model/table-model.ts
CHANGED
|
@@ -2,40 +2,41 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { computed, effect, signal
|
|
5
|
+
import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
|
|
6
6
|
|
|
7
7
|
import { Resource } from '@dxos/context';
|
|
8
8
|
import { Obj, Ref } from '@dxos/echo';
|
|
9
9
|
import {
|
|
10
10
|
type FieldSortType,
|
|
11
11
|
FormatEnum,
|
|
12
|
-
getValue,
|
|
13
|
-
setValue,
|
|
14
12
|
type JsonProp,
|
|
15
|
-
|
|
13
|
+
type JsonSchemaType,
|
|
16
14
|
getSchema,
|
|
15
|
+
getValue,
|
|
16
|
+
setValue,
|
|
17
17
|
toEffectSchema,
|
|
18
|
-
type JsonSchemaType,
|
|
19
18
|
} from '@dxos/echo-schema';
|
|
20
19
|
import { invariant } from '@dxos/invariant';
|
|
21
20
|
import { ObjectId } from '@dxos/keys';
|
|
22
|
-
import { isLiveObject } from '@dxos/live-object';
|
|
21
|
+
import { getSnapshot, isLiveObject } from '@dxos/live-object';
|
|
23
22
|
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
23
|
+
import { type Label } from '@dxos/react-ui';
|
|
24
24
|
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
25
25
|
import {
|
|
26
26
|
type DxGridAxisMeta,
|
|
27
|
-
type DxGridPlaneRange,
|
|
28
27
|
type DxGridPlanePosition,
|
|
28
|
+
type DxGridPlaneRange,
|
|
29
29
|
type DxGridPosition,
|
|
30
30
|
} from '@dxos/react-ui-grid';
|
|
31
|
-
import { type DataType, ProjectionModel, type PropertyType,
|
|
31
|
+
import { type DataType, ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
|
|
32
32
|
|
|
33
|
-
import {
|
|
34
|
-
import { TableSorting } from './table-sorting';
|
|
35
|
-
import { TableView } from '../types';
|
|
33
|
+
import { Table } from '../types';
|
|
36
34
|
import { touch } from '../util';
|
|
37
35
|
import { extractTagIds } from '../util/tag';
|
|
38
36
|
|
|
37
|
+
import { type SelectionMode, SelectionModel } from './selection-model';
|
|
38
|
+
import { TableSorting } from './table-sorting';
|
|
39
|
+
|
|
39
40
|
// Domain types for cell classification
|
|
40
41
|
export type TableCellType = 'standard' | 'draft' | 'header';
|
|
41
42
|
|
|
@@ -53,7 +54,7 @@ export type TableRow = Record<JsonProp, any> & { id: string };
|
|
|
53
54
|
|
|
54
55
|
export type TableRowAction = {
|
|
55
56
|
id: string;
|
|
56
|
-
|
|
57
|
+
label: Label;
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
export type TableFeatures = {
|
|
@@ -68,6 +69,8 @@ const defaultFeatures: TableFeatures = {
|
|
|
68
69
|
schemaEditable: false,
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
export type InsertRowResult = 'draft' | 'final';
|
|
73
|
+
|
|
71
74
|
export type TableModelProps<T extends TableRow = TableRow> = {
|
|
72
75
|
view: DataType.View;
|
|
73
76
|
schema: JsonSchemaType;
|
|
@@ -78,7 +81,7 @@ export type TableModelProps<T extends TableRow = TableRow> = {
|
|
|
78
81
|
pinnedRows?: { top: number[]; bottom: number[] };
|
|
79
82
|
rowActions?: TableRowAction[];
|
|
80
83
|
onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
|
|
81
|
-
onInsertRow?: (data?: any) =>
|
|
84
|
+
onInsertRow?: (data?: any) => InsertRowResult;
|
|
82
85
|
onDeleteRows?: (index: number, obj: T[]) => void;
|
|
83
86
|
onDeleteColumn?: (fieldId: string) => void;
|
|
84
87
|
onCellUpdate?: (cell: DxGridPosition) => void;
|
|
@@ -89,14 +92,14 @@ export type TableModelProps<T extends TableRow = TableRow> = {
|
|
|
89
92
|
export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
90
93
|
private readonly _view: DataType.View;
|
|
91
94
|
private readonly _projection: ProjectionModel;
|
|
92
|
-
private _table?:
|
|
95
|
+
private _table?: Table.Table;
|
|
93
96
|
|
|
94
97
|
private readonly _visibleRange = signal<DxGridPlaneRange>({
|
|
95
98
|
start: { row: 0, col: 0 },
|
|
96
99
|
end: { row: 0, col: 0 },
|
|
97
100
|
});
|
|
98
101
|
|
|
99
|
-
private readonly _onInsertRow?: (data?: any) =>
|
|
102
|
+
private readonly _onInsertRow?: (data?: any) => InsertRowResult;
|
|
100
103
|
private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
|
|
101
104
|
private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
|
|
102
105
|
private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
|
|
@@ -173,7 +176,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
173
176
|
return this._view;
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
public get table():
|
|
179
|
+
public get table(): Table.Table {
|
|
177
180
|
invariant(this._table, 'Model not initialized');
|
|
178
181
|
return this._table;
|
|
179
182
|
}
|
|
@@ -236,7 +239,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
236
239
|
|
|
237
240
|
protected override async _open(): Promise<void> {
|
|
238
241
|
const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
|
|
239
|
-
invariant(Obj.instanceOf(
|
|
242
|
+
invariant(Obj.instanceOf(Table.Table, presentation));
|
|
240
243
|
this._table = presentation;
|
|
241
244
|
|
|
242
245
|
this.initializeColumnMeta();
|
|
@@ -254,7 +257,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
254
257
|
return {
|
|
255
258
|
grid: meta,
|
|
256
259
|
frozenColsStart: { 0: { size: 30, resizeable: false } },
|
|
257
|
-
frozenColsEnd: { 0: { size:
|
|
260
|
+
frozenColsEnd: { 0: { size: 32, resizeable: false } },
|
|
258
261
|
};
|
|
259
262
|
});
|
|
260
263
|
}
|
|
@@ -334,11 +337,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
334
337
|
|
|
335
338
|
public getColumnCount = (): number => this._projection?.fields.length ?? 0;
|
|
336
339
|
|
|
337
|
-
public insertRow = ():
|
|
340
|
+
public insertRow = (): InsertRowResult => {
|
|
338
341
|
const result = this._onInsertRow?.();
|
|
339
|
-
if (result ===
|
|
342
|
+
if (result === 'draft' && this._draftRows.value.length === 0) {
|
|
340
343
|
this.createDraftRow();
|
|
341
344
|
}
|
|
345
|
+
return result ?? 'final';
|
|
342
346
|
};
|
|
343
347
|
|
|
344
348
|
private createDraftRow(): void {
|
|
@@ -393,14 +397,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
393
397
|
return false;
|
|
394
398
|
}
|
|
395
399
|
|
|
396
|
-
const
|
|
400
|
+
const insertRowResult = this._onInsertRow?.(draftRow.data);
|
|
397
401
|
|
|
398
|
-
if (
|
|
402
|
+
if (insertRowResult === 'final') {
|
|
399
403
|
const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
|
|
400
404
|
this._draftRows.value = newDraftRows;
|
|
401
405
|
}
|
|
402
406
|
|
|
403
|
-
return
|
|
407
|
+
return insertRowResult === 'final';
|
|
404
408
|
};
|
|
405
409
|
|
|
406
410
|
public deleteRow = (rowIndex: number): void => {
|
|
@@ -497,6 +501,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
497
501
|
setValue(snapshot, field.path, transformedValue);
|
|
498
502
|
|
|
499
503
|
const validationErrors = this.validateDraftRowData(snapshot);
|
|
504
|
+
// TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
|
|
500
505
|
if (validationErrors.length > 0) {
|
|
501
506
|
const error = validationErrors.find((err) => err.path === field.path);
|
|
502
507
|
if (error) {
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { Schema } from 'effect';
|
|
6
|
-
import { describe, expect, it
|
|
6
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
7
7
|
|
|
8
|
-
import { Obj } from '@dxos/echo';
|
|
9
8
|
import { TypedObject } from '@dxos/echo-schema';
|
|
10
9
|
import { live } from '@dxos/live-object';
|
|
11
10
|
import { createEchoSchema } from '@dxos/live-object/testing';
|
|
12
11
|
import { createView } from '@dxos/schema';
|
|
13
12
|
|
|
13
|
+
import { Table } from '../types';
|
|
14
|
+
|
|
14
15
|
import { TableModel, type TableModelProps } from './table-model';
|
|
15
16
|
import { TablePresentation } from './table-presentation';
|
|
16
|
-
import { TableView } from '../types';
|
|
17
17
|
|
|
18
18
|
describe('TablePresentation', () => {
|
|
19
19
|
describe('row reactivity', () => {
|
|
@@ -97,7 +97,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
|
|
|
97
97
|
|
|
98
98
|
const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
|
|
99
99
|
const schema = createEchoSchema(Test);
|
|
100
|
-
const table =
|
|
100
|
+
const table = Table.make();
|
|
101
101
|
const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
|
|
102
102
|
return new TableModel({ view, schema: schema.jsonSchema, ...props });
|
|
103
103
|
};
|
|
@@ -6,7 +6,7 @@ import { signal } from '@preact/signals-core';
|
|
|
6
6
|
import { isNotNullable } from 'effect/Predicate';
|
|
7
7
|
|
|
8
8
|
import { Obj } from '@dxos/echo';
|
|
9
|
-
import { FormatEnum,
|
|
9
|
+
import { FormatEnum, TypeEnum, getValue } from '@dxos/echo-schema';
|
|
10
10
|
import { cellClassesForFieldType, formatForDisplay } from '@dxos/react-ui-form';
|
|
11
11
|
import {
|
|
12
12
|
type DxGridCellValue,
|
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
toPlaneCellIndex,
|
|
17
17
|
} from '@dxos/react-ui-grid';
|
|
18
18
|
import { mx } from '@dxos/react-ui-theme';
|
|
19
|
-
import {
|
|
19
|
+
import { type FieldType, VIEW_FIELD_LIMIT } from '@dxos/schema';
|
|
20
20
|
|
|
21
|
-
import { type SelectionMode } from './selection-model';
|
|
22
|
-
import { type TableRow, type TableModel } from './table-model';
|
|
23
21
|
import { tableButtons, tableControls } from '../util';
|
|
24
22
|
|
|
23
|
+
import { type SelectionMode } from './selection-model';
|
|
24
|
+
import { type TableModel, type TableRow } from './table-model';
|
|
25
|
+
|
|
25
26
|
/**
|
|
26
27
|
* Presentation layer for a table component, handling cell rendering and grid display logic.
|
|
27
28
|
* Manages rendering of table cells, headers, selection columns, and action columns across
|
|
@@ -67,6 +68,9 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
67
68
|
case 'frozenRowsEnd':
|
|
68
69
|
cells = this.getDraftRowCells(range);
|
|
69
70
|
break;
|
|
71
|
+
case 'fixedEndStart':
|
|
72
|
+
cells = this.getDraftIconCells(range);
|
|
73
|
+
break;
|
|
70
74
|
case 'fixedEndEnd':
|
|
71
75
|
cells = this.getDraftActionCells(range);
|
|
72
76
|
break;
|
|
@@ -137,20 +141,22 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
137
141
|
};
|
|
138
142
|
|
|
139
143
|
const classes = [];
|
|
140
|
-
const formatClasses = cellClassesForFieldType(
|
|
144
|
+
const formatClasses = cellClassesForFieldType(props);
|
|
145
|
+
|
|
141
146
|
if (formatClasses) {
|
|
142
147
|
classes.push(formatClasses);
|
|
143
148
|
}
|
|
149
|
+
|
|
144
150
|
const rowSelectionClasses = cellClassesForRowSelection(
|
|
145
151
|
this.model.selection.isObjectSelected(obj),
|
|
146
152
|
this.model.selection.selectionMode,
|
|
147
153
|
);
|
|
154
|
+
|
|
148
155
|
if (rowSelectionClasses) {
|
|
149
156
|
classes.push(rowSelectionClasses);
|
|
150
157
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
158
|
+
|
|
159
|
+
cell.className = mx(classes);
|
|
154
160
|
|
|
155
161
|
// Arrays.
|
|
156
162
|
if (props.type === TypeEnum.Array) {
|
|
@@ -199,7 +205,7 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
199
205
|
const targetObj = getValue(obj, field.path)?.target;
|
|
200
206
|
if (targetObj) {
|
|
201
207
|
const dxn = Obj.getDXN(targetObj)?.toString();
|
|
202
|
-
cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-
|
|
208
|
+
cell.accessoryHtml = `<dx-ref-tag refId=${dxn} class="dx-button is-6 pli-[3px] pbe-[2px] min-bs-0 absolute inline-end-2 block-start-[.2rem]" data-dx-grid-action="accessory"><dx-icon icon="ph--link-simple--regular"/></dx-ref-tag>`;
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
|
|
@@ -275,23 +281,34 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
275
281
|
const fields = this.model.projection?.fields ?? [];
|
|
276
282
|
const draftRows = this.model.draftRows.value;
|
|
277
283
|
|
|
278
|
-
|
|
279
|
-
|
|
284
|
+
// Return cells of the CTA row if no draft row is active
|
|
285
|
+
if (draftRows.length === 0) {
|
|
280
286
|
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
288
|
+
value: '',
|
|
289
|
+
readonly: true,
|
|
290
|
+
className: 'dx-grid__row--cta__cell',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
|
|
295
|
+
const draftRow = draftRows[row];
|
|
296
|
+
for (let col = range.start.col; col <= range.end.col && col < fields.length; col++) {
|
|
297
|
+
const cellIndex = toPlaneCellIndex({ col, row });
|
|
298
|
+
const field = fields[col];
|
|
299
|
+
if (!field) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
285
302
|
|
|
286
|
-
|
|
303
|
+
this.createDataCell(cells, draftRow.data, field, col, row);
|
|
287
304
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
305
|
+
if (this.model.hasDraftRowValidationError(row, field.path)) {
|
|
306
|
+
const cellValue = cells[cellIndex];
|
|
307
|
+
if (cellValue) {
|
|
308
|
+
const existingClasses = cellValue.className || '';
|
|
309
|
+
const draftClasses = 'dx-grid__cell--flagged';
|
|
310
|
+
cellValue.className = existingClasses ? `${existingClasses} ${draftClasses}` : draftClasses;
|
|
311
|
+
}
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
314
|
}
|
|
@@ -310,14 +327,14 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
310
327
|
|
|
311
328
|
cells[toPlaneCellIndex({ col, row: 0 })] = {
|
|
312
329
|
// TODO(burdon): Use same logic as form for fallback title.
|
|
313
|
-
value:
|
|
314
|
-
readonly: true,
|
|
330
|
+
value: '',
|
|
315
331
|
resizeHandle: 'col',
|
|
316
332
|
accessoryHtml: `
|
|
333
|
+
<span class="grow min-is-0 truncate">${props.title ?? field.path}</span>
|
|
317
334
|
${direction !== undefined ? tableButtons.sort.render({ fieldId: field.id, direction }) : ''}
|
|
318
335
|
${tableButtons.columnSettings.render({ fieldId: field.id })}
|
|
319
336
|
`,
|
|
320
|
-
className: '!bg-toolbarSurface',
|
|
337
|
+
className: '!bg-toolbarSurface !text-description [&>div]:flex [&>div]:items-stretch',
|
|
321
338
|
};
|
|
322
339
|
}
|
|
323
340
|
|
|
@@ -400,14 +417,39 @@ export class TablePresentation<T extends TableRow = TableRow> {
|
|
|
400
417
|
const cells: DxGridPlaneCells = {};
|
|
401
418
|
const draftRows = this.model.draftRows.value;
|
|
402
419
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
420
|
+
// Return cells of the CTA row if no draft row is active
|
|
421
|
+
if (draftRows.length === 0) {
|
|
422
|
+
cells[toPlaneCellIndex({ col: 0, row: 0 })] = {
|
|
423
|
+
value: '',
|
|
424
|
+
className: 'dx-grid__row--cta__cell',
|
|
425
|
+
readonly: true,
|
|
426
|
+
};
|
|
427
|
+
} else {
|
|
428
|
+
for (let row = range.start.row; row <= range.end.row && row < draftRows.length; row++) {
|
|
429
|
+
const draftRow = draftRows[row];
|
|
430
|
+
const disabled = !draftRow.valid;
|
|
431
|
+
|
|
432
|
+
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
433
|
+
value: '',
|
|
434
|
+
readonly: true,
|
|
435
|
+
accessoryHtml: tableButtons.saveDraftRow.render({ rowIndex: row, disabled }),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return cells;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private getDraftIconCells(range: DxGridPlaneRange): DxGridPlaneCells {
|
|
444
|
+
const cells: DxGridPlaneCells = {};
|
|
445
|
+
const draftRows = this.model.draftRows.value;
|
|
406
446
|
|
|
447
|
+
for (let row = range.start.row; row <= range.end.row; row++) {
|
|
407
448
|
cells[toPlaneCellIndex({ col: 0, row })] = {
|
|
408
449
|
value: '',
|
|
409
450
|
readonly: true,
|
|
410
|
-
accessoryHtml:
|
|
451
|
+
accessoryHtml: '<dx-icon icon="ph--plus--regular" class="contents"></dx-icon>',
|
|
452
|
+
className: mx('[&>div]:grid [&>div]:place-content-center', draftRows.length < 1 && 'dx-grid__row--cta__cell'),
|
|
411
453
|
};
|
|
412
454
|
}
|
|
413
455
|
|
|
@@ -2,12 +2,12 @@
|
|
|
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 { type FieldSortType, FormatEnum, type SortDirectionType, TypeEnum, getValue } from '@dxos/echo-schema';
|
|
9
9
|
import { formatForDisplay } from '@dxos/react-ui-form';
|
|
10
|
-
import type {
|
|
10
|
+
import type { DataType, FieldType, ProjectionModel, PropertyType } from '@dxos/schema';
|
|
11
11
|
|
|
12
12
|
import { type TableRow } from './table-model';
|
|
13
13
|
|
|
@@ -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,6 +123,7 @@ 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
|
|
|
@@ -126,44 +136,9 @@ test.describe('Table', () => {
|
|
|
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,121 @@ test.describe('Table', () => {
|
|
|
183
158
|
|
|
184
159
|
await page.close();
|
|
185
160
|
});
|
|
161
|
+
|
|
162
|
+
test('extant relations work as expected', async ({ browser, browserName }) => {
|
|
163
|
+
test.skip(browserName === 'webkit');
|
|
164
|
+
test.skip(browserName === 'firefox');
|
|
165
|
+
const { page } = await setupPage(browser, { url: relationsStoryUrl });
|
|
166
|
+
|
|
167
|
+
// Wait for the page to load
|
|
168
|
+
await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
|
|
169
|
+
|
|
170
|
+
// Find the dx-grid element for the contactModel (second table)
|
|
171
|
+
// The contactModel is used in the second Table.Main component in the story
|
|
172
|
+
const dxGrid = page.locator('dx-grid').nth(1);
|
|
173
|
+
await dxGrid.waitFor({ state: 'visible' });
|
|
174
|
+
|
|
175
|
+
// Scroll to the last column (column 4)
|
|
176
|
+
await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
|
|
177
|
+
dxGridElement.scrollToColumn(4);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Click on the cell at aria-rowindex=0 aria-colindex=4 to focus it
|
|
181
|
+
const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
|
|
182
|
+
await targetCell.click();
|
|
183
|
+
|
|
184
|
+
// Click again to engage edit mode
|
|
185
|
+
await page.keyboard.press('Enter');
|
|
186
|
+
|
|
187
|
+
// Click on the combobox to open options
|
|
188
|
+
await page.getByRole('combobox').click();
|
|
189
|
+
await page.pause();
|
|
190
|
+
await page.getByPlaceholder('Search...').focus();
|
|
191
|
+
|
|
192
|
+
// Type the first few letters of an org name.
|
|
193
|
+
const orgName =
|
|
194
|
+
(await page
|
|
195
|
+
.locator('dx-grid')
|
|
196
|
+
.nth(0)
|
|
197
|
+
.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="0"] .dx-grid__cell__content')
|
|
198
|
+
.textContent()) ?? 'never';
|
|
199
|
+
await page.keyboard.type(orgName.substring(0, 4), { delay: 500 });
|
|
200
|
+
|
|
201
|
+
// Assert that there is an element with aria-selected on the page
|
|
202
|
+
await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
|
|
203
|
+
|
|
204
|
+
// Type the enter key
|
|
205
|
+
await page.keyboard.press('Enter');
|
|
206
|
+
|
|
207
|
+
// Save the result
|
|
208
|
+
await page.getByTestId('save-button').click();
|
|
209
|
+
|
|
210
|
+
// Assert that the cell element has the org name
|
|
211
|
+
await expect(targetCell).toHaveText(orgName);
|
|
212
|
+
|
|
213
|
+
await page.close();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('new relations work as expected', async ({ browser, browserName }) => {
|
|
217
|
+
test.skip(browserName === 'webkit');
|
|
218
|
+
test.skip(browserName === 'firefox');
|
|
219
|
+
const { page } = await setupPage(browser, { url: relationsStoryUrl });
|
|
220
|
+
|
|
221
|
+
// Wait for the page to load
|
|
222
|
+
await page.locator('dx-grid > .dx-grid').nth(1).waitFor({ state: 'visible' });
|
|
223
|
+
|
|
224
|
+
// Find the dx-grid element for the contactModel (second table)
|
|
225
|
+
// The contactModel is used in the second Table.Main component in the story
|
|
226
|
+
const dxGrid = page.locator('dx-grid').nth(1);
|
|
227
|
+
await dxGrid.waitFor({ state: 'visible' });
|
|
228
|
+
|
|
229
|
+
// Scroll to the last column (column 4)
|
|
230
|
+
await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
|
|
231
|
+
dxGridElement.scrollToColumn(4);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Click on the cell at aria-rowindex=0 aria-colindex=8 to focus it
|
|
235
|
+
const targetCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="4"]');
|
|
236
|
+
await targetCell.click();
|
|
237
|
+
|
|
238
|
+
// Click again to engage edit mode
|
|
239
|
+
await page.keyboard.press('Enter');
|
|
240
|
+
|
|
241
|
+
// Click on the combobox to open options
|
|
242
|
+
await page.getByRole('combobox').click();
|
|
243
|
+
await page.getByPlaceholder('Search...').focus();
|
|
244
|
+
|
|
245
|
+
// Type the first few letters of an org name.
|
|
246
|
+
const orgName = 'Sally';
|
|
247
|
+
await page.keyboard.type(orgName, { delay: 500 });
|
|
248
|
+
|
|
249
|
+
// Assert that there is an element with aria-selected on the page
|
|
250
|
+
await expect(page.locator('[role="option"][aria-selected]')).toBeVisible();
|
|
251
|
+
|
|
252
|
+
// Type the enter key
|
|
253
|
+
await page.keyboard.press('Enter');
|
|
254
|
+
await page.pause();
|
|
255
|
+
|
|
256
|
+
// Click the save button in the popover
|
|
257
|
+
await page.getByTestId('save-button').click();
|
|
258
|
+
|
|
259
|
+
// Assert that the cell element has the org name
|
|
260
|
+
await expect(targetCell).toHaveText(orgName);
|
|
261
|
+
|
|
262
|
+
// Make a change to a non-ref cell to check that populated refs don’t cause problems with snapshots or schemas.
|
|
263
|
+
await dxGrid.evaluate(async (dxGridElement: DxGrid) => {
|
|
264
|
+
dxGridElement.scrollToColumn(6);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const nonRefContent = faker.lorem.words(3);
|
|
268
|
+
const nonRefCell = dxGrid.locator('[data-dx-grid-plane="grid"] [aria-rowindex="0"][aria-colindex="6"]');
|
|
269
|
+
await nonRefCell.click();
|
|
270
|
+
await page.keyboard.press('Enter');
|
|
271
|
+
await page.getByTestId('grid.cell-editor').waitFor({ state: 'visible' });
|
|
272
|
+
await page.keyboard.type(nonRefContent, { delay: 500 });
|
|
273
|
+
await page.keyboard.press('Enter');
|
|
274
|
+
await expect(nonRefCell).toHaveText(nonRefContent);
|
|
275
|
+
|
|
276
|
+
await page.close();
|
|
277
|
+
});
|
|
186
278
|
});
|
|
@@ -13,7 +13,7 @@ import { useClientProvider } from '@dxos/react-client/testing';
|
|
|
13
13
|
import { DataType, ProjectionModel } from '@dxos/schema';
|
|
14
14
|
|
|
15
15
|
import { type TableController } from '../components';
|
|
16
|
-
import {
|
|
16
|
+
import { useAddRow, useTableModel } from '../hooks';
|
|
17
17
|
import { TablePresentation } from '../model';
|
|
18
18
|
|
|
19
19
|
faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
|
|
@@ -92,8 +92,9 @@ export const useTestTableModel = () => {
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
const handleInsertRow = useCallback(() => {
|
|
95
|
-
model?.insertRow();
|
|
96
|
-
|
|
95
|
+
const insertResult = model?.insertRow();
|
|
96
|
+
tableRef.current?.handleInsertRowResult?.(insertResult);
|
|
97
|
+
}, [model, tableRef.current]);
|
|
97
98
|
|
|
98
99
|
const handleSaveView = useCallback(() => {
|
|
99
100
|
model?.saveView();
|
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': 'Record type',
|
|
18
|
+
'new schema': 'New record type',
|
|
19
19
|
'continue label': 'Continue',
|
|
20
20
|
'create stack section label': 'Create table',
|
|
21
21
|
'add row label': 'Add row',
|
package/src/types/index.ts
CHANGED