@dxos/plugin-navtree 0.7.1-staging.8709ba7 → 0.7.2-main.f1adc9f
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/index.mjs +226 -200
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
- package/dist/types/src/components/NavTree.d.ts +7 -5
- package/dist/types/src/components/NavTree.d.ts.map +1 -1
- package/dist/types/src/components/NavTree.stories.d.ts.map +1 -1
- package/dist/types/src/components/NavTreeContainer.d.ts +2 -6
- package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
- package/dist/types/src/components/NavTreeItemAction.stories.d.ts +1 -0
- package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +1 -1
- package/dist/types/src/stories/Graph.stories.d.ts +2 -1
- package/dist/types/src/stories/Graph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/generator.d.ts +0 -1
- package/dist/types/src/testing/generator.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/update-state.d.ts +10 -0
- package/dist/types/src/testing/update-state.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +12 -3
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +1 -20
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +28 -28
- package/src/NavTreePlugin.tsx +103 -90
- package/src/components/NavTree.stories.tsx +73 -69
- package/src/components/NavTree.tsx +30 -20
- package/src/components/NavTreeContainer.tsx +97 -56
- package/src/stories/Graph.stories.tsx +68 -96
- package/src/testing/generator.ts +2 -7
- package/src/testing/index.ts +1 -1
- package/src/testing/update-state.ts +70 -0
- package/src/types.ts +18 -3
- package/src/util.ts +2 -94
- package/dist/types/src/testing/visitor.d.ts +0 -18
- package/dist/types/src/testing/visitor.d.ts.map +0 -1
- package/src/testing/visitor.ts +0 -152
package/src/NavTreePlugin.tsx
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { effect } from '@preact/signals-core';
|
|
5
6
|
import React from 'react';
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -19,13 +20,14 @@ import {
|
|
|
19
20
|
parseGraphPlugin,
|
|
20
21
|
NavigationAction,
|
|
21
22
|
parseNavigationPlugin,
|
|
23
|
+
parseLayoutPlugin,
|
|
22
24
|
} from '@dxos/app-framework';
|
|
23
|
-
import { createExtension, type Graph, isAction, isGraphNode, type Node
|
|
24
|
-
import {
|
|
25
|
+
import { createExtension, type Graph, isAction, isGraphNode, type Node } from '@dxos/app-graph';
|
|
26
|
+
import { type UnsubscribeCallback } from '@dxos/async';
|
|
27
|
+
import { create, type ReactiveObject } from '@dxos/echo-schema';
|
|
25
28
|
import { Keyboard } from '@dxos/keyboard';
|
|
26
|
-
import {
|
|
29
|
+
import { type TreeData } from '@dxos/react-ui-list';
|
|
27
30
|
import { Path } from '@dxos/react-ui-mosaic';
|
|
28
|
-
import { mx } from '@dxos/react-ui-theme';
|
|
29
31
|
import { getHostPlatform } from '@dxos/util';
|
|
30
32
|
|
|
31
33
|
import {
|
|
@@ -39,14 +41,8 @@ import {
|
|
|
39
41
|
import { CommandsTrigger } from './components/CommandsTrigger';
|
|
40
42
|
import meta, { KEY_BINDING, NAVTREE_PLUGIN } from './meta';
|
|
41
43
|
import translations from './translations';
|
|
42
|
-
import { type
|
|
43
|
-
import {
|
|
44
|
-
expandOpenGraphNodes,
|
|
45
|
-
getActions,
|
|
46
|
-
getChildren,
|
|
47
|
-
treeItemsFromRootNode,
|
|
48
|
-
type NavTreeItemGraphNode,
|
|
49
|
-
} from './util';
|
|
44
|
+
import { type NavTreeItemGraphNode } from './types';
|
|
45
|
+
import { expandChildrenAndActions } from './util';
|
|
50
46
|
|
|
51
47
|
export type NavTreePluginProvides = SurfaceProvides &
|
|
52
48
|
MetadataRecordsProvides &
|
|
@@ -54,99 +50,117 @@ export type NavTreePluginProvides = SurfaceProvides &
|
|
|
54
50
|
TranslationsProvides &
|
|
55
51
|
IntentResolverProvides;
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
const KEY = `${NAVTREE_PLUGIN}/state`;
|
|
54
|
+
const getInitialState = () => {
|
|
55
|
+
const stringified = localStorage.getItem(KEY);
|
|
56
|
+
if (!stringified) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const cached: [string, { open: boolean; current: boolean }][] = JSON.parse(stringified);
|
|
62
|
+
return cached.map(
|
|
63
|
+
([key, value]): [
|
|
64
|
+
string,
|
|
65
|
+
ReactiveObject<{
|
|
66
|
+
open: boolean;
|
|
67
|
+
current: boolean;
|
|
68
|
+
}>,
|
|
69
|
+
] => [key, create(value)],
|
|
70
|
+
);
|
|
71
|
+
} catch {}
|
|
61
72
|
};
|
|
62
73
|
|
|
63
74
|
export const NavTreePlugin = (): PluginDefinition<NavTreePluginProvides> => {
|
|
64
75
|
let graphPlugin: Plugin<GraphProvides> | undefined;
|
|
65
76
|
let graph: Graph | undefined;
|
|
66
|
-
|
|
77
|
+
let unsubscribe: UnsubscribeCallback | undefined;
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
// TODO(wittjosiah): This currently needs to be not a ReactiveObject at the root.
|
|
80
|
+
// If it is a ReactiveObject then React errors when initializing new paths because of state change during render.
|
|
81
|
+
// Ideally this could be a ReactiveObject but be able to access and update the root level without breaking render.
|
|
82
|
+
// Wrapping accesses and updates in `untracked` didn't seem to work in all cases.
|
|
83
|
+
const state = new Map<string, ReactiveObject<{ open: boolean; current: boolean }>>(
|
|
84
|
+
getInitialState() ?? [
|
|
85
|
+
// TODO(thure): Initialize these dynamically.
|
|
86
|
+
['root', create({ open: true, current: false })],
|
|
87
|
+
['root~dxos.org/plugin/space-spaces', create({ open: true, current: false })],
|
|
88
|
+
['root~dxos.org/plugin/files', create({ open: true, current: false })],
|
|
89
|
+
],
|
|
90
|
+
);
|
|
74
91
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
const getItem = (_path: string[]) => {
|
|
93
|
+
const path = Path.create(..._path);
|
|
94
|
+
const value = state.get(path) ?? create({ open: false, current: false });
|
|
95
|
+
if (!state.has(path)) {
|
|
96
|
+
state.set(path, value);
|
|
97
|
+
}
|
|
80
98
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const path = [...parent, node.id];
|
|
84
|
-
const { actions, groupedActions } = getActions(graph, node);
|
|
85
|
-
const children = getChildren(graph, node, filter, path);
|
|
86
|
-
const parentOf =
|
|
87
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
88
|
-
const item = {
|
|
89
|
-
id: node.id,
|
|
90
|
-
label: node.properties.label ?? node.id,
|
|
91
|
-
icon: node.properties.icon,
|
|
92
|
-
disabled: node.properties.disabled,
|
|
93
|
-
testId: node.properties.testId,
|
|
94
|
-
className: mx(node.properties.modified && 'italic', node.properties.className),
|
|
95
|
-
headingClassName: node.properties.headingClassName,
|
|
96
|
-
path,
|
|
97
|
-
parentOf,
|
|
98
|
-
node,
|
|
99
|
-
actions,
|
|
100
|
-
groupedActions,
|
|
101
|
-
} satisfies NavTreeItem;
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} else {
|
|
109
|
-
itemCache.set(node.id, item);
|
|
110
|
-
return item;
|
|
111
|
-
}
|
|
102
|
+
const setItem = (path: string[], key: 'open' | 'current', next: boolean) => {
|
|
103
|
+
const value = getItem(path);
|
|
104
|
+
value[key] = next;
|
|
105
|
+
|
|
106
|
+
localStorage.setItem(KEY, JSON.stringify(Array.from(state.entries())));
|
|
112
107
|
};
|
|
113
108
|
|
|
114
|
-
const
|
|
115
|
-
|
|
109
|
+
const isOpen = (path: string[]) => getItem(path).open;
|
|
110
|
+
const isCurrent = (path: string[]) => getItem(path).current;
|
|
111
|
+
|
|
112
|
+
const handleOpenChange = ({ item: { id }, path, open }: { item: Node; path: string[]; open: boolean }) => {
|
|
116
113
|
// TODO(thure): This might become a localstorage leak; openItemIds that no longer exist should be removed from this map.
|
|
117
|
-
|
|
118
|
-
state.values.open.push(path);
|
|
119
|
-
} else {
|
|
120
|
-
const index = state.values.open.indexOf(path);
|
|
121
|
-
if (index > -1) {
|
|
122
|
-
state.values.open.splice(index, 1);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
114
|
+
setItem(path, 'open', open);
|
|
125
115
|
|
|
126
116
|
if (graph) {
|
|
127
|
-
|
|
117
|
+
const node = graph.findNode(id);
|
|
118
|
+
return node && expandChildrenAndActions(graph, node as NavTreeItemGraphNode);
|
|
128
119
|
}
|
|
129
120
|
};
|
|
130
121
|
|
|
131
122
|
return {
|
|
132
123
|
meta,
|
|
133
124
|
ready: async (plugins) => {
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
const layout = resolvePlugin(plugins, parseLayoutPlugin)?.provides.layout;
|
|
126
|
+
const location = resolvePlugin(plugins, parseNavigationPlugin)?.provides.location;
|
|
136
127
|
graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
|
|
137
128
|
graph = graphPlugin?.provides.graph;
|
|
138
|
-
if (!graph) {
|
|
129
|
+
if (!graph || !location || !layout) {
|
|
139
130
|
return;
|
|
140
131
|
}
|
|
141
132
|
|
|
133
|
+
const soloPart = location?.active.solo?.[0];
|
|
142
134
|
const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatch;
|
|
143
|
-
const soloPart = resolvePlugin(plugins, parseNavigationPlugin)?.provides.location.active.solo?.[0];
|
|
144
135
|
if (dispatch && soloPart) {
|
|
145
136
|
void dispatch({ plugin: NAVTREE_PLUGIN, action: NavigationAction.EXPOSE, data: { id: soloPart.id } });
|
|
146
137
|
}
|
|
147
138
|
|
|
148
|
-
|
|
149
|
-
|
|
139
|
+
let previous: string[] = [];
|
|
140
|
+
unsubscribe = effect(() => {
|
|
141
|
+
const part = layout.layoutMode === 'solo' ? 'solo' : 'main';
|
|
142
|
+
const current = location.active[part]?.map(({ id }) => id) ?? [];
|
|
143
|
+
const removed = previous.filter((id) => !current.includes(id));
|
|
144
|
+
previous = current;
|
|
145
|
+
|
|
146
|
+
// TODO(wittjosiah): This is setTimeout because there's a race between the keys be initialized.
|
|
147
|
+
// This could be avoided if the location was a path as well and not just an id.
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
removed.forEach((id) => {
|
|
150
|
+
const keys = Array.from(state.keys()).filter((key) => Path.last(key) === id);
|
|
151
|
+
keys.forEach((key) => {
|
|
152
|
+
setItem(Path.parts(key), 'current', false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
current.forEach((id) => {
|
|
157
|
+
const keys = Array.from(new Set([...state.keys(), id])).filter((key) => Path.last(key) === id);
|
|
158
|
+
keys.forEach((key) => {
|
|
159
|
+
setItem(Path.parts(key), 'current', true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
150
164
|
|
|
151
165
|
// TODO(wittjosiah): Factor out.
|
|
152
166
|
// TODO(wittjosiah): Handle removal of actions.
|
|
@@ -184,20 +198,21 @@ export const NavTreePlugin = (): PluginDefinition<NavTreePluginProvides> => {
|
|
|
184
198
|
Keyboard.singleton.setCurrentContext(graphPlugin?.provides.graph.root.id);
|
|
185
199
|
},
|
|
186
200
|
unload: async () => {
|
|
201
|
+
unsubscribe?.();
|
|
187
202
|
Keyboard.singleton.destroy();
|
|
188
203
|
},
|
|
189
204
|
provides: {
|
|
190
205
|
metadata: {
|
|
191
206
|
records: {
|
|
192
207
|
[NODE_TYPE]: {
|
|
193
|
-
parse: (item:
|
|
208
|
+
parse: ({ item }: TreeData, type: string) => {
|
|
194
209
|
switch (type) {
|
|
195
210
|
case 'node':
|
|
196
|
-
return item
|
|
211
|
+
return item;
|
|
197
212
|
case 'object':
|
|
198
|
-
return item.
|
|
213
|
+
return item.data;
|
|
199
214
|
case 'view-object':
|
|
200
|
-
return { id: `${item.id}-view`, object: item.
|
|
215
|
+
return { id: `${item.id}-view`, object: item.data };
|
|
201
216
|
}
|
|
202
217
|
},
|
|
203
218
|
},
|
|
@@ -216,9 +231,8 @@ export const NavTreePlugin = (): PluginDefinition<NavTreePluginProvides> => {
|
|
|
216
231
|
case 'navigation':
|
|
217
232
|
return (
|
|
218
233
|
<NavTreeContainer
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
open={state.values.open}
|
|
234
|
+
isOpen={isOpen}
|
|
235
|
+
isCurrent={isCurrent}
|
|
222
236
|
onOpenChange={handleOpenChange}
|
|
223
237
|
popoverAnchorId={data.popoverAnchorId as string}
|
|
224
238
|
/>
|
|
@@ -229,7 +243,7 @@ export const NavTreePlugin = (): PluginDefinition<NavTreePluginProvides> => {
|
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
case 'navbar-start': {
|
|
232
|
-
if (
|
|
246
|
+
if (data.activeNode) {
|
|
233
247
|
return {
|
|
234
248
|
node: (
|
|
235
249
|
<NavBarStart
|
|
@@ -262,14 +276,13 @@ export const NavTreePlugin = (): PluginDefinition<NavTreePluginProvides> => {
|
|
|
262
276
|
case NavigationAction.EXPOSE: {
|
|
263
277
|
if (graph && intent.data?.id) {
|
|
264
278
|
const path = await graph.waitForPath({ target: intent.data.id });
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
279
|
+
[...Array(path.length)].forEach((_, index) => {
|
|
280
|
+
const subpath = path.slice(0, index);
|
|
281
|
+
const value = getItem(subpath);
|
|
282
|
+
if (!value.open) {
|
|
283
|
+
setItem(subpath, 'open', true);
|
|
269
284
|
}
|
|
270
|
-
|
|
271
|
-
}, []);
|
|
272
|
-
state.values.open.push(...additionalOpenItems);
|
|
285
|
+
});
|
|
273
286
|
}
|
|
274
287
|
break;
|
|
275
288
|
}
|
|
@@ -9,91 +9,95 @@ import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-a
|
|
|
9
9
|
import { type Meta } from '@storybook/react';
|
|
10
10
|
import React, { useEffect } from 'react';
|
|
11
11
|
|
|
12
|
-
import { type NodeArg } from '@dxos/app-graph';
|
|
13
|
-
import { create } from '@dxos/echo-schema';
|
|
12
|
+
import { isActionLike, type NodeArg } from '@dxos/app-graph';
|
|
13
|
+
import { create, type ReactiveObject } from '@dxos/echo-schema';
|
|
14
14
|
import { faker } from '@dxos/random';
|
|
15
|
-
import {
|
|
15
|
+
import { isTreeData, type TreeData } from '@dxos/react-ui-list';
|
|
16
16
|
import { Path } from '@dxos/react-ui-mosaic';
|
|
17
17
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
18
18
|
|
|
19
|
-
import { NavTree
|
|
20
|
-
import { createTree,
|
|
21
|
-
import { type NavTreeItem } from '../types';
|
|
19
|
+
import { NavTree } from './NavTree';
|
|
20
|
+
import { createTree, updateState } from '../testing';
|
|
22
21
|
|
|
23
22
|
faker.seed(1234);
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
flatTree: NavTreeItem[];
|
|
28
|
-
open: string[];
|
|
29
|
-
current: string[];
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const state = create<State>({
|
|
33
|
-
tree: createTree(),
|
|
34
|
-
get flatTree() {
|
|
35
|
-
return flattenTree(this.tree, this.open);
|
|
36
|
-
},
|
|
37
|
-
open: [],
|
|
38
|
-
current: [],
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const Story = (args: Partial<NavTreeProps>) => {
|
|
42
|
-
// NOTE: If passed directly to args, this won't be reactive.
|
|
43
|
-
const items = state.flatTree;
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
return monitorForElements({
|
|
47
|
-
canMonitor: ({ source }) => isItem(source.data),
|
|
48
|
-
onDrop: ({ location, source }) => {
|
|
49
|
-
// Didn't drop on anything.
|
|
50
|
-
if (!location.current.dropTargets.length) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const target = location.current.dropTargets[0];
|
|
55
|
-
const instruction: Instruction | null = extractInstruction(target.data);
|
|
56
|
-
if (instruction !== null) {
|
|
57
|
-
updateState({
|
|
58
|
-
state: state.tree,
|
|
59
|
-
instruction,
|
|
60
|
-
source: source.data as NavTreeItem,
|
|
61
|
-
target: target.data as NavTreeItem,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}, []);
|
|
67
|
-
|
|
68
|
-
return <NavTree items={items} open={state.open} current={state.current} {...args} />;
|
|
69
|
-
};
|
|
24
|
+
const tree = create<NodeArg<any>>(createTree());
|
|
25
|
+
const state = new Map<string, ReactiveObject<{ open: boolean; current: boolean }>>();
|
|
70
26
|
|
|
71
27
|
const meta: Meta<typeof NavTree> = {
|
|
72
28
|
title: 'plugins/plugin-navtree/NavTree',
|
|
73
29
|
component: NavTree,
|
|
74
|
-
render: Story,
|
|
75
30
|
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
31
|
+
render: (args) => {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
return monitorForElements({
|
|
34
|
+
canMonitor: ({ source }) => isTreeData(source),
|
|
35
|
+
onDrop: ({ location, source }) => {
|
|
36
|
+
// Didn't drop on anything.
|
|
37
|
+
if (!location.current.dropTargets.length) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const target = location.current.dropTargets[0];
|
|
42
|
+
const instruction: Instruction | null = extractInstruction(target.data);
|
|
43
|
+
if (instruction !== null) {
|
|
44
|
+
updateState({
|
|
45
|
+
state: tree,
|
|
46
|
+
instruction,
|
|
47
|
+
source: source.data as TreeData,
|
|
48
|
+
target: target.data as TreeData,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return <NavTree {...args} />;
|
|
56
|
+
},
|
|
76
57
|
args: {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
58
|
+
id: tree.id,
|
|
59
|
+
getActions: (arg: NodeArg<any>) => ({
|
|
60
|
+
actions: arg.nodes?.filter((node) => isActionLike(node)) ?? [],
|
|
61
|
+
groupedActions: {},
|
|
62
|
+
}),
|
|
63
|
+
getItems: (testItem?: NodeArg<any>) => {
|
|
64
|
+
return testItem?.nodes ?? tree.nodes ?? [];
|
|
65
|
+
},
|
|
66
|
+
getProps: (testItem: NodeArg<any>) => ({
|
|
67
|
+
id: testItem.id,
|
|
68
|
+
label: testItem.properties?.label ?? testItem.id,
|
|
69
|
+
icon: testItem.properties?.icon,
|
|
70
|
+
...((testItem.nodes?.length ?? 0) > 0 && {
|
|
71
|
+
parentOf: testItem.nodes!.map(({ id }) => id),
|
|
72
|
+
}),
|
|
73
|
+
}),
|
|
74
|
+
isOpen: (_path: string[]) => {
|
|
75
|
+
const path = Path.create(..._path);
|
|
76
|
+
const value = state.get(path) ?? create({ open: false, current: false });
|
|
77
|
+
if (!state.has(path)) {
|
|
78
|
+
state.set(path, value);
|
|
86
79
|
}
|
|
80
|
+
|
|
81
|
+
return value.open;
|
|
87
82
|
},
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (index > -1) {
|
|
94
|
-
state.current.splice(index, 1);
|
|
95
|
-
}
|
|
83
|
+
isCurrent: (_path: string[]) => {
|
|
84
|
+
const path = Path.create(..._path);
|
|
85
|
+
const value = state.get(path) ?? create({ open: false, current: false });
|
|
86
|
+
if (!state.has(path)) {
|
|
87
|
+
state.set(path, value);
|
|
96
88
|
}
|
|
89
|
+
|
|
90
|
+
return value.current;
|
|
91
|
+
},
|
|
92
|
+
onOpenChange: ({ path: _path, open }) => {
|
|
93
|
+
const path = Path.create(..._path);
|
|
94
|
+
const object = state.get(path);
|
|
95
|
+
object!.open = open;
|
|
96
|
+
},
|
|
97
|
+
onSelect: ({ path: _path, current }) => {
|
|
98
|
+
const path = Path.create(..._path);
|
|
99
|
+
const object = state.get(path);
|
|
100
|
+
object!.current = current;
|
|
97
101
|
},
|
|
98
102
|
},
|
|
99
103
|
};
|
|
@@ -11,19 +11,24 @@ import { type MaybePromise } from '@dxos/util';
|
|
|
11
11
|
|
|
12
12
|
import { NavTreeItemAction } from './NavTreeItemAction';
|
|
13
13
|
import { NAVTREE_PLUGIN } from '../meta';
|
|
14
|
-
import { type
|
|
14
|
+
import { type FlattenedActions, type NavTreeItemGraphNode } from '../types';
|
|
15
15
|
|
|
16
16
|
export const NAV_TREE_ITEM = 'NavTreeItem';
|
|
17
17
|
|
|
18
|
-
export type NavTreeProps = Omit<
|
|
19
|
-
|
|
18
|
+
export type NavTreeProps = Omit<
|
|
19
|
+
TreeProps<NavTreeItemGraphNode>,
|
|
20
|
+
'draggable' | 'gridTemplateColumns' | 'renderColumns'
|
|
21
|
+
> &
|
|
22
|
+
Pick<NavTreeColumnsProps, 'getActions' | 'loadDescendents' | 'renderPresence' | 'popoverAnchorId'>;
|
|
20
23
|
|
|
21
|
-
export const NavTree = ({ loadDescendents, renderPresence, popoverAnchorId, ...props }: NavTreeProps) => {
|
|
22
|
-
const renderColumns = useCallback<NonNullable<TreeProps<
|
|
23
|
-
({ item, menuOpen, setMenuOpen }) => {
|
|
24
|
+
export const NavTree = ({ getActions, loadDescendents, renderPresence, popoverAnchorId, ...props }: NavTreeProps) => {
|
|
25
|
+
const renderColumns = useCallback<NonNullable<TreeProps<NavTreeItemGraphNode>['renderColumns']>>(
|
|
26
|
+
({ item, path, menuOpen, setMenuOpen }) => {
|
|
24
27
|
return (
|
|
25
28
|
<NavTreeColumns
|
|
26
|
-
|
|
29
|
+
path={path}
|
|
30
|
+
node={item}
|
|
31
|
+
getActions={getActions}
|
|
27
32
|
menuOpen={menuOpen}
|
|
28
33
|
setMenuOpen={setMenuOpen}
|
|
29
34
|
loadDescendents={loadDescendents}
|
|
@@ -36,7 +41,7 @@ export const NavTree = ({ loadDescendents, renderPresence, popoverAnchorId, ...p
|
|
|
36
41
|
);
|
|
37
42
|
|
|
38
43
|
return (
|
|
39
|
-
<Tree
|
|
44
|
+
<Tree
|
|
40
45
|
{...props}
|
|
41
46
|
draggable
|
|
42
47
|
gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
|
|
@@ -46,16 +51,20 @@ export const NavTree = ({ loadDescendents, renderPresence, popoverAnchorId, ...p
|
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
type NavTreeColumnsProps = {
|
|
49
|
-
|
|
54
|
+
path: string[];
|
|
55
|
+
node: Node;
|
|
56
|
+
getActions: (node: Node) => FlattenedActions;
|
|
50
57
|
menuOpen: boolean;
|
|
51
58
|
setMenuOpen: (open: boolean) => void;
|
|
52
59
|
loadDescendents?: (node: Node) => MaybePromise<void>;
|
|
53
|
-
renderPresence?: FC<{
|
|
60
|
+
renderPresence?: FC<{ node: Node }>;
|
|
54
61
|
popoverAnchorId?: string;
|
|
55
62
|
};
|
|
56
63
|
|
|
57
64
|
const NavTreeColumns = ({
|
|
58
|
-
|
|
65
|
+
path,
|
|
66
|
+
node,
|
|
67
|
+
getActions,
|
|
59
68
|
menuOpen,
|
|
60
69
|
setMenuOpen,
|
|
61
70
|
loadDescendents,
|
|
@@ -64,22 +73,23 @@ const NavTreeColumns = ({
|
|
|
64
73
|
}: NavTreeColumnsProps) => {
|
|
65
74
|
const { t } = useTranslation(NAVTREE_PLUGIN);
|
|
66
75
|
|
|
67
|
-
const level =
|
|
76
|
+
const level = path.length - 2;
|
|
68
77
|
|
|
69
|
-
const
|
|
78
|
+
const { actions: _actions, groupedActions } = getActions(node);
|
|
79
|
+
const [primaryAction, ...secondaryActions] = _actions.toSorted((a, b) =>
|
|
70
80
|
a.properties?.disposition === 'toolbar' ? -1 : 1,
|
|
71
81
|
);
|
|
72
82
|
|
|
73
|
-
const actions = (primaryAction?.properties?.disposition === 'toolbar' ? secondaryActions :
|
|
83
|
+
const actions = (primaryAction?.properties?.disposition === 'toolbar' ? secondaryActions : _actions)
|
|
74
84
|
.flatMap((action) => (isAction(action) ? [action] : []))
|
|
75
85
|
.filter((action) => !action.properties?.hidden);
|
|
76
86
|
|
|
77
|
-
const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${
|
|
87
|
+
const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${node.id}` ? Popover.Anchor : Fragment;
|
|
78
88
|
|
|
79
89
|
// TODO(thure): Ideally this should not be necessary.
|
|
80
90
|
useEffect(() => {
|
|
81
91
|
const frame = requestAnimationFrame(() => {
|
|
82
|
-
void loadDescendents?.(
|
|
92
|
+
void loadDescendents?.(node);
|
|
83
93
|
if (primaryAction && !isAction(primaryAction)) {
|
|
84
94
|
void loadDescendents?.(primaryAction);
|
|
85
95
|
}
|
|
@@ -94,9 +104,9 @@ const NavTreeColumns = ({
|
|
|
94
104
|
testId={primaryAction.properties?.testId}
|
|
95
105
|
label={toLocalizedString(primaryAction.properties?.label, t)}
|
|
96
106
|
icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
|
|
97
|
-
parent={
|
|
107
|
+
parent={node}
|
|
98
108
|
monolithic={isAction(primaryAction)}
|
|
99
|
-
menuActions={isAction(primaryAction) ? [primaryAction] :
|
|
109
|
+
menuActions={isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction.id]}
|
|
100
110
|
menuType={primaryAction.properties?.menuType}
|
|
101
111
|
caller={NAV_TREE_ITEM}
|
|
102
112
|
/>
|
|
@@ -109,7 +119,7 @@ const NavTreeColumns = ({
|
|
|
109
119
|
testId={`navtree.treeItem.actionsLevel${level}`}
|
|
110
120
|
label={t('tree item actions label')}
|
|
111
121
|
icon='ph--dots-three-vertical--regular'
|
|
112
|
-
parent={
|
|
122
|
+
parent={node}
|
|
113
123
|
menuActions={actions}
|
|
114
124
|
menuType='dropdown'
|
|
115
125
|
caller={NAV_TREE_ITEM}
|
|
@@ -120,7 +130,7 @@ const NavTreeColumns = ({
|
|
|
120
130
|
<Treegrid.Cell />
|
|
121
131
|
)}
|
|
122
132
|
</ActionRoot>
|
|
123
|
-
{Presence && <Presence
|
|
133
|
+
{Presence && <Presence node={node} />}
|
|
124
134
|
</>
|
|
125
135
|
);
|
|
126
136
|
};
|