@dxos/react-ui-table 0.9.0 → 0.9.1-staging.ee54ba693a
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-4KOQTPFC.mjs → chunk-K4J2CKTZ.mjs} +73 -24
- package/dist/lib/browser/chunk-K4J2CKTZ.mjs.map +7 -0
- package/dist/lib/browser/{chunk-62PYOJPY.mjs → chunk-VV3P3ADH.mjs} +5 -5
- package/dist/lib/browser/chunk-VV3P3ADH.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +76 -10
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +2 -2
- package/dist/lib/browser/translations.mjs +5 -2
- package/dist/lib/browser/translations.mjs.map +2 -2
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node-esm/{chunk-KOSV2J5N.mjs → chunk-E6V274ZW.mjs} +73 -24
- package/dist/lib/node-esm/chunk-E6V274ZW.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-7LMOZUTS.mjs → chunk-RPEWNOZZ.mjs} +5 -5
- package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +76 -10
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +2 -2
- package/dist/lib/node-esm/translations.mjs +5 -2
- package/dist/lib/node-esm/translations.mjs.map +2 -2
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts +4 -1
- package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts +4 -1
- package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.d.ts +1 -0
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +6 -3
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableToolbar.d.ts +3 -1
- package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -1
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +7 -4
- package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
- package/dist/types/src/components/testing.d.ts +2 -3
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
- package/dist/types/src/model/index.d.ts +1 -0
- package/dist/types/src/model/index.d.ts.map +1 -1
- package/dist/types/src/model/table-model.d.ts +15 -8
- package/dist/types/src/model/table-model.d.ts.map +1 -1
- package/dist/types/src/model/table-presentation.d.ts.map +1 -1
- package/dist/types/src/model/table-view-state.d.ts +19 -0
- package/dist/types/src/model/table-view-state.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +4 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Table.d.ts +3 -11
- package/dist/types/src/types/Table.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +43 -44
- package/src/components/Table/Table.stories.tsx +3 -3
- package/src/components/Table/Table.tsx +1 -0
- package/src/components/Table/TableToolbar.tsx +38 -5
- package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -2
- package/src/components/testing.ts +8 -6
- package/src/hooks/useTableModel.ts +7 -6
- package/src/model/index.ts +1 -0
- package/src/model/table-model.test.ts +93 -5
- package/src/model/table-model.ts +36 -20
- package/src/model/table-presentation.test.ts +6 -4
- package/src/model/table-presentation.ts +34 -4
- package/src/model/table-view-state.ts +30 -0
- package/src/translations.ts +4 -1
- package/src/types/Table.ts +17 -19
- package/dist/lib/browser/chunk-4KOQTPFC.mjs.map +0 -7
- package/dist/lib/browser/chunk-62PYOJPY.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-7LMOZUTS.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-KOSV2J5N.mjs.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-table",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1-staging.ee54ba693a",
|
|
4
4
|
"description": "A table component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -58,58 +58,57 @@
|
|
|
58
58
|
"@radix-ui/react-use-controllable-state": "1.1.0",
|
|
59
59
|
"date-fns": "^3.6.0",
|
|
60
60
|
"lodash.orderby": "^4.6.0",
|
|
61
|
-
"@dxos/
|
|
62
|
-
"@dxos/async": "0.9.
|
|
63
|
-
"@dxos/
|
|
64
|
-
"@dxos/echo": "0.9.
|
|
65
|
-
"@dxos/effect": "0.9.
|
|
66
|
-
"@dxos/
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/react-client": "0.9.
|
|
72
|
-
"@dxos/react-ui-
|
|
73
|
-
"@dxos/react-ui-
|
|
74
|
-
"@dxos/
|
|
75
|
-
"@dxos/react-ui-form": "0.9.
|
|
76
|
-
"@dxos/react-ui-grid": "0.9.
|
|
77
|
-
"@dxos/react-ui-
|
|
78
|
-
"@dxos/react-ui-
|
|
79
|
-
"@dxos/react-ui-search": "0.9.
|
|
80
|
-
"@dxos/
|
|
81
|
-
"@dxos/react-ui-syntax-highlighter": "0.9.
|
|
82
|
-
"@dxos/types": "0.9.
|
|
83
|
-
"@dxos/
|
|
84
|
-
"@dxos/
|
|
85
|
-
"@dxos/util": "0.9.0"
|
|
61
|
+
"@dxos/context": "0.9.1-staging.ee54ba693a",
|
|
62
|
+
"@dxos/async": "0.9.1-staging.ee54ba693a",
|
|
63
|
+
"@dxos/debug": "0.9.1-staging.ee54ba693a",
|
|
64
|
+
"@dxos/echo": "0.9.1-staging.ee54ba693a",
|
|
65
|
+
"@dxos/effect": "0.9.1-staging.ee54ba693a",
|
|
66
|
+
"@dxos/keys": "0.9.1-staging.ee54ba693a",
|
|
67
|
+
"@dxos/lit-grid": "0.9.1-staging.ee54ba693a",
|
|
68
|
+
"@dxos/invariant": "0.9.1-staging.ee54ba693a",
|
|
69
|
+
"@dxos/log": "0.9.1-staging.ee54ba693a",
|
|
70
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
71
|
+
"@dxos/react-client": "0.9.1-staging.ee54ba693a",
|
|
72
|
+
"@dxos/react-ui-attention": "0.9.1-staging.ee54ba693a",
|
|
73
|
+
"@dxos/react-ui-components": "0.9.1-staging.ee54ba693a",
|
|
74
|
+
"@dxos/react-ui-editor": "0.9.1-staging.ee54ba693a",
|
|
75
|
+
"@dxos/react-ui-form": "0.9.1-staging.ee54ba693a",
|
|
76
|
+
"@dxos/react-ui-grid": "0.9.1-staging.ee54ba693a",
|
|
77
|
+
"@dxos/react-ui-menu": "0.9.1-staging.ee54ba693a",
|
|
78
|
+
"@dxos/react-ui-list": "0.9.1-staging.ee54ba693a",
|
|
79
|
+
"@dxos/react-ui-search": "0.9.1-staging.ee54ba693a",
|
|
80
|
+
"@dxos/schema": "0.9.1-staging.ee54ba693a",
|
|
81
|
+
"@dxos/react-ui-syntax-highlighter": "0.9.1-staging.ee54ba693a",
|
|
82
|
+
"@dxos/types": "0.9.1-staging.ee54ba693a",
|
|
83
|
+
"@dxos/ui-types": "0.9.1-staging.ee54ba693a",
|
|
84
|
+
"@dxos/util": "0.9.1-staging.ee54ba693a"
|
|
86
85
|
},
|
|
87
86
|
"devDependencies": {
|
|
88
|
-
"@effect/platform": "0.96.
|
|
87
|
+
"@effect/platform": "0.96.2",
|
|
89
88
|
"@types/lodash.orderby": "^4.6.9",
|
|
90
|
-
"@types/react": "~19.2.
|
|
89
|
+
"@types/react": "~19.2.17",
|
|
91
90
|
"@types/react-dom": "~19.2.3",
|
|
92
|
-
"effect": "3.21.
|
|
91
|
+
"effect": "3.21.4",
|
|
93
92
|
"fast-check": "^3.19.0",
|
|
94
|
-
"react": "~19.2.
|
|
95
|
-
"react-dom": "~19.2.
|
|
93
|
+
"react": "~19.2.7",
|
|
94
|
+
"react-dom": "~19.2.7",
|
|
96
95
|
"vite": "^8.0.16",
|
|
97
|
-
"@dxos/lit-ui": "0.9.
|
|
98
|
-
"@dxos/react-client": "0.9.
|
|
99
|
-
"@dxos/react-ui": "0.9.
|
|
100
|
-
"@dxos/
|
|
101
|
-
"@dxos/
|
|
102
|
-
"@dxos/
|
|
103
|
-
"@dxos/
|
|
96
|
+
"@dxos/lit-ui": "0.9.1-staging.ee54ba693a",
|
|
97
|
+
"@dxos/react-client": "0.9.1-staging.ee54ba693a",
|
|
98
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a",
|
|
99
|
+
"@dxos/random": "0.9.1-staging.ee54ba693a",
|
|
100
|
+
"@dxos/test-utils": "0.9.1-staging.ee54ba693a",
|
|
101
|
+
"@dxos/storybook-utils": "0.9.1-staging.ee54ba693a",
|
|
102
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
|
|
104
103
|
},
|
|
105
104
|
"peerDependencies": {
|
|
106
105
|
"@effect-atom/atom-react": "^0.5.0",
|
|
107
|
-
"@effect/platform": "0.96.
|
|
108
|
-
"effect": "3.21.
|
|
109
|
-
"react": "~19.2.
|
|
110
|
-
"react-dom": "~19.2.
|
|
111
|
-
"@dxos/react-ui": "0.9.
|
|
112
|
-
"@dxos/ui-theme": "0.9.
|
|
106
|
+
"@effect/platform": "0.96.2",
|
|
107
|
+
"effect": "3.21.4",
|
|
108
|
+
"react": "~19.2.7",
|
|
109
|
+
"react-dom": "~19.2.7",
|
|
110
|
+
"@dxos/react-ui": "0.9.1-staging.ee54ba693a",
|
|
111
|
+
"@dxos/ui-theme": "0.9.1-staging.ee54ba693a"
|
|
113
112
|
},
|
|
114
113
|
"publishConfig": {
|
|
115
114
|
"access": "public"
|
|
@@ -7,7 +7,7 @@ import * as Schema from 'effect/Schema';
|
|
|
7
7
|
import React, { useCallback } from 'react';
|
|
8
8
|
import { expect, userEvent, waitFor, within } from 'storybook/test';
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { Annotation, type Database, DXN, Format, Obj, type QueryAST, Ref, Type, View } from '@dxos/echo';
|
|
11
11
|
import { type Mutable, PropertyMetaAnnotationId } from '@dxos/echo/internal';
|
|
12
12
|
import { invariant } from '@dxos/invariant';
|
|
13
13
|
import { random } from '@dxos/random';
|
|
@@ -150,7 +150,7 @@ const DefaultStory = () => {
|
|
|
150
150
|
);
|
|
151
151
|
};
|
|
152
152
|
|
|
153
|
-
type
|
|
153
|
+
type StoryArgs = { rows?: number };
|
|
154
154
|
|
|
155
155
|
//
|
|
156
156
|
// Story definitions.
|
|
@@ -338,7 +338,7 @@ export const RequiredSchema: StoryObj = {
|
|
|
338
338
|
},
|
|
339
339
|
};
|
|
340
340
|
|
|
341
|
-
export const Tags: Meta<
|
|
341
|
+
export const Tags: Meta<StoryArgs> = {
|
|
342
342
|
title: 'ui/react-ui-table/Table',
|
|
343
343
|
render: DefaultStory,
|
|
344
344
|
decorators: [
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
MenuRootProps,
|
|
16
16
|
createGapSeparator,
|
|
17
17
|
createMenuAction,
|
|
18
|
+
createMenuItemGroup,
|
|
18
19
|
useMenuActions,
|
|
19
20
|
} from '@dxos/react-ui-menu';
|
|
20
21
|
|
|
@@ -24,7 +25,9 @@ export type TableToolbarActionProperties = { type: TableToolbarActionType };
|
|
|
24
25
|
|
|
25
26
|
export type TableToolbarAction = MenuAction<TableToolbarActionProperties>;
|
|
26
27
|
|
|
27
|
-
export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
|
|
28
|
+
export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view' | 'export-csv' | 'export-json' | 'export-xml';
|
|
29
|
+
|
|
30
|
+
export type TableExportFormat = 'csv' | 'json' | 'xml';
|
|
28
31
|
|
|
29
32
|
type TableToolbarState = Partial<{ viewDirty: boolean }>;
|
|
30
33
|
|
|
@@ -32,15 +35,18 @@ const createTableToolbarActions = ({
|
|
|
32
35
|
stateAtom,
|
|
33
36
|
onAdd,
|
|
34
37
|
onSave,
|
|
38
|
+
onExport,
|
|
35
39
|
customActions,
|
|
36
40
|
}: {
|
|
37
41
|
stateAtom: Atom.Atom<TableToolbarState>;
|
|
38
42
|
onAdd?: () => void;
|
|
39
43
|
onSave?: () => void;
|
|
44
|
+
onExport?: (format: TableExportFormat) => void;
|
|
40
45
|
customActions?: Atom.Atom<ActionGraphProps>;
|
|
41
46
|
}) =>
|
|
42
47
|
Atom.make((get) => {
|
|
43
48
|
const nodes: ActionGraphNodes = [];
|
|
49
|
+
const edges: ActionGraphEdges = [];
|
|
44
50
|
if (onAdd) {
|
|
45
51
|
const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
|
|
46
52
|
type: 'add-row' as const,
|
|
@@ -49,6 +55,7 @@ const createTableToolbarActions = ({
|
|
|
49
55
|
testId: 'table.toolbar.add-row',
|
|
50
56
|
});
|
|
51
57
|
nodes.push(add);
|
|
58
|
+
edges.push({ source: 'root', target: add.id, relation: 'child' });
|
|
52
59
|
}
|
|
53
60
|
if (onSave) {
|
|
54
61
|
const state = get(stateAtom);
|
|
@@ -61,10 +68,35 @@ const createTableToolbarActions = ({
|
|
|
61
68
|
hidden: !state.viewDirty,
|
|
62
69
|
});
|
|
63
70
|
nodes.push(save);
|
|
71
|
+
edges.push({ source: 'root', target: save.id, relation: 'child' });
|
|
72
|
+
}
|
|
73
|
+
if (onExport) {
|
|
74
|
+
const exportGroup = createMenuItemGroup('export-rows', {
|
|
75
|
+
label: ['export-rows.menu', { ns: translationKey }],
|
|
76
|
+
icon: 'ph--export--regular',
|
|
77
|
+
iconOnly: true,
|
|
78
|
+
variant: 'dropdownMenu',
|
|
79
|
+
});
|
|
80
|
+
nodes.push(exportGroup);
|
|
81
|
+
edges.push({ source: 'root', target: exportGroup.id, relation: 'child' });
|
|
82
|
+
|
|
83
|
+
const formats: Array<{ format: TableExportFormat; type: TableToolbarActionType; label: string }> = [
|
|
84
|
+
{ format: 'csv', type: 'export-csv', label: 'export-rows-csv.menu' },
|
|
85
|
+
{ format: 'json', type: 'export-json', label: 'export-rows-json.menu' },
|
|
86
|
+
{ format: 'xml', type: 'export-xml', label: 'export-rows-xml.menu' },
|
|
87
|
+
];
|
|
88
|
+
for (const { format, type, label } of formats) {
|
|
89
|
+
const action = createMenuAction<TableToolbarActionProperties>(type, () => onExport(format), {
|
|
90
|
+
type,
|
|
91
|
+
label: [label, { ns: translationKey }],
|
|
92
|
+
});
|
|
93
|
+
nodes.push(action);
|
|
94
|
+
edges.push({ source: exportGroup.id, target: action.id, relation: 'child' });
|
|
95
|
+
}
|
|
64
96
|
}
|
|
65
97
|
const gap = createGapSeparator();
|
|
66
98
|
nodes.push(...gap.nodes);
|
|
67
|
-
|
|
99
|
+
edges.push(...gap.edges);
|
|
68
100
|
if (customActions) {
|
|
69
101
|
const custom = get(customActions);
|
|
70
102
|
nodes.push(...custom.nodes);
|
|
@@ -81,11 +113,12 @@ export type TableToolbarProps = Pick<MenuRootProps, 'attendableId'> &
|
|
|
81
113
|
TableToolbarState & {
|
|
82
114
|
onAdd?: () => void;
|
|
83
115
|
onSave?: () => void;
|
|
116
|
+
onExport?: (format: TableExportFormat) => void;
|
|
84
117
|
customActions?: Atom.Atom<ActionGraphProps>;
|
|
85
118
|
};
|
|
86
119
|
|
|
87
120
|
export const TableToolbar = composable<HTMLDivElement, TableToolbarProps>(
|
|
88
|
-
({ attendableId, viewDirty, onAdd, onSave, customActions, ...props }, forwardedRef) => {
|
|
121
|
+
({ attendableId, viewDirty, onAdd, onSave, onExport, customActions, ...props }, forwardedRef) => {
|
|
89
122
|
const registry = useContext(RegistryContext);
|
|
90
123
|
const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
|
|
91
124
|
|
|
@@ -95,8 +128,8 @@ export const TableToolbar = composable<HTMLDivElement, TableToolbarProps>(
|
|
|
95
128
|
}, [registry, stateAtom, viewDirty]);
|
|
96
129
|
|
|
97
130
|
const actionsCreator = useMemo(
|
|
98
|
-
() => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
|
|
99
|
-
[stateAtom, onAdd, onSave, customActions],
|
|
131
|
+
() => createTableToolbarActions({ stateAtom, onAdd, onSave, onExport, customActions }),
|
|
132
|
+
[stateAtom, onAdd, onSave, onExport, customActions],
|
|
100
133
|
);
|
|
101
134
|
const menuActions = useMenuActions(actionsCreator);
|
|
102
135
|
|
|
@@ -19,12 +19,12 @@ import { useTestTableModel } from '../../testing';
|
|
|
19
19
|
import { Table } from '../../types';
|
|
20
20
|
import { TableCellEditor } from './TableCellEditor';
|
|
21
21
|
|
|
22
|
-
type
|
|
22
|
+
type StoryArgs = {
|
|
23
23
|
editing: GridEditing;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
// TODO(burdon): Broken layout.
|
|
27
|
-
const DefaultStory = ({ editing }:
|
|
27
|
+
const DefaultStory = ({ editing }: StoryArgs) => {
|
|
28
28
|
const { model, table } = useTestTableModel();
|
|
29
29
|
|
|
30
30
|
if (!model || !table) {
|
|
@@ -12,12 +12,14 @@ import { SchemaEx } from '@dxos/effect';
|
|
|
12
12
|
import { random } from '@dxos/random';
|
|
13
13
|
import { type ProjectionModel } from '@dxos/schema';
|
|
14
14
|
|
|
15
|
-
export const TestSchema =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
export const TestSchema = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
|
|
16
|
+
Schema.Struct({
|
|
17
|
+
name: Schema.optional(Schema.String),
|
|
18
|
+
age: Schema.optional(Schema.Number),
|
|
19
|
+
active: Schema.optional(Schema.Boolean),
|
|
20
|
+
netWorth: Schema.optional(Schema.Number),
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
21
23
|
|
|
22
24
|
export type TestItem = {
|
|
23
25
|
name: string;
|
|
@@ -6,9 +6,8 @@ import { RegistryContext } from '@effect-atom/atom-react';
|
|
|
6
6
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
8
|
import { type Database, Obj } from '@dxos/echo';
|
|
9
|
-
import {
|
|
9
|
+
import { useSelection, useSelectionActions, useViewStateManagerOptional } from '@dxos/react-ui-attention';
|
|
10
10
|
import { type ProjectionModel } from '@dxos/schema';
|
|
11
|
-
import { isNonNullable } from '@dxos/util';
|
|
12
11
|
|
|
13
12
|
import {
|
|
14
13
|
TableModel,
|
|
@@ -44,7 +43,8 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
44
43
|
...props
|
|
45
44
|
}: UseTableModelProps<T>): TableModel<T> | undefined => {
|
|
46
45
|
const registry = useContext(RegistryContext);
|
|
47
|
-
const
|
|
46
|
+
const viewState = useViewStateManagerOptional();
|
|
47
|
+
const selected = useSelection(object && Obj.getURI(object), 'multi');
|
|
48
48
|
const initialSelection = useMemo(() => selected, [object]);
|
|
49
49
|
|
|
50
50
|
const [model, setModel] = useState<TableModel<T>>();
|
|
@@ -60,6 +60,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
60
60
|
object,
|
|
61
61
|
projection,
|
|
62
62
|
db,
|
|
63
|
+
viewState,
|
|
63
64
|
change: createEchoChangeCallback<T>(object),
|
|
64
65
|
features,
|
|
65
66
|
rowActions,
|
|
@@ -76,7 +77,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
76
77
|
void model?.close();
|
|
77
78
|
};
|
|
78
79
|
// TODO(burdon): Trigger if callbacks change?
|
|
79
|
-
}, [registry, object, projection, features, rowActions, initialSelection]);
|
|
80
|
+
}, [registry, viewState, object, projection, features, rowActions, initialSelection]);
|
|
80
81
|
|
|
81
82
|
// Update data when rows change.
|
|
82
83
|
useEffect(() => {
|
|
@@ -85,7 +86,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
85
86
|
}
|
|
86
87
|
}, [model, rows]);
|
|
87
88
|
|
|
88
|
-
const {
|
|
89
|
+
const { multi, clear } = useSelectionActions(model?.id);
|
|
89
90
|
|
|
90
91
|
useEffect(() => {
|
|
91
92
|
if (!model) {
|
|
@@ -94,7 +95,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
|
|
|
94
95
|
|
|
95
96
|
const unsubscribe = registry.subscribe(model.selection.selectionAtom, () => {
|
|
96
97
|
const selectedItems = [...model.selection.selection];
|
|
97
|
-
|
|
98
|
+
multi(selectedItems);
|
|
98
99
|
onSelectionChanged?.(selectedItems);
|
|
99
100
|
});
|
|
100
101
|
|
package/src/model/index.ts
CHANGED
|
@@ -8,6 +8,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, ViewStateManager } from '@dxos/react-ui-attention';
|
|
11
12
|
import { ProjectionModel, ViewModel, createDirectChangeCallback } from '@dxos/schema';
|
|
12
13
|
|
|
13
14
|
import { Table } from '../types';
|
|
@@ -115,12 +116,94 @@ describe('TableModel', () => {
|
|
|
115
116
|
});
|
|
116
117
|
});
|
|
117
118
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
describe('TableModel sort view state', () => {
|
|
120
|
+
// Share `storage` (and the table object) across instances; a fresh registry/manager/model
|
|
121
|
+
// simulates a reload, proving the sort is externalized to the `local` view state keyed by the
|
|
122
|
+
// table URI rather than held per-model.
|
|
123
|
+
const disposables: LocalBackend[] = [];
|
|
124
|
+
afterEach(() => {
|
|
125
|
+
disposables.splice(0).forEach((backend) => backend.dispose());
|
|
126
|
+
});
|
|
122
127
|
|
|
123
|
-
|
|
128
|
+
test('column sort persists to the local view state and is restored after reload', async () => {
|
|
129
|
+
const storage = fakeStorage();
|
|
130
|
+
const schema = createEchoSchema(Type.getSchema(Test));
|
|
131
|
+
const view = ViewModel.make({ query: Query.select(Filter.type(schema)), jsonSchema: schema.jsonSchema });
|
|
132
|
+
const object = Table.make({ view });
|
|
133
|
+
|
|
134
|
+
// Each reopen builds a fresh registry/manager/projection over the SAME table object (and its view),
|
|
135
|
+
// so it exercises a real reload rather than reusing projection state across instances.
|
|
136
|
+
const reopen = async (): Promise<TableModel> => {
|
|
137
|
+
const registry = Registry.make();
|
|
138
|
+
const local = new LocalBackend({ registry, storage });
|
|
139
|
+
disposables.push(local);
|
|
140
|
+
const viewState = new ViewStateManager({ registry, backends: { memory: new MemoryBackend(), local } });
|
|
141
|
+
const projection = new ProjectionModel({
|
|
142
|
+
registry,
|
|
143
|
+
view,
|
|
144
|
+
baseSchema: schema.jsonSchema,
|
|
145
|
+
change: createDirectChangeCallback(view.projection, JsonSchema.toJsonSchema(Type.getSchema(schema))),
|
|
146
|
+
});
|
|
147
|
+
projection.normalizeView();
|
|
148
|
+
const model = new TableModel({
|
|
149
|
+
registry,
|
|
150
|
+
object,
|
|
151
|
+
projection,
|
|
152
|
+
viewState,
|
|
153
|
+
change: createTableDirectChangeCallback(object),
|
|
154
|
+
});
|
|
155
|
+
await model.open();
|
|
156
|
+
return model;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const first = await reopen();
|
|
160
|
+
const titleFieldId = first.projection.getFields().find((field) => field.path === 'title')!.id;
|
|
161
|
+
first.setSort(titleFieldId, 'asc');
|
|
162
|
+
expect(first.getSorting()).toEqual({ fieldId: titleFieldId, direction: 'asc' });
|
|
163
|
+
expect(JSON.parse(storage.getItem(`dxos:view-state:table-sort:${first.id}`)!)).toEqual({
|
|
164
|
+
fieldId: titleFieldId,
|
|
165
|
+
direction: 'asc',
|
|
166
|
+
});
|
|
167
|
+
await first.close();
|
|
168
|
+
|
|
169
|
+
// Reload: fresh registry/manager/projection over the same storage + table object.
|
|
170
|
+
const second = await reopen();
|
|
171
|
+
expect(second.getSorting()).toEqual({ fieldId: titleFieldId, direction: 'asc' });
|
|
172
|
+
|
|
173
|
+
second.clearSort();
|
|
174
|
+
expect(second.getSorting()).toBeUndefined();
|
|
175
|
+
await second.close();
|
|
176
|
+
|
|
177
|
+
// The cleared sort is also persisted: a further reload reads no sort.
|
|
178
|
+
const third = await reopen();
|
|
179
|
+
expect(third.getSorting()).toBeUndefined();
|
|
180
|
+
await third.close();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Minimal in-memory Storage stand-in (no real localStorage in the test runner).
|
|
185
|
+
const fakeStorage = (): Storage => {
|
|
186
|
+
const map = new Map<string, string>();
|
|
187
|
+
return {
|
|
188
|
+
get length() {
|
|
189
|
+
return map.size;
|
|
190
|
+
},
|
|
191
|
+
clear: () => map.clear(),
|
|
192
|
+
getItem: (key) => (map.has(key) ? map.get(key)! : null),
|
|
193
|
+
key: (index) => [...map.keys()][index] ?? null,
|
|
194
|
+
removeItem: (key) => void map.delete(key),
|
|
195
|
+
setItem: (key, value) => void map.set(key, value),
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
|
|
200
|
+
Schema.Struct({
|
|
201
|
+
title: Schema.String,
|
|
202
|
+
completed: Schema.Boolean,
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const createTableInputs = (registry: Registry.Registry): { object: Table.Table; projection: ProjectionModel } => {
|
|
124
207
|
const schema = createEchoSchema(Type.getSchema(Test));
|
|
125
208
|
const view = ViewModel.make({
|
|
126
209
|
query: Query.select(Filter.type(schema)),
|
|
@@ -134,6 +217,11 @@ const createTableModel = (registry: Registry.Registry, props: Partial<TableModel
|
|
|
134
217
|
change: createDirectChangeCallback(view.projection, JsonSchema.toJsonSchema(Type.getSchema(schema))),
|
|
135
218
|
});
|
|
136
219
|
projection.normalizeView();
|
|
220
|
+
return { object, projection };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const createTableModel = (registry: Registry.Registry, props: Partial<TableModelProps> = {}): TableModel => {
|
|
224
|
+
const { object, projection } = createTableInputs(registry);
|
|
137
225
|
return new TableModel({
|
|
138
226
|
registry,
|
|
139
227
|
object,
|
package/src/model/table-model.ts
CHANGED
|
@@ -7,11 +7,12 @@ import { Atom, type Registry } from '@effect-atom/atom-react';
|
|
|
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';
|
|
9
9
|
import { type JsonSchema as JsonSchemaType, toEffectSchema } from '@dxos/echo/JsonSchema';
|
|
10
|
-
import {
|
|
10
|
+
import { type Mutable, getSnapshot } from '@dxos/echo/Obj';
|
|
11
11
|
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 { type ViewStateManager } from '@dxos/react-ui-attention';
|
|
15
16
|
import { formatForEditing, parseValue } from '@dxos/react-ui-form';
|
|
16
17
|
import {
|
|
17
18
|
type DxGridAxisMeta,
|
|
@@ -25,16 +26,8 @@ import { type Table } from '../types';
|
|
|
25
26
|
import { extractOrder } from '../util';
|
|
26
27
|
import { compareValues } from '../util/sort';
|
|
27
28
|
import { extractTagIds } from '../util/tag';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Field sort configuration.
|
|
31
|
-
*/
|
|
32
|
-
export type FieldSortType = {
|
|
33
|
-
fieldId: string;
|
|
34
|
-
direction: QueryAST.OrderDirection;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
29
|
import { type SelectionMode, SelectionModel } from './selection-model';
|
|
30
|
+
import { type FieldSortType, tableSortAspect } from './table-view-state';
|
|
38
31
|
|
|
39
32
|
/**
|
|
40
33
|
* Callback type for wrapping mutations in Obj.update().
|
|
@@ -110,6 +103,12 @@ export type TableModelProps<T extends TableRow = TableRow> = {
|
|
|
110
103
|
object: Table.Table;
|
|
111
104
|
projection: ProjectionModel;
|
|
112
105
|
db?: Database.Database;
|
|
106
|
+
/**
|
|
107
|
+
* Per-context UI state manager (from `@dxos/react-ui-attention`). When provided, the column sort is
|
|
108
|
+
* stored in the `tableSortAspect` view state keyed by the table URI; otherwise it falls back to an
|
|
109
|
+
* ephemeral atom (e.g. isolated stories/tests with no `ViewStateProvider`).
|
|
110
|
+
*/
|
|
111
|
+
viewState?: ViewStateManager;
|
|
113
112
|
/**
|
|
114
113
|
* Callbacks to wrap mutations in Obj.update().
|
|
115
114
|
* Use createEchoChangeCallback() for ECHO-backed objects or createDirectChangeCallback() for plain objects.
|
|
@@ -148,7 +147,10 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
148
147
|
|
|
149
148
|
private readonly _rows: Atom.Writable<T[]>;
|
|
150
149
|
private readonly _draftRows: Atom.Writable<DraftRow<T>[]>;
|
|
151
|
-
//
|
|
150
|
+
// Optional per-context UI state manager; when present the column sort is view-state-backed.
|
|
151
|
+
private readonly _viewState?: ViewStateManager;
|
|
152
|
+
// Per-user sort state - backed by the `tableSortAspect` view state (keyed by table URI) when a
|
|
153
|
+
// ViewStateManager is provided, else an ephemeral atom. Changes are local until saved to the view.
|
|
152
154
|
private readonly _inMemorySort: Atom.Writable<FieldSortType | undefined>;
|
|
153
155
|
// Derived atom for persisted sort from view.query.ast.
|
|
154
156
|
private readonly _persistedSort: Atom.Atom<FieldSortType | undefined>;
|
|
@@ -170,6 +172,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
170
172
|
object,
|
|
171
173
|
projection,
|
|
172
174
|
db,
|
|
175
|
+
viewState,
|
|
173
176
|
change,
|
|
174
177
|
features = {},
|
|
175
178
|
initialSelection = [],
|
|
@@ -185,6 +188,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
185
188
|
super();
|
|
186
189
|
this._registry = registry;
|
|
187
190
|
this._object = object;
|
|
191
|
+
this._viewState = viewState;
|
|
188
192
|
this._projection = projection;
|
|
189
193
|
this._projection.normalizeView();
|
|
190
194
|
this._db = db;
|
|
@@ -205,7 +209,11 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
205
209
|
});
|
|
206
210
|
this._rows = Atom.make<T[]>([]);
|
|
207
211
|
this._draftRows = Atom.make<DraftRow<T>[]>([]);
|
|
208
|
-
|
|
212
|
+
// View-state-backed atom survives remount/reload (local backend, keyed by table URI); the
|
|
213
|
+
// ephemeral fallback preserves prior behaviour when no ViewStateManager is supplied.
|
|
214
|
+
this._inMemorySort = this._viewState
|
|
215
|
+
? this._viewState.atom(tableSortAspect, this.id)
|
|
216
|
+
: Atom.make<FieldSortType | undefined>(undefined);
|
|
209
217
|
this._cellUpdateCounter = Atom.make<number>(0);
|
|
210
218
|
|
|
211
219
|
// Create derived atom for persisted sort from view.query.ast.
|
|
@@ -854,11 +862,19 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
854
862
|
return;
|
|
855
863
|
}
|
|
856
864
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
865
|
+
this._writeSort({ fieldId, direction });
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Writes the per-user sort, routing through the ViewStateManager (so the `local` backend persists)
|
|
870
|
+
* when one is configured, otherwise updating the ephemeral atom directly.
|
|
871
|
+
*/
|
|
872
|
+
private _writeSort(value: FieldSortType | undefined): void {
|
|
873
|
+
if (this._viewState) {
|
|
874
|
+
this._viewState.set(tableSortAspect, this.id, value);
|
|
875
|
+
} else {
|
|
876
|
+
this._registry.set(this._inMemorySort, value);
|
|
877
|
+
}
|
|
862
878
|
}
|
|
863
879
|
|
|
864
880
|
/**
|
|
@@ -879,7 +895,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
879
895
|
* Clears the in-memory sort order.
|
|
880
896
|
*/
|
|
881
897
|
public clearSort(): void {
|
|
882
|
-
this.
|
|
898
|
+
this._writeSort(undefined);
|
|
883
899
|
}
|
|
884
900
|
|
|
885
901
|
/**
|
|
@@ -912,8 +928,8 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
|
|
|
912
928
|
});
|
|
913
929
|
}
|
|
914
930
|
|
|
915
|
-
// Clear
|
|
916
|
-
this.
|
|
931
|
+
// Clear per-user sort since it's now persisted to the view.
|
|
932
|
+
this._writeSort(undefined);
|
|
917
933
|
}
|
|
918
934
|
|
|
919
935
|
/**
|
|
@@ -86,10 +86,12 @@ describe('TablePresentation', () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
const Test =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
const Test = Type.makeObject(DXN.make('com.example.type.test', '0.1.0'))(
|
|
90
|
+
Schema.Struct({
|
|
91
|
+
title: Schema.String,
|
|
92
|
+
count: Schema.Number,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
93
95
|
|
|
94
96
|
const createTableModel = (registry: Registry.Registry, props: Partial<TableModelProps> = {}): TableModel => {
|
|
95
97
|
const schema = createEchoSchema(Type.getSchema(Test));
|