@dxos/react-ui-table 0.9.1-staging.ee54ba693a → 0.11.0
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/chunk-types.mjs +65 -0
- package/dist/lib/chunk-types.mjs.map +1 -0
- package/dist/lib/chunk-useTableModel.mjs +1595 -0
- package/dist/lib/chunk-useTableModel.mjs.map +1 -0
- package/dist/lib/index.mjs +1133 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/testing.mjs +88 -0
- package/dist/lib/testing.mjs.map +1 -0
- package/dist/lib/translations.mjs +30 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/lib/types.mjs +2 -0
- package/dist/types/src/components/Table/DynamicTable.d.ts +4 -1
- package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +3 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +1 -0
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +3 -9
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +2 -0
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableContent.d.ts +1 -1
- package/dist/types/src/components/Table/TableContent.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableContext.d.ts +10 -0
- package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
- package/dist/types/src/components/Table/TableToolbar.d.ts +1 -1
- package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/Table/index.d.ts +1 -0
- package/dist/types/src/components/Table/index.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +1 -1
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts +1 -1
- package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts +8 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/modal-controller.d.ts +1 -1
- package/dist/types/src/model/modal-controller.d.ts.map +1 -1
- package/dist/types/src/model/selection-model.d.ts +1 -1
- package/dist/types/src/model/selection-model.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +42 -7
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts +20 -5
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/model/table-view-state.d.ts +5 -1
- package/dist/types/src/model/table-view-state.d.ts.map +1 -1
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/src/util/dynamic-table.d.ts +1 -1
- package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
- package/dist/types/src/util/table-controls.d.ts +5 -3
- package/dist/types/src/util/table-controls.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +48 -50
- package/src/components/Table/DynamicTable.stories.tsx +59 -1
- package/src/components/Table/DynamicTable.tsx +16 -8
- package/src/components/Table/Relations.stories.tsx +2 -2
- package/src/components/Table/Table.stories.tsx +76 -2
- package/src/components/Table/Table.tsx +3 -15
- package/src/components/Table/TableContent.tsx +6 -5
- package/src/components/Table/TableContext.ts +21 -0
- package/src/components/Table/index.ts +2 -0
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -3
- package/src/components/TableCellEditor/TableCellEditor.tsx +27 -22
- package/src/components/testing.ts +1 -1
- package/src/hooks/useProjectionModel.ts +1 -1
- package/src/hooks/useTableModel.ts +18 -6
- package/src/model/modal-controller.ts +1 -1
- package/src/model/selection-model.ts +1 -1
- package/src/model/table-model.test.ts +3 -3
- package/src/model/table-model.ts +148 -31
- package/src/model/table-presentation.test.ts +59 -1
- package/src/model/table-presentation.ts +171 -81
- package/src/model/table-view-state.ts +2 -2
- package/src/testing/useTestTableModel.ts +1 -1
- package/src/types/index.ts +4 -0
- package/src/util/dynamic-table.test.ts +1 -1
- package/src/util/dynamic-table.ts +1 -1
- package/src/util/table-buttons.ts +1 -1
- package/src/util/table-controls.ts +18 -10
- package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
- package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
- package/dist/lib/browser/chunk-K4J2CKTZ.mjs +0 -1751
- package/dist/lib/browser/chunk-K4J2CKTZ.mjs.map +0 -7
- package/dist/lib/browser/chunk-VV3P3ADH.mjs +0 -67
- package/dist/lib/browser/chunk-VV3P3ADH.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -1416
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs +0 -115
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/browser/translations.mjs +0 -39
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/browser/types/index.mjs +0 -8
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-E6V274ZW.mjs +0 -1753
- package/dist/lib/node-esm/chunk-E6V274ZW.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs +0 -68
- package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -1417
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -116
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/lib/node-esm/translations.mjs +0 -40
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- package/dist/lib/node-esm/types/index.mjs +0 -9
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
-
import { useContext, useEffect,
|
|
6
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type Database, Obj } from '@dxos/echo';
|
|
9
|
-
import { useSelection, useSelectionActions
|
|
9
|
+
import { useManagerOptional, useSelection, useSelectionActions } from '@dxos/react-ui-attention';
|
|
10
10
|
import { type ProjectionModel } from '@dxos/schema';
|
|
11
11
|
|
|
12
12
|
import {
|
|
@@ -24,6 +24,13 @@ export type UseTableModelProps<T extends TableRow = TableRow> = {
|
|
|
24
24
|
db?: Database.Database;
|
|
25
25
|
rows?: T[];
|
|
26
26
|
rowActions?: TableRowAction[];
|
|
27
|
+
/**
|
|
28
|
+
* Rows to select when the model is built. Overrides the selection read from the view state for
|
|
29
|
+
* the table object — pass this when the surrounding container owns the selection under a
|
|
30
|
+
* different context id (e.g. a type article keyed by the type's URI), so switching into the
|
|
31
|
+
* table preserves what was already selected.
|
|
32
|
+
*/
|
|
33
|
+
selection?: readonly string[];
|
|
27
34
|
onSelectionChanged?: (selection: string[]) => void;
|
|
28
35
|
onRowAction?: (actionId: string, data: T) => void;
|
|
29
36
|
} & Pick<
|
|
@@ -38,14 +45,19 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
38
45
|
rows,
|
|
39
46
|
rowActions,
|
|
40
47
|
features,
|
|
48
|
+
selection,
|
|
41
49
|
onSelectionChanged,
|
|
42
50
|
onRowAction,
|
|
43
51
|
...props
|
|
44
52
|
}: UseTableModelProps<T>): TableModel<T> | undefined => {
|
|
45
53
|
const registry = useContext(RegistryContext);
|
|
46
|
-
const viewState =
|
|
54
|
+
const viewState = useManagerOptional();
|
|
47
55
|
const selected = useSelection(object && Obj.getURI(object), 'multi');
|
|
48
|
-
|
|
56
|
+
// Read at construction time, not frozen into the effect's deps. The model is rebuilt whenever any
|
|
57
|
+
// of its inputs change identity, and each rebuild re-seeds from here — a frozen snapshot would
|
|
58
|
+
// reset the selection to whatever it was when the table mounted, discarding the user's clicks.
|
|
59
|
+
const initialSelection = useRef<readonly string[]>(selection ?? selected);
|
|
60
|
+
initialSelection.current = selection ?? selected;
|
|
49
61
|
|
|
50
62
|
const [model, setModel] = useState<TableModel<T>>();
|
|
51
63
|
useEffect(() => {
|
|
@@ -64,7 +76,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
64
76
|
change: createEchoChangeCallback<T>(object),
|
|
65
77
|
features,
|
|
66
78
|
rowActions,
|
|
67
|
-
initialSelection,
|
|
79
|
+
initialSelection: [...initialSelection.current],
|
|
68
80
|
onRowAction,
|
|
69
81
|
...props,
|
|
70
82
|
});
|
|
@@ -77,7 +89,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
77
89
|
void model?.close();
|
|
78
90
|
};
|
|
79
91
|
// TODO(burdon): Trigger if callbacks change?
|
|
80
|
-
}, [registry, viewState, object, projection, features, rowActions
|
|
92
|
+
}, [registry, viewState, object, projection, features, rowActions]);
|
|
81
93
|
|
|
82
94
|
// Update data when rows change.
|
|
83
95
|
useEffect(() => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom, type Registry } from '@effect-atom/atom
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom';
|
|
6
6
|
import { type MutableRefObject, type RefObject } from 'react';
|
|
7
7
|
|
|
8
8
|
export type ColumnSettingsMode = { type: 'create' } | { type: 'edit'; fieldId: string };
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Registry } from '@effect-atom/atom
|
|
5
|
+
import { Registry } from '@effect-atom/atom';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
8
8
|
|
|
9
9
|
import { DXN, Filter, JsonSchema, Query, Type } from '@dxos/echo';
|
|
10
10
|
import { createEchoSchema } from '@dxos/echo/testing';
|
|
11
|
-
import { LocalBackend, MemoryBackend,
|
|
11
|
+
import { LocalBackend, MemoryBackend, ViewState } from '@dxos/react-ui-attention';
|
|
12
12
|
import { ProjectionModel, ViewModel, createDirectChangeCallback } from '@dxos/schema';
|
|
13
13
|
|
|
14
14
|
import { Table } from '../types';
|
|
@@ -137,7 +137,7 @@ describe('TableModel sort view state', () => {
|
|
|
137
137
|
const registry = Registry.make();
|
|
138
138
|
const local = new LocalBackend({ registry, storage });
|
|
139
139
|
disposables.push(local);
|
|
140
|
-
const viewState = new
|
|
140
|
+
const viewState = new ViewState.Manager({ registry, backends: { memory: new MemoryBackend(), local } });
|
|
141
141
|
const projection = new ProjectionModel({
|
|
142
142
|
registry,
|
|
143
143
|
view,
|
package/src/model/table-model.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom, type Registry } from '@effect-atom/atom
|
|
5
|
+
import { Atom, type Registry } from '@effect-atom/atom';
|
|
6
6
|
|
|
7
7
|
import { Resource } from '@dxos/context';
|
|
8
8
|
import { type Database, Format, Obj, Order, Query, type QueryAST, Ref, Type, type View } from '@dxos/echo';
|
|
@@ -12,14 +12,16 @@ import { SchemaEx } from '@dxos/effect';
|
|
|
12
12
|
import { invariant } from '@dxos/invariant';
|
|
13
13
|
import { EntityId } from '@dxos/keys';
|
|
14
14
|
import { type Label } from '@dxos/react-ui';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { ViewState } from '@dxos/react-ui-attention';
|
|
16
|
+
import { parseValue } from '@dxos/react-ui-form';
|
|
17
17
|
import {
|
|
18
18
|
type DxGridAxisMeta,
|
|
19
|
+
type DxGridPlane,
|
|
19
20
|
type DxGridPlanePosition,
|
|
20
21
|
type DxGridPlaneRange,
|
|
21
22
|
type DxGridPosition,
|
|
22
23
|
} from '@dxos/react-ui-grid';
|
|
24
|
+
import { formatForEditing } from '@dxos/schema';
|
|
23
25
|
import { type ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
|
|
24
26
|
|
|
25
27
|
import { type Table } from '../types';
|
|
@@ -88,12 +90,18 @@ export type TableFeatures = {
|
|
|
88
90
|
selection: { enabled: boolean; mode?: SelectionMode };
|
|
89
91
|
dataEditable: boolean;
|
|
90
92
|
schemaEditable: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Number of leading data columns to pin (freeze) so they remain visible while the remaining
|
|
95
|
+
* columns scroll horizontally. Pinned columns are rendered in the grid's frozen-column planes.
|
|
96
|
+
*/
|
|
97
|
+
pinColumns: number;
|
|
91
98
|
};
|
|
92
99
|
|
|
93
100
|
const defaultFeatures: TableFeatures = {
|
|
94
101
|
selection: { enabled: true, mode: 'multiple' },
|
|
95
102
|
dataEditable: false,
|
|
96
103
|
schemaEditable: false,
|
|
104
|
+
pinColumns: 0,
|
|
97
105
|
};
|
|
98
106
|
|
|
99
107
|
export type InsertRowResult = 'draft' | 'final';
|
|
@@ -108,7 +116,7 @@ export type TableModelProps<T extends TableRow = TableRow> = {
|
|
|
108
116
|
* stored in the `tableSortAspect` view state keyed by the table URI; otherwise it falls back to an
|
|
109
117
|
* ephemeral atom (e.g. isolated stories/tests with no `ViewStateProvider`).
|
|
110
118
|
*/
|
|
111
|
-
viewState?:
|
|
119
|
+
viewState?: ViewState.Manager;
|
|
112
120
|
/**
|
|
113
121
|
* Callbacks to wrap mutations in Obj.update().
|
|
114
122
|
* Use createEchoChangeCallback() for ECHO-backed objects or createDirectChangeCallback() for plain objects.
|
|
@@ -148,9 +156,9 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
148
156
|
private readonly _rows: Atom.Writable<T[]>;
|
|
149
157
|
private readonly _draftRows: Atom.Writable<DraftRow<T>[]>;
|
|
150
158
|
// Optional per-context UI state manager; when present the column sort is view-state-backed.
|
|
151
|
-
private readonly _viewState?:
|
|
159
|
+
private readonly _viewState?: ViewState.Manager;
|
|
152
160
|
// Per-user sort state - backed by the `tableSortAspect` view state (keyed by table URI) when a
|
|
153
|
-
//
|
|
161
|
+
// ViewState.Manager is provided, else an ephemeral atom. Changes are local until saved to the view.
|
|
154
162
|
private readonly _inMemorySort: Atom.Writable<FieldSortType | undefined>;
|
|
155
163
|
// Derived atom for persisted sort from view.query.ast.
|
|
156
164
|
private readonly _persistedSort: Atom.Atom<FieldSortType | undefined>;
|
|
@@ -210,7 +218,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
210
218
|
this._rows = Atom.make<T[]>([]);
|
|
211
219
|
this._draftRows = Atom.make<DraftRow<T>[]>([]);
|
|
212
220
|
// View-state-backed atom survives remount/reload (local backend, keyed by table URI); the
|
|
213
|
-
// ephemeral fallback preserves prior behaviour when no
|
|
221
|
+
// ephemeral fallback preserves prior behaviour when no ViewState.Manager is supplied.
|
|
214
222
|
this._inMemorySort = this._viewState
|
|
215
223
|
? this._viewState.atom(tableSortAspect, this.id)
|
|
216
224
|
: Atom.make<FieldSortType | undefined>(undefined);
|
|
@@ -353,6 +361,24 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
353
361
|
return this._cellUpdateCounter;
|
|
354
362
|
}
|
|
355
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Reports the grid's currently-visible cell range (called by the presentation from `getCells`). Drives the
|
|
366
|
+
* per-row subscriptions that keep cells reactive to external mutations, so it must be wired for the grid to
|
|
367
|
+
* reflect edits made outside it. Guarded so identical ranges don't churn the subscriptions.
|
|
368
|
+
*/
|
|
369
|
+
public setVisibleRange(range: DxGridPlaneRange): void {
|
|
370
|
+
const current = this._registry.get(this._visibleRange);
|
|
371
|
+
if (
|
|
372
|
+
current.start.row === range.start.row &&
|
|
373
|
+
current.end.row === range.end.row &&
|
|
374
|
+
current.start.col === range.start.col &&
|
|
375
|
+
current.end.col === range.end.col
|
|
376
|
+
) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this._registry.set(this._visibleRange, range);
|
|
380
|
+
}
|
|
381
|
+
|
|
356
382
|
/**
|
|
357
383
|
* Change a row using the configured change callback.
|
|
358
384
|
* Use this instead of directly mutating to ensure consistent mutation handling.
|
|
@@ -365,6 +391,57 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
365
391
|
return this._features;
|
|
366
392
|
}
|
|
367
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Number of leading data columns pinned (frozen), clamped to the available field count.
|
|
396
|
+
*/
|
|
397
|
+
public get pinColumns(): number {
|
|
398
|
+
const fieldCount = this.getColumnCount();
|
|
399
|
+
return Math.max(0, Math.min(this._features.pinColumns, fieldCount));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Number of frozen-start columns preceding the pinned data columns (the selection column, if enabled).
|
|
404
|
+
*/
|
|
405
|
+
public get selectionColumns(): number {
|
|
406
|
+
return this._features.selection.enabled ? 1 : 0;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Total number of columns rendered in the `frozenColsStart` plane: the selection column (if enabled)
|
|
411
|
+
* plus the pinned data columns. Single source of truth for the frozen-start boundary.
|
|
412
|
+
*/
|
|
413
|
+
public get frozenColsStart(): number {
|
|
414
|
+
return this.selectionColumns + this.pinColumns;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Maps a plane-local column index to a field index for the data-bearing planes.
|
|
419
|
+
* Returns undefined for non-data columns (e.g. the selection or action columns).
|
|
420
|
+
*
|
|
421
|
+
* Column layout across planes:
|
|
422
|
+
* - `frozenColsStart`: [selection?] then the first `pinColumns` data fields.
|
|
423
|
+
* - `grid` / `frozenRowsStart` / `frozenRowsEnd`: data fields starting at index `pinColumns`.
|
|
424
|
+
*/
|
|
425
|
+
public getFieldIndex(plane: DxGridPlane, planeCol: number): number | undefined {
|
|
426
|
+
switch (plane) {
|
|
427
|
+
case 'frozenColsStart':
|
|
428
|
+
case 'fixedStartStart':
|
|
429
|
+
case 'fixedEndStart': {
|
|
430
|
+
const dataCol = planeCol - this.selectionColumns;
|
|
431
|
+
if (dataCol < 0 || dataCol >= this.pinColumns) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
return dataCol;
|
|
435
|
+
}
|
|
436
|
+
case 'grid':
|
|
437
|
+
case 'frozenRowsStart':
|
|
438
|
+
case 'frozenRowsEnd':
|
|
439
|
+
return this.pinColumns + planeCol;
|
|
440
|
+
default:
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
368
445
|
/**
|
|
369
446
|
* Gets the current sort state.
|
|
370
447
|
* Priority: in-memory sort (local changes) > persisted sort (from query AST).
|
|
@@ -420,14 +497,38 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
420
497
|
// The projection.fields atom handles subscriptions to view changes internally.
|
|
421
498
|
this._columnMeta = Atom.make((get) => {
|
|
422
499
|
const fields = get(this._projection.fields);
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
500
|
+
const pin = this.pinColumns;
|
|
501
|
+
const selectionColumns = this.selectionColumns;
|
|
502
|
+
|
|
503
|
+
const fieldSize = (field: (typeof fields)[number]) => ({
|
|
504
|
+
size: this.table.sizes[field.path] ?? 256,
|
|
505
|
+
resizeable: true,
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// The scrolling `grid` plane holds the un-pinned fields, re-keyed to plane-local indices.
|
|
509
|
+
const grid = Object.fromEntries(fields.slice(pin).map((field, index: number) => [index, fieldSize(field)]));
|
|
510
|
+
|
|
511
|
+
// The `frozenColsStart` plane holds the selection column (if enabled) followed by the pinned fields.
|
|
512
|
+
const frozenColsStart: Record<number, { size: number; resizeable: boolean }> = {};
|
|
513
|
+
if (selectionColumns > 0) {
|
|
514
|
+
frozenColsStart[0] = {
|
|
515
|
+
size: 32,
|
|
516
|
+
resizeable: false,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
fields.slice(0, pin).forEach((field, index: number) => {
|
|
520
|
+
frozenColsStart[selectionColumns + index] = fieldSize(field);
|
|
521
|
+
});
|
|
426
522
|
|
|
427
523
|
return {
|
|
428
|
-
grid
|
|
429
|
-
frozenColsStart
|
|
430
|
-
frozenColsEnd: {
|
|
524
|
+
grid,
|
|
525
|
+
frozenColsStart,
|
|
526
|
+
frozenColsEnd: {
|
|
527
|
+
0: {
|
|
528
|
+
size: 32,
|
|
529
|
+
resizeable: false,
|
|
530
|
+
},
|
|
531
|
+
},
|
|
431
532
|
};
|
|
432
533
|
});
|
|
433
534
|
}
|
|
@@ -442,9 +543,10 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
442
543
|
// Track row subscriptions for cleanup.
|
|
443
544
|
const rowSubscriptions = new Map<string, () => void>();
|
|
444
545
|
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
546
|
+
// (Re)subscribe to each visible row's mutations so external edits (e.g. from a companion form) bump the
|
|
547
|
+
// cell-update counter and repaint the affected cells. Must re-run when either the visible range changes (scroll)
|
|
548
|
+
// or the row set changes (data load / sort), since rows arrive asynchronously after the range is first reported.
|
|
549
|
+
const subscribeVisibleRows = () => {
|
|
448
550
|
for (const [id, unsub] of rowSubscriptions) {
|
|
449
551
|
unsub();
|
|
450
552
|
rowSubscriptions.delete(id);
|
|
@@ -453,21 +555,25 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
453
555
|
const { start, end } = this._registry.get(this._visibleRange);
|
|
454
556
|
const sortedRows = this._registry.get(this._sortedRows);
|
|
455
557
|
|
|
456
|
-
// Subscribe to each visible row's changes.
|
|
457
558
|
for (let row = start.row; row <= end.row && row < sortedRows.length; row++) {
|
|
458
559
|
const obj = sortedRows[row];
|
|
459
560
|
if (Obj.isObject(obj) && !rowSubscriptions.has(obj.id)) {
|
|
561
|
+
const cell = { row, col: start.col, plane: 'grid' as const };
|
|
460
562
|
const unsub = Obj.subscribe(obj, () => {
|
|
461
563
|
// Increment cell update counter to notify UI of changes.
|
|
462
564
|
this._registry.set(this._cellUpdateCounter, this._registry.get(this._cellUpdateCounter) + 1);
|
|
463
|
-
this._onCellUpdate?.(
|
|
565
|
+
this._onCellUpdate?.(cell);
|
|
464
566
|
});
|
|
465
567
|
rowSubscriptions.set(obj.id, unsub);
|
|
466
568
|
}
|
|
467
569
|
}
|
|
468
|
-
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const visibleRangeUnsubscribe = this._registry.subscribe(this._visibleRange, subscribeVisibleRows);
|
|
573
|
+
const sortedRowsUnsubscribe = this._registry.subscribe(this._sortedRows, subscribeVisibleRows);
|
|
469
574
|
this._ctx.onDispose(() => {
|
|
470
575
|
visibleRangeUnsubscribe();
|
|
576
|
+
sortedRowsUnsubscribe();
|
|
471
577
|
for (const unsub of rowSubscriptions.values()) {
|
|
472
578
|
unsub();
|
|
473
579
|
}
|
|
@@ -625,11 +731,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
625
731
|
public getCellData = (cell: DxGridPosition): any => {
|
|
626
732
|
const { col, row, plane } = cell;
|
|
627
733
|
const fields = this._projection?.getFields() ?? [];
|
|
628
|
-
|
|
734
|
+
const fieldIndex = this.getFieldIndex(plane, col);
|
|
735
|
+
if (fieldIndex === undefined || fieldIndex < 0 || fieldIndex >= fields.length) {
|
|
629
736
|
return undefined;
|
|
630
737
|
}
|
|
631
738
|
|
|
632
|
-
const field = fields[
|
|
739
|
+
const field = fields[fieldIndex];
|
|
633
740
|
let value: any;
|
|
634
741
|
|
|
635
742
|
if (plane === 'frozenRowsEnd') {
|
|
@@ -671,11 +778,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
671
778
|
|
|
672
779
|
public validateCellData = async ({ col, row, plane }: DxGridPosition, value: any): Promise<ValidationResult> => {
|
|
673
780
|
const fields = this._projection?.getFields() ?? [];
|
|
674
|
-
|
|
781
|
+
const fieldIndex = this.getFieldIndex(plane, col);
|
|
782
|
+
if (fieldIndex === undefined || fieldIndex < 0 || fieldIndex >= fields.length) {
|
|
675
783
|
return { valid: false, error: 'Invalid column index' };
|
|
676
784
|
}
|
|
677
785
|
|
|
678
|
-
const field = fields[
|
|
786
|
+
const field = fields[fieldIndex];
|
|
679
787
|
const { props } = this._projection.getFieldProjection(field.id);
|
|
680
788
|
const transformedValue = editorTextToCellValue(props, value);
|
|
681
789
|
|
|
@@ -727,11 +835,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
727
835
|
public setCellData = (cell: DxGridPosition, value: any): void => {
|
|
728
836
|
const { col, row, plane } = cell;
|
|
729
837
|
const fields = this._projection?.getFields() ?? [];
|
|
730
|
-
|
|
838
|
+
const fieldIndex = this.getFieldIndex(plane, col);
|
|
839
|
+
if (fieldIndex === undefined || fieldIndex < 0 || fieldIndex >= fields.length) {
|
|
731
840
|
return;
|
|
732
841
|
}
|
|
733
842
|
|
|
734
|
-
const field = fields[
|
|
843
|
+
const field = fields[fieldIndex];
|
|
735
844
|
const { props } = this._projection.getFieldProjection(field.id);
|
|
736
845
|
const transformedValue = editorTextToCellValue(props, value);
|
|
737
846
|
|
|
@@ -762,9 +871,16 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
762
871
|
* @param {DxGridPlanePosition} position - The position of the cell to update.
|
|
763
872
|
* @param {(value: any) => any} update - A function that takes the current value and returns the updated value.
|
|
764
873
|
*/
|
|
765
|
-
public updateCellData(
|
|
874
|
+
public updateCellData(
|
|
875
|
+
{ col, row, plane = 'grid' }: DxGridPlanePosition & { plane?: DxGridPlane },
|
|
876
|
+
update: (value: any) => any,
|
|
877
|
+
): void {
|
|
766
878
|
const fields = this._projection?.getFields() ?? [];
|
|
767
|
-
const
|
|
879
|
+
const fieldIndex = this.getFieldIndex(plane, col);
|
|
880
|
+
if (fieldIndex === undefined || fieldIndex < 0 || fieldIndex >= fields.length) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const field = fields[fieldIndex];
|
|
768
884
|
const sortedRows = this._registry.get(this._sortedRows);
|
|
769
885
|
const rowData = sortedRows[row];
|
|
770
886
|
|
|
@@ -821,11 +937,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
821
937
|
// Resize
|
|
822
938
|
//
|
|
823
939
|
|
|
824
|
-
public setColumnWidth(columnIndex: number, width: number): void {
|
|
940
|
+
public setColumnWidth(columnIndex: number, width: number, plane: DxGridPlane = 'grid'): void {
|
|
825
941
|
const fields = this._projection?.getFields() ?? [];
|
|
826
|
-
|
|
942
|
+
const fieldIndex = this.getFieldIndex(plane, columnIndex);
|
|
943
|
+
if (fieldIndex !== undefined && fieldIndex < fields.length) {
|
|
827
944
|
const newWidth = Math.max(0, width);
|
|
828
|
-
const field = fields[
|
|
945
|
+
const field = fields[fieldIndex];
|
|
829
946
|
if (field) {
|
|
830
947
|
this._change.table((mutableTable) => {
|
|
831
948
|
mutableTable.sizes[field.path] = newWidth;
|
|
@@ -866,7 +983,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
866
983
|
}
|
|
867
984
|
|
|
868
985
|
/**
|
|
869
|
-
* Writes the per-user sort, routing through the
|
|
986
|
+
* Writes the per-user sort, routing through the ViewState.Manager (so the `local` backend persists)
|
|
870
987
|
* when one is configured, otherwise updating the ephemeral atom directly.
|
|
871
988
|
*/
|
|
872
989
|
private _writeSort(value: FieldSortType | undefined): void {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Registry } from '@effect-atom/atom
|
|
5
|
+
import { Registry } from '@effect-atom/atom';
|
|
6
6
|
import * as Schema from 'effect/Schema';
|
|
7
7
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
8
8
|
|
|
@@ -84,6 +84,64 @@ describe('TablePresentation', () => {
|
|
|
84
84
|
expect(cells['0,1']?.value).toBe('C');
|
|
85
85
|
});
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
describe('pinned columns', () => {
|
|
89
|
+
let registry: Registry.Registry;
|
|
90
|
+
let model: TableModel;
|
|
91
|
+
let presentation: TablePresentation;
|
|
92
|
+
|
|
93
|
+
beforeEach(async () => {
|
|
94
|
+
registry = Registry.make();
|
|
95
|
+
model = createTableModel(registry, { features: { selection: { enabled: false }, pinColumns: 1 } });
|
|
96
|
+
await model.open();
|
|
97
|
+
const rows: any[] = [
|
|
98
|
+
{ id: '1', title: 'A', count: 1 },
|
|
99
|
+
{ id: '2', title: 'B', count: 2 },
|
|
100
|
+
];
|
|
101
|
+
model.setRows(rows);
|
|
102
|
+
presentation = new TablePresentation(registry, model);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('renders the first data column in the frozenColsStart plane', () => {
|
|
106
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 0 } }, 'frozenColsStart');
|
|
107
|
+
// Pinned field occupies plane-local column 0 (no selection column).
|
|
108
|
+
expect(cells['0,0']?.value).toBe('A');
|
|
109
|
+
expect(cells['0,1']?.value).toBe('B');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('offsets the scrolling grid plane past the pinned columns', () => {
|
|
113
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 1, col: 0 } }, 'grid');
|
|
114
|
+
// The grid plane starts at the second field (count) since the first (title) is pinned.
|
|
115
|
+
expect(cells['0,0']?.value).toBe('1');
|
|
116
|
+
expect(cells['0,1']?.value).toBe('2');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders the pinned column header in the fixedStartStart corner', () => {
|
|
120
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'fixedStartStart');
|
|
121
|
+
expect(cells['0,0']?.accessoryHtml).toContain('title');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('maps plane-local columns to field indices', () => {
|
|
125
|
+
// frozenColsStart column 0 -> field 0 (pinned); grid column 0 -> field 1.
|
|
126
|
+
expect(model.getFieldIndex('frozenColsStart', 0)).toBe(0);
|
|
127
|
+
expect(model.getFieldIndex('frozenColsStart', 1)).toBeUndefined();
|
|
128
|
+
expect(model.getFieldIndex('grid', 0)).toBe(1);
|
|
129
|
+
expect(model.getFieldIndex('grid', 1)).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('mutates the pinned field via its frozen-plane column', () => {
|
|
133
|
+
model.updateCellData({ plane: 'frozenColsStart', col: 0, row: 0 }, () => 'Pinned');
|
|
134
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'frozenColsStart');
|
|
135
|
+
expect(cells['0,0']?.value).toBe('Pinned');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('ignores mutations for out-of-range columns without throwing', () => {
|
|
139
|
+
expect(() => model.updateCellData({ plane: 'grid', col: 999, row: 0 }, () => 'x')).not.toThrow();
|
|
140
|
+
// The pinned field is unchanged.
|
|
141
|
+
const cells = presentation.getCells({ start: { row: 0, col: 0 }, end: { row: 0, col: 0 } }, 'frozenColsStart');
|
|
142
|
+
expect(cells['0,0']?.value).toBe('A');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
87
145
|
});
|
|
88
146
|
|
|
89
147
|
const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
|