@dxos/plugin-navtree 0.7.1-staging.8709ba7 → 0.7.2-staging.6d26b2a
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 +190 -187
- 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/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 +71 -47
- package/src/stories/Graph.stories.tsx +68 -96
- 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
|
@@ -4,49 +4,82 @@
|
|
|
4
4
|
|
|
5
5
|
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
|
6
6
|
import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
7
|
-
import
|
|
7
|
+
import { untracked } from '@preact/signals-core';
|
|
8
|
+
import React, { memo, useCallback, useEffect } from 'react';
|
|
8
9
|
|
|
9
10
|
import { NavigationAction, Surface, useIntentDispatcher } from '@dxos/app-framework';
|
|
10
11
|
import { isAction, isActionLike, type Node } from '@dxos/app-graph';
|
|
11
12
|
import { useGraph } from '@dxos/plugin-graph';
|
|
12
13
|
import { isEchoObject, isSpace } from '@dxos/react-client/echo';
|
|
13
14
|
import { ElevationProvider, useMediaQuery, useSidebars } from '@dxos/react-ui';
|
|
14
|
-
import {
|
|
15
|
+
import { isTreeData, type TreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
|
|
16
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
15
17
|
import { arrayMove } from '@dxos/util';
|
|
16
18
|
|
|
17
19
|
import { NAV_TREE_ITEM, NavTree, type NavTreeProps } from './NavTree';
|
|
18
20
|
import { NavTreeFooter } from './NavTreeFooter';
|
|
19
21
|
import { NAVTREE_PLUGIN } from '../meta';
|
|
20
|
-
import { type
|
|
22
|
+
import { type NavTreeItemGraphNode } from '../types';
|
|
21
23
|
import {
|
|
22
24
|
expandActions,
|
|
23
25
|
expandChildren,
|
|
26
|
+
getActions as naturalGetActions,
|
|
24
27
|
getChildren,
|
|
25
28
|
getParent,
|
|
26
29
|
resolveMigrationOperation,
|
|
27
|
-
type NavTreeItemGraphNode,
|
|
28
30
|
} from '../util';
|
|
29
31
|
|
|
30
32
|
// TODO(thure): Is NavTree truly authoritative in this regard?
|
|
31
33
|
export const NODE_TYPE = 'dxos/app-graph/node';
|
|
32
34
|
|
|
33
|
-
const renderPresence = ({
|
|
34
|
-
<Surface role='presence--glyph' data={{ object:
|
|
35
|
+
const renderPresence = ({ node }: { node: Node }) => (
|
|
36
|
+
<Surface role='presence--glyph' data={{ object: node.data, id: node.id }} />
|
|
35
37
|
);
|
|
36
38
|
|
|
37
39
|
export type NavTreeContainerProps = {
|
|
38
|
-
items: NavTreeItem[];
|
|
39
|
-
current: string[];
|
|
40
|
-
open: string[];
|
|
41
40
|
popoverAnchorId?: string;
|
|
42
|
-
} & Pick<NavTreeProps, 'onOpenChange'>;
|
|
41
|
+
} & Pick<NavTreeProps, 'isOpen' | 'isCurrent' | 'onOpenChange'>;
|
|
43
42
|
|
|
44
|
-
export const NavTreeContainer = ({
|
|
43
|
+
export const NavTreeContainer = memo(({ popoverAnchorId, ...props }: NavTreeContainerProps) => {
|
|
45
44
|
const { closeNavigationSidebar } = useSidebars(NAVTREE_PLUGIN);
|
|
46
45
|
const [isLg] = useMediaQuery('lg', { ssr: false });
|
|
47
46
|
const dispatch = useIntentDispatcher();
|
|
48
47
|
const { graph } = useGraph();
|
|
49
48
|
|
|
49
|
+
const getActions = useCallback((node: Node) => naturalGetActions(graph, node), [graph]);
|
|
50
|
+
const getItems = useCallback(
|
|
51
|
+
(node?: Node) => {
|
|
52
|
+
return graph.nodes(node ?? graph.root, {
|
|
53
|
+
filter: (node): node is Node => {
|
|
54
|
+
return untracked(() => {
|
|
55
|
+
const action = isActionLike(node);
|
|
56
|
+
const disposition = node.properties.disposition;
|
|
57
|
+
return !action || (action && disposition === 'item');
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
[graph],
|
|
63
|
+
);
|
|
64
|
+
const getProps = useCallback(
|
|
65
|
+
(node: Node, path: string[]): PropsFromTreeItem => {
|
|
66
|
+
const children = getChildren(graph, node, undefined, path);
|
|
67
|
+
const parentOf =
|
|
68
|
+
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
69
|
+
return {
|
|
70
|
+
id: node.id,
|
|
71
|
+
label: node.properties.label ?? node.id,
|
|
72
|
+
parentOf,
|
|
73
|
+
icon: node.properties.icon,
|
|
74
|
+
disabled: node.properties.disabled,
|
|
75
|
+
className: mx(node.properties.modified && 'italic', node.properties.className),
|
|
76
|
+
headingClassName: node.properties.headingClassName,
|
|
77
|
+
testId: node.properties.testId,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
[graph],
|
|
81
|
+
);
|
|
82
|
+
|
|
50
83
|
const loadDescendents = useCallback(
|
|
51
84
|
(node: Node) => {
|
|
52
85
|
void expandActions(graph, node);
|
|
@@ -62,26 +95,27 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
62
95
|
[graph],
|
|
63
96
|
);
|
|
64
97
|
|
|
65
|
-
const canDrop = useCallback((source:
|
|
66
|
-
|
|
98
|
+
const canDrop = useCallback((source: TreeData, target: TreeData) => {
|
|
99
|
+
const sourceNode = source.item as Node;
|
|
100
|
+
const targetNode = target.item as Node;
|
|
101
|
+
if (isSpace(targetNode.data)) {
|
|
67
102
|
// TODO(wittjosiah): Find a way to only allow space as source for rearranging.
|
|
68
|
-
return isEchoObject(
|
|
69
|
-
} else if (isEchoObject(
|
|
70
|
-
return isEchoObject(
|
|
103
|
+
return isEchoObject(sourceNode.data) || isSpace(sourceNode.data);
|
|
104
|
+
} else if (isEchoObject(targetNode.data)) {
|
|
105
|
+
return isEchoObject(sourceNode.data);
|
|
71
106
|
} else {
|
|
72
107
|
return false;
|
|
73
108
|
}
|
|
74
109
|
}, []);
|
|
75
110
|
|
|
76
111
|
const handleSelect = useCallback(
|
|
77
|
-
({ node
|
|
112
|
+
({ item: node }: { item: Node }) => {
|
|
78
113
|
if (!node.data) {
|
|
79
114
|
return;
|
|
80
115
|
}
|
|
81
116
|
|
|
82
117
|
if (isAction(node)) {
|
|
83
|
-
const [
|
|
84
|
-
const parent = items.find(({ id }) => id === parentId)?.node;
|
|
118
|
+
const [parent] = graph.nodes(node, { relation: 'inbound' });
|
|
85
119
|
void (parent && node.data({ node: parent, caller: NAV_TREE_ITEM }));
|
|
86
120
|
return;
|
|
87
121
|
}
|
|
@@ -96,7 +130,7 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
96
130
|
},
|
|
97
131
|
});
|
|
98
132
|
|
|
99
|
-
const defaultAction = actions
|
|
133
|
+
const defaultAction = graph.actions(node).find((action) => action.properties?.disposition === 'default');
|
|
100
134
|
if (isAction(defaultAction)) {
|
|
101
135
|
void (defaultAction.data as () => void)();
|
|
102
136
|
}
|
|
@@ -105,69 +139,58 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
105
139
|
closeNavigationSidebar();
|
|
106
140
|
}
|
|
107
141
|
},
|
|
108
|
-
[
|
|
142
|
+
[graph, dispatch, isLg, closeNavigationSidebar],
|
|
109
143
|
);
|
|
110
144
|
|
|
111
145
|
// TODO(wittjosiah): Factor out hook.
|
|
112
146
|
useEffect(() => {
|
|
113
147
|
return monitorForElements({
|
|
114
|
-
canMonitor: ({ source }) =>
|
|
148
|
+
canMonitor: ({ source }) => isTreeData(source.data),
|
|
115
149
|
onDrop: ({ location, source }) => {
|
|
116
150
|
// Didn't drop on anything.
|
|
117
151
|
if (!location.current.dropTargets.length) {
|
|
118
152
|
return;
|
|
119
153
|
}
|
|
120
|
-
|
|
121
154
|
const target = location.current.dropTargets[0];
|
|
122
155
|
const instruction: Instruction | null = extractInstruction(target.data);
|
|
123
156
|
if (instruction !== null && instruction.type !== 'instruction-blocked') {
|
|
124
|
-
const sourceNode = source.data.
|
|
125
|
-
const targetNode = target.data.
|
|
126
|
-
|
|
157
|
+
const sourceNode = source.data.item as NavTreeItemGraphNode;
|
|
158
|
+
const targetNode = target.data.item as NavTreeItemGraphNode;
|
|
127
159
|
const sourcePath = source.data.path as string[];
|
|
128
160
|
const targetPath = target.data.path as string[];
|
|
129
|
-
|
|
130
|
-
const nextItems = instruction.type.startsWith('reorder')
|
|
131
|
-
? items.filter((item) => item.path.slice(0, -1).join() === targetPath.slice(0, -1).join())
|
|
132
|
-
: items.filter((item) => item.path.slice(0, -1).join() === targetPath.join());
|
|
133
161
|
const operation =
|
|
134
162
|
sourcePath.slice(0, -1).join() === targetPath.slice(0, -1).join() && instruction.type !== 'make-child'
|
|
135
163
|
? 'rearrange'
|
|
136
164
|
: resolveMigrationOperation(graph, sourceNode, targetPath, targetNode);
|
|
137
|
-
|
|
138
165
|
const sourceParent = getParent(graph, sourceNode, sourcePath);
|
|
139
166
|
const targetParent = getParent(graph, targetNode, targetPath);
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
167
|
+
const sourceItems = getItems(sourceParent);
|
|
168
|
+
const targetItems = getItems(targetParent);
|
|
169
|
+
const sourceIndex = sourceItems.findIndex(({ id }) => id === sourceNode.id);
|
|
170
|
+
const targetIndex = targetItems.findIndex(({ id }) => id === targetNode.id);
|
|
143
171
|
const migrationIndex =
|
|
144
172
|
instruction.type === 'make-child'
|
|
145
|
-
?
|
|
146
|
-
? nextItems.length - 1
|
|
147
|
-
: 0
|
|
173
|
+
? undefined
|
|
148
174
|
: instruction.type === 'reorder-below'
|
|
149
175
|
? targetIndex + 1
|
|
150
176
|
: targetIndex;
|
|
151
|
-
|
|
152
177
|
switch (operation) {
|
|
153
178
|
case 'rearrange': {
|
|
179
|
+
const nextItems = sourceItems.map(({ data }) => data);
|
|
154
180
|
arrayMove(nextItems, sourceIndex, targetIndex);
|
|
155
|
-
void sourceParent?.properties.onRearrangeChildren?.(nextItems
|
|
181
|
+
void sourceParent?.properties.onRearrangeChildren?.(nextItems);
|
|
156
182
|
break;
|
|
157
183
|
}
|
|
158
|
-
|
|
159
184
|
case 'copy': {
|
|
160
185
|
const target = instruction.type === 'make-child' ? targetNode : targetParent;
|
|
161
186
|
void target?.properties.onCopy?.(sourceNode, migrationIndex);
|
|
162
187
|
break;
|
|
163
188
|
}
|
|
164
|
-
|
|
165
189
|
case 'transfer': {
|
|
166
190
|
const target = instruction.type === 'make-child' ? targetNode : targetParent;
|
|
167
191
|
if (!target?.properties.onTransferStart || !sourceParent?.properties.onTransferEnd) {
|
|
168
192
|
break;
|
|
169
193
|
}
|
|
170
|
-
|
|
171
194
|
void target?.properties.onTransferStart(sourceNode, migrationIndex);
|
|
172
195
|
void sourceParent?.properties.onTransferEnd?.(sourceNode, target);
|
|
173
196
|
break;
|
|
@@ -176,7 +199,7 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
176
199
|
}
|
|
177
200
|
},
|
|
178
201
|
});
|
|
179
|
-
}, [
|
|
202
|
+
}, [graph]);
|
|
180
203
|
|
|
181
204
|
return (
|
|
182
205
|
<ElevationProvider elevation='chrome'>
|
|
@@ -191,9 +214,10 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
191
214
|
{/* TODO(thure): What gives this an inline `overflow: initial`? */}
|
|
192
215
|
<div role='none' className='border-be border-separator !overflow-y-auto'>
|
|
193
216
|
<NavTree
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
217
|
+
id={graph.root.id}
|
|
218
|
+
getActions={getActions}
|
|
219
|
+
getItems={getItems}
|
|
220
|
+
getProps={getProps}
|
|
197
221
|
loadDescendents={loadDescendents}
|
|
198
222
|
renderPresence={renderPresence}
|
|
199
223
|
popoverAnchorId={popoverAnchorId}
|
|
@@ -207,4 +231,4 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
207
231
|
</div>
|
|
208
232
|
</ElevationProvider>
|
|
209
233
|
);
|
|
210
|
-
};
|
|
234
|
+
});
|
|
@@ -10,18 +10,16 @@ import { batch } from '@preact/signals-core';
|
|
|
10
10
|
import { type Meta } from '@storybook/react';
|
|
11
11
|
import React, { useEffect } from 'react';
|
|
12
12
|
|
|
13
|
-
import { Graph, ROOT_ID, type
|
|
14
|
-
import { create } from '@dxos/echo-schema';
|
|
13
|
+
import { Graph, ROOT_ID, type Node } from '@dxos/app-graph';
|
|
14
|
+
import { create, type ReactiveObject } from '@dxos/echo-schema';
|
|
15
15
|
import { registerSignalsRuntime } from '@dxos/echo-signals/react';
|
|
16
|
-
import { invariant } from '@dxos/invariant';
|
|
17
16
|
import { faker } from '@dxos/random';
|
|
18
|
-
import {
|
|
17
|
+
import { isTreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
|
|
19
18
|
import { Path } from '@dxos/react-ui-mosaic';
|
|
20
19
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
21
20
|
|
|
22
|
-
import { NavTree
|
|
23
|
-
import {
|
|
24
|
-
import { getActions, getChildren, type NavTreeItemGraphNode, treeItemsFromRootNode } from '../util';
|
|
21
|
+
import { NavTree } from '../components';
|
|
22
|
+
import { getActions, getChildren } from '../util';
|
|
25
23
|
|
|
26
24
|
faker.seed(3);
|
|
27
25
|
registerSignalsRuntime();
|
|
@@ -50,106 +48,80 @@ const createGraph = () => {
|
|
|
50
48
|
return graph;
|
|
51
49
|
};
|
|
52
50
|
|
|
53
|
-
type State = {
|
|
54
|
-
flatTree: NavTreeItem[];
|
|
55
|
-
open: string[];
|
|
56
|
-
current: string[];
|
|
57
|
-
};
|
|
58
|
-
|
|
59
51
|
const graph = createGraph();
|
|
60
|
-
const
|
|
61
|
-
const state = create<State>({
|
|
62
|
-
get flatTree() {
|
|
63
|
-
return treeItemsFromRootNode(graph, graph.root, this.open, getItem);
|
|
64
|
-
},
|
|
65
|
-
open: [],
|
|
66
|
-
current: [],
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const getItem = (node: NavTreeItemGraphNode, parent: readonly string[], filter?: NodeFilter) => {
|
|
70
|
-
invariant(graph);
|
|
71
|
-
const path = [...parent, node.id];
|
|
72
|
-
const { actions, groupedActions } = getActions(graph, node);
|
|
73
|
-
const children = getChildren(graph, node, filter, path);
|
|
74
|
-
const parentOf =
|
|
75
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
76
|
-
const item = {
|
|
77
|
-
id: node.id,
|
|
78
|
-
label: node.properties.label ?? node.id,
|
|
79
|
-
icon: node.properties.icon,
|
|
80
|
-
path,
|
|
81
|
-
parentOf,
|
|
82
|
-
node,
|
|
83
|
-
actions,
|
|
84
|
-
groupedActions,
|
|
85
|
-
} satisfies NavTreeItem;
|
|
86
|
-
|
|
87
|
-
const cachedItem = itemCache.get(node.id);
|
|
88
|
-
// TODO(wittjosiah): This is not a good enough check.
|
|
89
|
-
// Consider better ways to doing reactive transformations which retain referential equality.
|
|
90
|
-
if (cachedItem && JSON.stringify(item) === JSON.stringify(cachedItem)) {
|
|
91
|
-
return cachedItem;
|
|
92
|
-
} else {
|
|
93
|
-
itemCache.set(node.id, item);
|
|
94
|
-
return item;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const Story = (args: Partial<NavTreeProps>) => {
|
|
99
|
-
// NOTE: If passed directly to args, this won't be reactive.
|
|
100
|
-
const items = state.flatTree;
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
return monitorForElements({
|
|
104
|
-
canMonitor: ({ source }) => isItem(source.data),
|
|
105
|
-
onDrop: ({ location, source }) => {
|
|
106
|
-
// Didn't drop on anything.
|
|
107
|
-
if (!location.current.dropTargets.length) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const target = location.current.dropTargets[0];
|
|
112
|
-
|
|
113
|
-
const instruction: Instruction | null = extractInstruction(target.data);
|
|
114
|
-
if (instruction !== null) {
|
|
115
|
-
// TODO(wittjosiah): Implement drop logic.
|
|
116
|
-
console.log('update', instruction);
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
}, []);
|
|
121
|
-
|
|
122
|
-
return <NavTree items={items} open={state.open} current={state.current} {...args} />;
|
|
123
|
-
};
|
|
52
|
+
const state = new Map<string, ReactiveObject<{ open: boolean; current: boolean }>>();
|
|
124
53
|
|
|
125
54
|
export const Default = {};
|
|
126
55
|
|
|
127
|
-
const meta: Meta = {
|
|
56
|
+
const meta: Meta<typeof NavTree> = {
|
|
128
57
|
title: 'plugins/plugin-navtree/Graph',
|
|
129
58
|
component: NavTree,
|
|
130
|
-
render: Story,
|
|
131
59
|
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
60
|
+
render: (args) => {
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
return monitorForElements({
|
|
63
|
+
canMonitor: ({ source }) => isTreeData(source.data),
|
|
64
|
+
onDrop: ({ location, source }) => {
|
|
65
|
+
// Didn't drop on anything.
|
|
66
|
+
if (!location.current.dropTargets.length) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const target = location.current.dropTargets[0];
|
|
71
|
+
|
|
72
|
+
const instruction: Instruction | null = extractInstruction(target.data);
|
|
73
|
+
if (instruction !== null) {
|
|
74
|
+
// TODO(wittjosiah): Implement drop logic.
|
|
75
|
+
console.log('update', instruction);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
return <NavTree {...args} />;
|
|
82
|
+
},
|
|
132
83
|
args: {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
84
|
+
id: graph.root.id,
|
|
85
|
+
getActions: (node: Node) => getActions(graph, node),
|
|
86
|
+
getItems: (node?: Node) => graph.nodes(node ?? graph.root),
|
|
87
|
+
getProps: (node: Node, path: string[]) => {
|
|
88
|
+
const children = getChildren(graph, node, undefined, path);
|
|
89
|
+
const parentOf =
|
|
90
|
+
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
91
|
+
return {
|
|
92
|
+
id: node.id,
|
|
93
|
+
label: node.properties.label ?? node.id,
|
|
94
|
+
icon: node.properties.icon,
|
|
95
|
+
parentOf,
|
|
96
|
+
} satisfies PropsFromTreeItem;
|
|
97
|
+
},
|
|
98
|
+
isOpen: (_path: string[]) => {
|
|
99
|
+
const path = Path.create(..._path);
|
|
100
|
+
const value = state.get(path) ?? create({ open: false, current: false });
|
|
101
|
+
if (!state.has(path)) {
|
|
102
|
+
state.set(path, value);
|
|
142
103
|
}
|
|
104
|
+
|
|
105
|
+
return value.open;
|
|
143
106
|
},
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (index > -1) {
|
|
150
|
-
state.current.splice(index, 1);
|
|
151
|
-
}
|
|
107
|
+
isCurrent: (_path: string[]) => {
|
|
108
|
+
const path = Path.create(..._path);
|
|
109
|
+
const value = state.get(path) ?? create({ open: false, current: false });
|
|
110
|
+
if (!state.has(path)) {
|
|
111
|
+
state.set(path, value);
|
|
152
112
|
}
|
|
113
|
+
|
|
114
|
+
return value.current;
|
|
115
|
+
},
|
|
116
|
+
onOpenChange: ({ path: _path, open }) => {
|
|
117
|
+
const path = Path.create(..._path);
|
|
118
|
+
const object = state.get(path);
|
|
119
|
+
object!.open = open;
|
|
120
|
+
},
|
|
121
|
+
onSelect: ({ path: _path, current }) => {
|
|
122
|
+
const path = Path.create(..._path);
|
|
123
|
+
const object = state.get(path);
|
|
124
|
+
object!.current = current;
|
|
153
125
|
},
|
|
154
126
|
},
|
|
155
127
|
};
|
package/src/testing/index.ts
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
|
|
6
|
+
|
|
7
|
+
import { type NodeArg } from '@dxos/app-graph';
|
|
8
|
+
import { log } from '@dxos/log';
|
|
9
|
+
import { type TreeData } from '@dxos/react-ui-list';
|
|
10
|
+
|
|
11
|
+
const removeItem = (tree: NodeArg<any>, source: TreeData) => {
|
|
12
|
+
const parent = getNode(tree, source.path.slice(1, -1));
|
|
13
|
+
const index = parent.nodes!.findIndex(({ id }) => id === source.id);
|
|
14
|
+
const item = parent.nodes![index];
|
|
15
|
+
parent.nodes!.splice(index, 1);
|
|
16
|
+
return item;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const getNode = (tree: NodeArg<any>, path: string[]) => {
|
|
20
|
+
let node = tree;
|
|
21
|
+
for (const part of path) {
|
|
22
|
+
node = node.nodes!.find(({ id }) => id === part)!;
|
|
23
|
+
}
|
|
24
|
+
return node;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// TODO(wittjosiah): Reconcile with react-ui-list/Tree/testing.
|
|
28
|
+
export const updateState = ({
|
|
29
|
+
state,
|
|
30
|
+
instruction,
|
|
31
|
+
source,
|
|
32
|
+
target,
|
|
33
|
+
}: {
|
|
34
|
+
state: NodeArg<any>;
|
|
35
|
+
instruction: Instruction;
|
|
36
|
+
source: TreeData;
|
|
37
|
+
target: TreeData;
|
|
38
|
+
}) => {
|
|
39
|
+
switch (instruction.type) {
|
|
40
|
+
case 'reorder-above': {
|
|
41
|
+
const item = removeItem(state, source);
|
|
42
|
+
const parent = getNode(state, target.path.slice(1, -1));
|
|
43
|
+
const index = parent.nodes!.findIndex(({ id }) => id === target.id);
|
|
44
|
+
parent.nodes!.splice(index, 0, item);
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case 'reorder-below': {
|
|
49
|
+
const item = removeItem(state, source);
|
|
50
|
+
const parent = getNode(state, target.path.slice(1, -1));
|
|
51
|
+
const index = parent.nodes!.findIndex(({ id }) => id === target.id);
|
|
52
|
+
parent.nodes!.splice(index + 1, 0, item);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
case 'make-child': {
|
|
57
|
+
const item = removeItem(state, source);
|
|
58
|
+
const parent = getNode(state, target.path.slice(1));
|
|
59
|
+
parent.nodes!.push(item);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
case 'instruction-blocked':
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
default:
|
|
67
|
+
log.warn('Unsupported instruction', instruction);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -4,10 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { type Action, type ActionLike, type Node } from '@dxos/app-graph';
|
|
6
6
|
import { type Label } from '@dxos/react-ui';
|
|
7
|
-
import { type
|
|
7
|
+
import { type MaybePromise } from '@dxos/util';
|
|
8
8
|
|
|
9
|
-
export type
|
|
10
|
-
|
|
9
|
+
export type NavTreeItemGraphNode = Node<
|
|
10
|
+
any,
|
|
11
|
+
Partial<
|
|
12
|
+
NodeProperties & {
|
|
13
|
+
persistenceClass: string;
|
|
14
|
+
persistenceKey: string;
|
|
15
|
+
acceptPersistenceClass: Set<string>;
|
|
16
|
+
acceptPersistenceKey: Set<string>;
|
|
17
|
+
onRearrangeChildren: (nextOrder: NavTreeItemGraphNode[]) => MaybePromise<void>;
|
|
18
|
+
onCopy: (activeNode: NavTreeItemGraphNode, index?: number) => MaybePromise<void>;
|
|
19
|
+
onTransferStart: (activeNode: NavTreeItemGraphNode, index?: number) => MaybePromise<void>;
|
|
20
|
+
onTransferEnd: (activeNode: NavTreeItemGraphNode, destinationParent: NavTreeItemGraphNode) => MaybePromise<void>;
|
|
21
|
+
}
|
|
22
|
+
>
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
export type FlattenedActions = {
|
|
11
26
|
actions: ActionLike[];
|
|
12
27
|
groupedActions: Record<string, Action[]>;
|
|
13
28
|
};
|
package/src/util.ts
CHANGED
|
@@ -11,26 +11,9 @@ import {
|
|
|
11
11
|
ACTION_GROUP_TYPE,
|
|
12
12
|
isAction,
|
|
13
13
|
} from '@dxos/app-graph';
|
|
14
|
-
import {
|
|
15
|
-
import { type MaybePromise, nonNullable } from '@dxos/util';
|
|
14
|
+
import { nonNullable } from '@dxos/util';
|
|
16
15
|
|
|
17
|
-
import { type
|
|
18
|
-
|
|
19
|
-
export type NavTreeItemGraphNode = Node<
|
|
20
|
-
any,
|
|
21
|
-
Partial<
|
|
22
|
-
NodeProperties & {
|
|
23
|
-
persistenceClass: string;
|
|
24
|
-
persistenceKey: string;
|
|
25
|
-
acceptPersistenceClass: Set<string>;
|
|
26
|
-
acceptPersistenceKey: Set<string>;
|
|
27
|
-
onRearrangeChildren: (nextOrder: NavTreeItemGraphNode[]) => MaybePromise<void>;
|
|
28
|
-
onCopy: (activeNode: NavTreeItemGraphNode, index?: number) => MaybePromise<void>;
|
|
29
|
-
onTransferStart: (activeNode: NavTreeItemGraphNode, index?: number) => MaybePromise<void>;
|
|
30
|
-
onTransferEnd: (activeNode: NavTreeItemGraphNode, destinationParent: NavTreeItemGraphNode) => MaybePromise<void>;
|
|
31
|
-
}
|
|
32
|
-
>
|
|
33
|
-
>;
|
|
16
|
+
import { type FlattenedActions, type NavTreeItemGraphNode } from './types';
|
|
34
17
|
|
|
35
18
|
export const getParent = (
|
|
36
19
|
graph: Graph,
|
|
@@ -98,16 +81,6 @@ export const sortActions = (actions: Action[]): Action[] =>
|
|
|
98
81
|
return 1;
|
|
99
82
|
});
|
|
100
83
|
|
|
101
|
-
export const getTreeItemNode = (treeItems: NavTreeItem[], path?: string[]): NavTreeItem | undefined => {
|
|
102
|
-
if (!path) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return treeItems.find((treeItem) => {
|
|
107
|
-
return treeItem.path && !treeItem.path.find((id, index) => path[index] !== id);
|
|
108
|
-
});
|
|
109
|
-
};
|
|
110
|
-
|
|
111
84
|
export const getChildren = (
|
|
112
85
|
graph: Graph,
|
|
113
86
|
node: NavTreeItemGraphNode,
|
|
@@ -124,8 +97,6 @@ export const getChildren = (
|
|
|
124
97
|
.filter(nonNullable) as NavTreeItemGraphNode[];
|
|
125
98
|
};
|
|
126
99
|
|
|
127
|
-
type FlattenedActions = Required<Pick<NavTreeItem, 'actions' | 'groupedActions'>>;
|
|
128
|
-
|
|
129
100
|
export const getActions = (graph: Graph, node: Node): FlattenedActions => {
|
|
130
101
|
return graph.actions(node).reduce(
|
|
131
102
|
(acc: FlattenedActions, arg) => {
|
|
@@ -155,66 +126,3 @@ export const expandChildren = (graph: Graph, node: Node) => {
|
|
|
155
126
|
export const expandActions = (graph: Graph, node: Node) => {
|
|
156
127
|
return Promise.all([graph.expand(node, 'outbound', ACTION_TYPE), graph.expand(node, 'outbound', ACTION_GROUP_TYPE)]);
|
|
157
128
|
};
|
|
158
|
-
|
|
159
|
-
function* navTreeItemVisitor({
|
|
160
|
-
graph,
|
|
161
|
-
node,
|
|
162
|
-
open,
|
|
163
|
-
getItem,
|
|
164
|
-
path = [],
|
|
165
|
-
filter,
|
|
166
|
-
}: {
|
|
167
|
-
graph: Graph;
|
|
168
|
-
node: NavTreeItemGraphNode;
|
|
169
|
-
open: string[];
|
|
170
|
-
getItem: (node: NavTreeItemGraphNode, parent: readonly string[], filter?: NodeFilter) => NavTreeItem;
|
|
171
|
-
path?: readonly string[];
|
|
172
|
-
filter?: NodeFilter;
|
|
173
|
-
}): Generator<NavTreeItem> {
|
|
174
|
-
const stack: NavTreeItem[] = [getItem(node, path, filter)];
|
|
175
|
-
while (stack.length > 0) {
|
|
176
|
-
const nextItem = stack.pop()!;
|
|
177
|
-
if (nextItem.path.length > 1) {
|
|
178
|
-
yield nextItem;
|
|
179
|
-
}
|
|
180
|
-
const { node, path } = nextItem;
|
|
181
|
-
const children = getChildren(graph, node, filter, path);
|
|
182
|
-
const actions = graph.actions(node);
|
|
183
|
-
if (path.length === 1 || open.includes(Path.create(...path))) {
|
|
184
|
-
for (let i = actions.length - 1; i >= 0; i--) {
|
|
185
|
-
if (actions[i].properties.disposition === 'item') {
|
|
186
|
-
stack.push(getItem(actions[i], path, filter));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
190
|
-
const child = children[i] as NavTreeItemGraphNode;
|
|
191
|
-
stack.push(getItem(child, path, filter));
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Get a reactive tree node from a graph node.
|
|
199
|
-
*/
|
|
200
|
-
export const treeItemsFromRootNode = (
|
|
201
|
-
graph: Graph,
|
|
202
|
-
rootNode: NavTreeItemGraphNode,
|
|
203
|
-
open: string[],
|
|
204
|
-
getItem: (node: NavTreeItemGraphNode, parent: readonly string[]) => NavTreeItem,
|
|
205
|
-
path?: readonly string[],
|
|
206
|
-
filter?: NodeFilter,
|
|
207
|
-
): NavTreeItem[] => {
|
|
208
|
-
return Array.from(navTreeItemVisitor({ graph, node: rootNode, open, getItem, path, filter }));
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
export const expandOpenGraphNodes = (graph: Graph, open: string[]) => {
|
|
212
|
-
return Promise.all(
|
|
213
|
-
open
|
|
214
|
-
.map((id) => {
|
|
215
|
-
const node = graph.findNode(Path.last(id));
|
|
216
|
-
return node && expandChildrenAndActions(graph, node as NavTreeItemGraphNode);
|
|
217
|
-
})
|
|
218
|
-
.filter(nonNullable),
|
|
219
|
-
);
|
|
220
|
-
};
|