@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
|
@@ -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
|
-
import { NavigationAction, Surface, useIntentDispatcher } from '@dxos/app-framework';
|
|
10
|
+
import { LayoutAction, 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(({ isCurrent, 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,41 +95,58 @@ 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, path, option }: { item: Node; path: string[]; option: boolean }) => {
|
|
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
|
}
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
123
|
+
const current = isCurrent(path, node);
|
|
124
|
+
if (!current) {
|
|
125
|
+
void dispatch({
|
|
126
|
+
action: NavigationAction.OPEN,
|
|
127
|
+
data: {
|
|
128
|
+
activeParts: {
|
|
129
|
+
main: [node.id],
|
|
130
|
+
},
|
|
95
131
|
},
|
|
96
|
-
}
|
|
97
|
-
})
|
|
132
|
+
});
|
|
133
|
+
} else if (option) {
|
|
134
|
+
void dispatch({
|
|
135
|
+
action: NavigationAction.CLOSE,
|
|
136
|
+
data: {
|
|
137
|
+
activeParts: {
|
|
138
|
+
main: [node.id],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
void dispatch({
|
|
144
|
+
action: LayoutAction.SCROLL_INTO_VIEW,
|
|
145
|
+
data: { id: node.id },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
98
148
|
|
|
99
|
-
const defaultAction = actions
|
|
149
|
+
const defaultAction = graph.actions(node).find((action) => action.properties?.disposition === 'default');
|
|
100
150
|
if (isAction(defaultAction)) {
|
|
101
151
|
void (defaultAction.data as () => void)();
|
|
102
152
|
}
|
|
@@ -105,69 +155,58 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
105
155
|
closeNavigationSidebar();
|
|
106
156
|
}
|
|
107
157
|
},
|
|
108
|
-
[
|
|
158
|
+
[graph, dispatch, isCurrent, isLg, closeNavigationSidebar],
|
|
109
159
|
);
|
|
110
160
|
|
|
111
161
|
// TODO(wittjosiah): Factor out hook.
|
|
112
162
|
useEffect(() => {
|
|
113
163
|
return monitorForElements({
|
|
114
|
-
canMonitor: ({ source }) =>
|
|
164
|
+
canMonitor: ({ source }) => isTreeData(source.data),
|
|
115
165
|
onDrop: ({ location, source }) => {
|
|
116
166
|
// Didn't drop on anything.
|
|
117
167
|
if (!location.current.dropTargets.length) {
|
|
118
168
|
return;
|
|
119
169
|
}
|
|
120
|
-
|
|
121
170
|
const target = location.current.dropTargets[0];
|
|
122
171
|
const instruction: Instruction | null = extractInstruction(target.data);
|
|
123
172
|
if (instruction !== null && instruction.type !== 'instruction-blocked') {
|
|
124
|
-
const sourceNode = source.data.
|
|
125
|
-
const targetNode = target.data.
|
|
126
|
-
|
|
173
|
+
const sourceNode = source.data.item as NavTreeItemGraphNode;
|
|
174
|
+
const targetNode = target.data.item as NavTreeItemGraphNode;
|
|
127
175
|
const sourcePath = source.data.path as string[];
|
|
128
176
|
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
177
|
const operation =
|
|
134
178
|
sourcePath.slice(0, -1).join() === targetPath.slice(0, -1).join() && instruction.type !== 'make-child'
|
|
135
179
|
? 'rearrange'
|
|
136
180
|
: resolveMigrationOperation(graph, sourceNode, targetPath, targetNode);
|
|
137
|
-
|
|
138
181
|
const sourceParent = getParent(graph, sourceNode, sourcePath);
|
|
139
182
|
const targetParent = getParent(graph, targetNode, targetPath);
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const
|
|
183
|
+
const sourceItems = getItems(sourceParent);
|
|
184
|
+
const targetItems = getItems(targetParent);
|
|
185
|
+
const sourceIndex = sourceItems.findIndex(({ id }) => id === sourceNode.id);
|
|
186
|
+
const targetIndex = targetItems.findIndex(({ id }) => id === targetNode.id);
|
|
143
187
|
const migrationIndex =
|
|
144
188
|
instruction.type === 'make-child'
|
|
145
|
-
?
|
|
146
|
-
? nextItems.length - 1
|
|
147
|
-
: 0
|
|
189
|
+
? undefined
|
|
148
190
|
: instruction.type === 'reorder-below'
|
|
149
191
|
? targetIndex + 1
|
|
150
192
|
: targetIndex;
|
|
151
|
-
|
|
152
193
|
switch (operation) {
|
|
153
194
|
case 'rearrange': {
|
|
195
|
+
const nextItems = sourceItems.map(({ data }) => data);
|
|
154
196
|
arrayMove(nextItems, sourceIndex, targetIndex);
|
|
155
|
-
void sourceParent?.properties.onRearrangeChildren?.(nextItems
|
|
197
|
+
void sourceParent?.properties.onRearrangeChildren?.(nextItems);
|
|
156
198
|
break;
|
|
157
199
|
}
|
|
158
|
-
|
|
159
200
|
case 'copy': {
|
|
160
201
|
const target = instruction.type === 'make-child' ? targetNode : targetParent;
|
|
161
202
|
void target?.properties.onCopy?.(sourceNode, migrationIndex);
|
|
162
203
|
break;
|
|
163
204
|
}
|
|
164
|
-
|
|
165
205
|
case 'transfer': {
|
|
166
206
|
const target = instruction.type === 'make-child' ? targetNode : targetParent;
|
|
167
207
|
if (!target?.properties.onTransferStart || !sourceParent?.properties.onTransferEnd) {
|
|
168
208
|
break;
|
|
169
209
|
}
|
|
170
|
-
|
|
171
210
|
void target?.properties.onTransferStart(sourceNode, migrationIndex);
|
|
172
211
|
void sourceParent?.properties.onTransferEnd?.(sourceNode, target);
|
|
173
212
|
break;
|
|
@@ -176,7 +215,7 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
176
215
|
}
|
|
177
216
|
},
|
|
178
217
|
});
|
|
179
|
-
}, [
|
|
218
|
+
}, [graph]);
|
|
180
219
|
|
|
181
220
|
return (
|
|
182
221
|
<ElevationProvider elevation='chrome'>
|
|
@@ -191,9 +230,11 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
191
230
|
{/* TODO(thure): What gives this an inline `overflow: initial`? */}
|
|
192
231
|
<div role='none' className='border-be border-separator !overflow-y-auto'>
|
|
193
232
|
<NavTree
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
233
|
+
id={graph.root.id}
|
|
234
|
+
getActions={getActions}
|
|
235
|
+
getItems={getItems}
|
|
236
|
+
getProps={getProps}
|
|
237
|
+
isCurrent={isCurrent}
|
|
197
238
|
loadDescendents={loadDescendents}
|
|
198
239
|
renderPresence={renderPresence}
|
|
199
240
|
popoverAnchorId={popoverAnchorId}
|
|
@@ -207,4 +248,4 @@ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...pro
|
|
|
207
248
|
</div>
|
|
208
249
|
</ElevationProvider>
|
|
209
250
|
);
|
|
210
|
-
};
|
|
251
|
+
});
|
|
@@ -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/generator.ts
CHANGED
|
@@ -5,16 +5,11 @@
|
|
|
5
5
|
import { type NodeArg } from '@dxos/app-graph';
|
|
6
6
|
import { create, S, type ReactiveObject, TypedObject } from '@dxos/echo-schema';
|
|
7
7
|
import { faker } from '@dxos/random';
|
|
8
|
+
import { range } from '@dxos/util';
|
|
8
9
|
|
|
9
10
|
// TODO(burdon): Reconcile with @dxos/plugin-debug, @dxos/react-ui/testing.
|
|
10
11
|
// TODO(burdon): Bug when adding stale objects to space (e.g., static objects already added in previous story invocation).
|
|
11
12
|
|
|
12
|
-
// TODO(burdon): Util.
|
|
13
|
-
export const range = <T>(fn: (i: number) => T | undefined, length: number): T[] =>
|
|
14
|
-
Array.from({ length })
|
|
15
|
-
.map((_, i) => fn(i))
|
|
16
|
-
.filter(Boolean) as T[];
|
|
17
|
-
|
|
18
13
|
// TODO(burdon): Commit to using ECHO to generate all test data? Or convert from raw data?
|
|
19
14
|
export type TestItem = { id: string; type: string } & Record<string, any>;
|
|
20
15
|
|
|
@@ -101,7 +96,7 @@ export class TestObjectGenerator {
|
|
|
101
96
|
}
|
|
102
97
|
|
|
103
98
|
createObjects({ types, length }: { types?: string[]; length: number }) {
|
|
104
|
-
return range(() => this.createObject({ types })
|
|
99
|
+
return range(length, () => this.createObject({ types }));
|
|
105
100
|
}
|
|
106
101
|
}
|
|
107
102
|
|
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
|
};
|