@dxos/plugin-navtree 0.6.13 → 0.6.14-main.69511f5

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.
Files changed (63) hide show
  1. package/dist/lib/browser/{chunk-U7MTKDFQ.mjs → chunk-PSVCPL4T.mjs} +1 -1
  2. package/dist/lib/browser/chunk-PSVCPL4T.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +595 -392
  4. package/dist/lib/browser/index.mjs.map +4 -4
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/meta.mjs +1 -1
  7. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  8. package/dist/types/src/components/CommandsDialog.d.ts.map +1 -1
  9. package/dist/types/src/components/HaloButton.stories.d.ts +5 -9
  10. package/dist/types/src/components/HaloButton.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/NavBarStart.d.ts.map +1 -1
  12. package/dist/types/src/components/NavTree.d.ts +20 -0
  13. package/dist/types/src/components/NavTree.d.ts.map +1 -0
  14. package/dist/types/src/components/NavTree.stories.d.ts +7 -0
  15. package/dist/types/src/components/NavTree.stories.d.ts.map +1 -0
  16. package/dist/types/src/components/NavTreeContainer.d.ts +8 -8
  17. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  18. package/dist/types/src/components/NavTreeItemAction.d.ts +23 -0
  19. package/dist/types/src/components/NavTreeItemAction.d.ts.map +1 -0
  20. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +45 -0
  21. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +1 -0
  22. package/dist/types/src/components/index.d.ts +1 -0
  23. package/dist/types/src/components/index.d.ts.map +1 -1
  24. package/dist/types/src/experimental/Tree.stories.d.ts +3 -7
  25. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  26. package/dist/types/src/meta.d.ts.map +1 -1
  27. package/dist/types/src/stories/Graph.stories.d.ts +4 -12
  28. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -1
  29. package/dist/types/src/testing/generator.d.ts +86 -0
  30. package/dist/types/src/testing/generator.d.ts.map +1 -0
  31. package/dist/types/src/testing/index.d.ts +3 -0
  32. package/dist/types/src/testing/index.d.ts.map +1 -0
  33. package/dist/types/src/testing/visitor.d.ts +18 -0
  34. package/dist/types/src/testing/visitor.d.ts.map +1 -0
  35. package/dist/types/src/translations.d.ts +5 -10
  36. package/dist/types/src/translations.d.ts.map +1 -1
  37. package/dist/types/src/types.d.ts +41 -0
  38. package/dist/types/src/types.d.ts.map +1 -0
  39. package/dist/types/src/util.d.ts +8 -9
  40. package/dist/types/src/util.d.ts.map +1 -1
  41. package/package.json +39 -34
  42. package/src/NavTreePlugin.tsx +89 -36
  43. package/src/components/CommandsDialog.tsx +2 -4
  44. package/src/components/HaloButton.stories.tsx +8 -6
  45. package/src/components/NavBarStart.tsx +2 -4
  46. package/src/components/NavTree.stories.tsx +104 -0
  47. package/src/components/NavTree.tsx +127 -0
  48. package/src/components/NavTreeContainer.tsx +132 -302
  49. package/src/components/NavTreeFooter.tsx +1 -1
  50. package/src/components/NavTreeItemAction.stories.tsx +57 -0
  51. package/src/components/NavTreeItemAction.tsx +367 -0
  52. package/src/components/index.ts +1 -0
  53. package/src/experimental/Tree.stories.tsx +11 -8
  54. package/src/experimental/Tree.tsx +2 -2
  55. package/src/meta.ts +3 -1
  56. package/src/stories/Graph.stories.tsx +111 -102
  57. package/src/testing/generator.ts +199 -0
  58. package/src/testing/index.ts +6 -0
  59. package/src/testing/visitor.ts +152 -0
  60. package/src/translations.ts +3 -3
  61. package/src/types.ts +50 -0
  62. package/src/util.ts +52 -63
  63. package/dist/lib/browser/chunk-U7MTKDFQ.mjs.map +0 -7
@@ -0,0 +1,127 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { Fragment, useCallback, useEffect, type FC } from 'react';
6
+
7
+ import { isAction, type Node } from '@dxos/app-graph';
8
+ import { Popover, toLocalizedString, Treegrid, useTranslation } from '@dxos/react-ui';
9
+ import { Tree, type TreeProps } from '@dxos/react-ui-list';
10
+ import { type MaybePromise } from '@dxos/util';
11
+
12
+ import { NavTreeItemAction } from './NavTreeItemAction';
13
+ import { NAVTREE_PLUGIN } from '../meta';
14
+ import { type NavTreeItem } from '../types';
15
+
16
+ export const NAV_TREE_ITEM = 'NavTreeItem';
17
+
18
+ export type NavTreeProps = Omit<TreeProps<NavTreeItem>, 'draggable' | 'gridTemplateColumns' | 'renderColumns'> &
19
+ Pick<NavTreeColumnsProps, 'loadDescendents' | 'renderPresence' | 'popoverAnchorId'>;
20
+
21
+ export const NavTree = ({ loadDescendents, renderPresence, popoverAnchorId, ...props }: NavTreeProps) => {
22
+ const renderColumns = useCallback<NonNullable<TreeProps<NavTreeItem>['renderColumns']>>(
23
+ ({ item, menuOpen, setMenuOpen }) => {
24
+ return (
25
+ <NavTreeColumns
26
+ item={item}
27
+ menuOpen={menuOpen}
28
+ setMenuOpen={setMenuOpen}
29
+ loadDescendents={loadDescendents}
30
+ renderPresence={renderPresence}
31
+ popoverAnchorId={popoverAnchorId}
32
+ />
33
+ );
34
+ },
35
+ [renderPresence, popoverAnchorId, loadDescendents],
36
+ );
37
+
38
+ return (
39
+ <Tree<NavTreeItem>
40
+ {...props}
41
+ draggable
42
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
43
+ classNames='pbs-1'
44
+ renderColumns={renderColumns}
45
+ />
46
+ );
47
+ };
48
+
49
+ type NavTreeColumnsProps = {
50
+ item: NavTreeItem;
51
+ menuOpen: boolean;
52
+ setMenuOpen: (open: boolean) => void;
53
+ loadDescendents?: (node: Node) => MaybePromise<void>;
54
+ renderPresence?: FC<{ item: NavTreeItem }>;
55
+ popoverAnchorId?: string;
56
+ };
57
+
58
+ const NavTreeColumns = ({
59
+ item,
60
+ menuOpen,
61
+ setMenuOpen,
62
+ loadDescendents,
63
+ renderPresence: Presence,
64
+ popoverAnchorId,
65
+ }: NavTreeColumnsProps) => {
66
+ const { t } = useTranslation(NAVTREE_PLUGIN);
67
+
68
+ const level = item.path.length - 2;
69
+
70
+ const [primaryAction, ...secondaryActions] = item.actions.toSorted((a, b) =>
71
+ a.properties?.disposition === 'toolbar' ? -1 : 1,
72
+ );
73
+
74
+ const actions = (primaryAction?.properties?.disposition === 'toolbar' ? secondaryActions : item.actions)
75
+ .flatMap((action) => (isAction(action) ? [action] : []))
76
+ .filter((action) => !action.properties?.hidden);
77
+
78
+ const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${item.id}` ? Popover.Anchor : Fragment;
79
+
80
+ // TODO(thure): Ideally this should not be necessary.
81
+ useEffect(() => {
82
+ const frame = requestAnimationFrame(() => {
83
+ void loadDescendents?.(item.node);
84
+ if (primaryAction && !isAction(primaryAction)) {
85
+ void loadDescendents?.(primaryAction);
86
+ }
87
+ });
88
+ return () => cancelAnimationFrame(frame);
89
+ }, [primaryAction]);
90
+
91
+ return (
92
+ <>
93
+ {primaryAction?.properties?.disposition === 'toolbar' ? (
94
+ <NavTreeItemAction
95
+ testId={primaryAction.properties?.testId}
96
+ label={toLocalizedString(primaryAction.properties?.label, t)}
97
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
98
+ parent={item.node}
99
+ monolithic={isAction(primaryAction)}
100
+ menuActions={isAction(primaryAction) ? [primaryAction] : item.groupedActions[primaryAction.id]}
101
+ menuType={primaryAction.properties?.menuType}
102
+ caller={NAV_TREE_ITEM}
103
+ />
104
+ ) : (
105
+ <Treegrid.Cell />
106
+ )}
107
+ <ActionRoot>
108
+ {actions.length > 0 ? (
109
+ <NavTreeItemAction
110
+ testId={`navtree.treeItem.actionsLevel${level}`}
111
+ label={t('tree item actions label')}
112
+ icon='ph--dots-three-vertical--regular'
113
+ parent={item.node}
114
+ menuActions={actions}
115
+ menuType='dropdown'
116
+ caller={NAV_TREE_ITEM}
117
+ menuOpen={menuOpen}
118
+ onChangeMenuOpen={setMenuOpen}
119
+ />
120
+ ) : (
121
+ <Treegrid.Cell />
122
+ )}
123
+ </ActionRoot>
124
+ {Presence && <Presence item={item} />}
125
+ </>
126
+ );
127
+ };
@@ -2,84 +2,59 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { type ReactNode, useCallback, useMemo } from 'react';
6
-
7
- import {
8
- NavigationAction,
9
- LayoutAction,
10
- Surface,
11
- SLUG_PATH_SEPARATOR,
12
- SLUG_COLLECTION_INDICATOR,
13
- useIntentDispatcher,
14
- } from '@dxos/app-framework';
15
- import { getGraph, isAction, isActionLike } from '@dxos/app-graph';
5
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
+ import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
7
+ import React, { useCallback, useEffect } from 'react';
8
+
9
+ import { NavigationAction, Surface, useIntentDispatcher } from '@dxos/app-framework';
10
+ import { isAction, isActionLike, type Node } from '@dxos/app-graph';
11
+ import { useGraph } from '@dxos/plugin-graph';
12
+ import { isEchoObject, isSpace } from '@dxos/react-client/echo';
16
13
  import { ElevationProvider, useMediaQuery, useSidebars } from '@dxos/react-ui';
17
- import { type MosaicDropEvent, type MosaicMoveEvent, Path } from '@dxos/react-ui-mosaic';
18
- import {
19
- NavTree,
20
- type NavTreeItemNode,
21
- type NavTreeNode,
22
- getLevel,
23
- type NavTreeActionsNode,
24
- type NavTreeProps,
25
- type NavTreeItemMoveDetails,
26
- } from '@dxos/react-ui-navtree';
14
+ import { isItem } from '@dxos/react-ui-list';
27
15
  import { arrayMove } from '@dxos/util';
28
16
 
17
+ import { NAV_TREE_ITEM, NavTree, type NavTreeProps } from './NavTree';
29
18
  import { NavTreeFooter } from './NavTreeFooter';
30
19
  import { NAVTREE_PLUGIN } from '../meta';
20
+ import { type NavTreeItem } from '../types';
31
21
  import {
32
22
  expandActions,
33
23
  expandChildren,
34
24
  getChildren,
35
25
  getParent,
36
- type NavTreeItem,
37
- type NavTreeItemGraphNode,
38
26
  resolveMigrationOperation,
39
- treeItemsFromRootNode,
27
+ type NavTreeItemGraphNode,
40
28
  } from '../util';
41
29
 
42
30
  // TODO(thure): Is NavTree truly authoritative in this regard?
43
31
  export const NODE_TYPE = 'dxos/app-graph/node';
44
32
 
45
- const renderPresence = (node: NavTreeNode): ReactNode => (
46
- <Surface role='presence--glyph' data={{ object: node.data }} />
33
+ const renderPresence = ({ item }: { item: NavTreeItem }) => (
34
+ <Surface role='presence--glyph' data={{ object: item.node.data, id: item.node.id }} />
47
35
  );
48
36
 
49
- export const NavTreeContainer = ({
50
- root,
51
- activeIds,
52
- openItemIds,
53
- onOpenItemIdsChange,
54
- attended,
55
- popoverAnchorId,
56
- }: {
57
- root: NavTreeItemGraphNode;
58
- activeIds: Set<string>;
59
- openItemIds: Record<string, true>;
60
- onOpenItemIdsChange: (nextOpenItemIds: Record<string, true>) => void;
61
- attended: Set<string>;
37
+ export type NavTreeContainerProps = {
38
+ items: NavTreeItem[];
39
+ current: string[];
40
+ open: string[];
62
41
  popoverAnchorId?: string;
63
- }) => {
42
+ } & Pick<NavTreeProps, 'onOpenChange'>;
43
+
44
+ export const NavTreeContainer = ({ items, current, open, popoverAnchorId, ...props }: NavTreeContainerProps) => {
64
45
  const { closeNavigationSidebar } = useSidebars(NAVTREE_PLUGIN);
65
46
  const [isLg] = useMediaQuery('lg', { ssr: false });
66
47
  const dispatch = useIntentDispatcher();
67
-
68
- const graph = useMemo(() => getGraph(root), [root]);
69
-
70
- const items = treeItemsFromRootNode(graph, root, openItemIds);
48
+ const { graph } = useGraph();
71
49
 
72
50
  const loadDescendents = useCallback(
73
- (node: NavTreeNode | NavTreeActionsNode) => {
74
- void expandActions(graph, node as NavTreeItemGraphNode);
51
+ (node: Node) => {
52
+ void expandActions(graph, node);
75
53
  if (!isActionLike(node)) {
76
- void expandChildren(graph, node as NavTreeItemGraphNode).then(() =>
54
+ void expandChildren(graph, node).then(() =>
77
55
  // Load one level deeper, which resolves some juddering observed on open/close.
78
56
  Promise.all(
79
- getChildren(graph, node as NavTreeItemGraphNode).flatMap((child) => [
80
- expandActions(graph, child),
81
- expandChildren(graph, child),
82
- ]),
57
+ getChildren(graph, node).flatMap((child) => [expandActions(graph, child), expandChildren(graph, child)]),
83
58
  ),
84
59
  );
85
60
  }
@@ -87,262 +62,121 @@ export const NavTreeContainer = ({
87
62
  [graph],
88
63
  );
89
64
 
90
- const handleNavigate = async ({ node, actions }: NavTreeItemNode) => {
91
- if (!node.data) {
92
- return;
93
- }
94
-
95
- // TODO(thure): Refactor opening related planks (comments in this case) to a generalized approach.
96
- await dispatch({
97
- action: NavigationAction.OPEN,
98
- data: {
99
- activeParts: node.data?.comments
100
- ? {
101
- main: [node.id],
102
- complementary: [`${node.id}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`],
103
- }
104
- : {
105
- main: [node.id],
106
- },
107
- },
108
- });
109
-
110
- await dispatch({ action: LayoutAction.SCROLL_INTO_VIEW, data: { id: node.id } });
111
-
112
- const defaultAction = actions?.find((action) => action.properties?.disposition === 'default');
113
- if (isAction(defaultAction)) {
114
- void (defaultAction.data as () => void)();
65
+ const canDrop = useCallback((source: NavTreeItem, target: NavTreeItem) => {
66
+ if (isSpace(target.node.data)) {
67
+ // TODO(wittjosiah): Find a way to only allow space as source for rearranging.
68
+ return isEchoObject(source.node.data) || isSpace(source.node.data);
69
+ } else if (isEchoObject(target.node.data)) {
70
+ return isEchoObject(source.node.data);
71
+ } else {
72
+ return false;
115
73
  }
116
- if (!isLg) {
117
- closeNavigationSidebar();
118
- }
119
- };
74
+ }, []);
120
75
 
121
- const handleItemOpenChange = ({ id, actions, path }: NavTreeItemNode, nextOpen: boolean) => {
122
- if (path) {
123
- if (nextOpen) {
124
- onOpenItemIdsChange({ ...openItemIds, [id]: true });
125
- } else {
126
- // TODO(thure): Filter vs single-remove, make setting?
127
- const { [id]: _, ...nextOpenItemIds } = openItemIds;
128
- onOpenItemIdsChange(nextOpenItemIds);
76
+ const handleSelect = useCallback(
77
+ ({ node, actions, path }: NavTreeItem) => {
78
+ if (!node.data) {
79
+ return;
129
80
  }
130
- }
131
-
132
- // TODO(wittjosiah): This is a temporary solution to ensure spaces get enabled when they are expanded.
133
- const defaultAction = actions?.find((action) => action.properties?.disposition === 'default');
134
- if (isAction(defaultAction)) {
135
- void (defaultAction.data as () => void)();
136
- }
137
- };
138
81
 
139
- const resolveItemLevel = useCallback(
140
- (overPosition: number | undefined, activeId: string | undefined, levelOffset: number) => {
141
- if (!(typeof overPosition === 'number' && activeId)) {
142
- return 1;
143
- } else {
144
- const nextItems = arrayMove(
145
- items,
146
- items.findIndex(({ id }) => id === activeId),
147
- overPosition,
148
- );
149
- const previousItem: NavTreeItem | undefined = nextItems[overPosition - 1];
150
- const nextItem: NavTreeItem | undefined = nextItems[overPosition + 1];
151
- return Math.min(
152
- previousItem ? getLevel(previousItem?.path) + 1 : 1,
153
- Math.max(nextItem ? getLevel(nextItem?.path) : 1, getLevel(nextItems[overPosition].path) + levelOffset),
154
- );
82
+ if (isAction(node)) {
83
+ const [parentId] = path.slice(1);
84
+ const parent = items.find(({ id }) => id === parentId)?.node;
85
+ void (parent && node.data({ node: parent, caller: NAV_TREE_ITEM }));
86
+ return;
155
87
  }
156
- },
157
- [items],
158
- );
159
88
 
160
- const handleMove: NavTreeProps['onMove'] = useCallback(
161
- ({ active, over, details }: MosaicMoveEvent<number, NavTreeItemMoveDetails>) => {
162
- const levelOffset = Math.floor((details?.delta?.x ?? 0) / 16);
163
- const overPosition = over.position ?? 0;
164
-
165
- const nextItems = arrayMove(
166
- items,
167
- items.findIndex(({ id }) => id === active.item.id),
168
- overPosition,
169
- );
170
-
171
- const previousItem: NavTreeItem | undefined = nextItems[overPosition - 1];
172
- const nextItem: NavTreeItem | undefined = nextItems[overPosition + 1];
173
-
174
- const activeNode = 'node' in active.item ? (active.item as NavTreeItem).node : undefined;
175
-
176
- if (!activeNode || !previousItem || !previousItem.path) {
177
- // console.log('[reject]', !activeNode, !previousItem, !previousItem.path);
178
- // log.warn('Top-level rearrange before the first item of the NavTree is unsupported at this time.');
179
- return { operation: 'reject' as const, details: { levelOffset } };
89
+ // TODO(thure): Refactor opening related planks (comments in this case) to a generalized approach.
90
+ void dispatch({
91
+ action: NavigationAction.OPEN,
92
+ data: {
93
+ activeParts: {
94
+ main: [node.id],
95
+ },
96
+ },
97
+ });
98
+
99
+ const defaultAction = actions?.find((action) => action.properties?.disposition === 'default');
100
+ if (isAction(defaultAction)) {
101
+ void (defaultAction.data as () => void)();
180
102
  }
181
103
 
182
- const overLevel = resolveItemLevel(overPosition, active.item.id, levelOffset);
183
-
184
- const previousLevel = getLevel(previousItem.path);
185
-
186
- // console.log('[over]', overLevel, previousLevel, levelOffset);
187
-
188
- if (previousLevel === overLevel - 1) {
189
- if (Path.hasChild(previousItem.id, active.item.id)) {
190
- // Previous is already parent of Active, rearrange.
191
- const operation = previousItem.node.properties.onRearrangeChildren
192
- ? ('rearrange' as const)
193
- : ('reject' as const);
194
- return { operation, details: { levelOffset } };
195
- } else {
196
- // Previous is not yet parent of Active, check transfer or copy.
197
- const operation = resolveMigrationOperation(graph, activeNode, previousItem.path, previousItem.node);
198
- // console.log('[migration result]', operation, 'Previous is not yet parent of Active, check transfer or copy.');
199
- return { operation, details: { levelOffset } };
200
- }
201
- } else if (previousLevel === overLevel) {
202
- const parent = getParent(graph, previousItem.node, previousItem.path);
203
- const parentPath = previousItem.path.slice(0, previousItem.path.length - 1);
204
- if (Path.siblings(previousItem.id, active.item.id)) {
205
- // Previous is already a sibling of Active, rearrange.
206
- const operation = parent?.properties.onRearrangeChildren ? ('rearrange' as const) : ('reject' as const);
207
- return { operation, details: { levelOffset } };
208
- } else {
209
- // Previous is not yet a sibling of Active, transfer/copy to Previous’s parent.
210
- const operation = resolveMigrationOperation(graph, activeNode, parentPath, parent);
211
- // console.log(
212
- // '[migration result]',
213
- // operation,
214
- // 'Previous is not yet a sibling of Active, transfer/copy to Previous’s parent.',
215
- // );
216
- return { operation, details: { levelOffset } };
217
- }
218
- } else if (nextItem && nextItem.path) {
219
- const nextLevel = getLevel(nextItem.path);
220
- if (nextLevel === overLevel) {
221
- const parent = getParent(graph, nextItem.node, nextItem.path);
222
- const parentPath = nextItem.path.slice(0, nextItem.path.length - 1);
223
- if (Path.siblings(nextItem.id, active.item.id)) {
224
- // Next is already a sibling of Active, rearrange.
225
- const operation = parent?.properties.onRearrangeChildren ? ('rearrange' as const) : ('reject' as const);
226
- return { operation, details: { levelOffset } };
227
- } else {
228
- // Next is not yet a sibling of Active, transfer to Next’s parent.
229
- const operation = resolveMigrationOperation(graph, activeNode, parentPath, parent);
230
- // console.log(
231
- // '[migration result]',
232
- // operation,
233
- // 'Next is not yet a sibling of Active, transfer to Next’s parent.',
234
- // );
235
- return { operation, details: { levelOffset } };
236
- }
237
- } else {
238
- // console.log('[migration result]', 'reject');
239
- return { operation: 'reject' as const, details: { levelOffset } };
240
- }
241
- } else {
242
- // console.log('[migration result]', 'reject');
243
- return { operation: 'reject' as const, details: { levelOffset } };
104
+ if (!isLg) {
105
+ closeNavigationSidebar();
244
106
  }
245
107
  },
246
- [root, items],
108
+ [items, dispatch, isLg, closeNavigationSidebar],
247
109
  );
248
110
 
249
- const handleDrop = useCallback(
250
- ({ operation, active, over, details = {} }: MosaicDropEvent<number, NavTreeItemMoveDetails>) => {
251
- if (Path.first(over.path) !== root.id) {
252
- return undefined;
253
- }
254
- const { levelOffset = 0 } = details;
255
- const overPosition = over.position ?? 0;
256
-
257
- const nextItems = arrayMove(
258
- items,
259
- items.findIndex(({ id }) => id === active.item.id),
260
- overPosition,
261
- );
262
-
263
- const activeNode = 'node' in active.item ? (active.item as NavTreeItem).node : undefined;
264
- const activeParentId = Path.parent(active.item.id);
265
-
266
- if (!activeNode) {
267
- return undefined;
268
- }
269
-
270
- const activeParent = getParent(graph, activeNode, (active.item as NavTreeItem).path ?? []);
111
+ // TODO(wittjosiah): Factor out hook.
112
+ useEffect(() => {
113
+ return monitorForElements({
114
+ canMonitor: ({ source }) => isItem(source.data),
115
+ onDrop: ({ location, source }) => {
116
+ // Didn't drop on anything.
117
+ if (!location.current.dropTargets.length) {
118
+ return;
119
+ }
271
120
 
272
- if (operation === 'rearrange') {
273
- void activeParent?.properties.onRearrangeChildren?.(
274
- nextItems.filter(({ id }) => Path.hasChild(activeParentId, id)).map(({ node }) => node.data),
275
- );
276
- return null;
277
- } else {
278
- const previousItem: NavTreeItem | undefined = nextItems[overPosition - 1];
279
- const nextItem: NavTreeItem | undefined = nextItems[overPosition + 1];
121
+ const target = location.current.dropTargets[0];
122
+ const instruction: Instruction | null = extractInstruction(target.data);
123
+ if (instruction !== null && instruction.type !== 'instruction-blocked') {
124
+ const sourceNode = source.data.node as NavTreeItemGraphNode;
125
+ const targetNode = target.data.node as NavTreeItemGraphNode;
126
+
127
+ const sourcePath = source.data.path as string[];
128
+ 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
+ const operation =
134
+ sourcePath.slice(0, -1).join() === targetPath.slice(0, -1).join() && instruction.type !== 'make-child'
135
+ ? 'rearrange'
136
+ : resolveMigrationOperation(graph, sourceNode, targetPath, targetNode);
137
+
138
+ const sourceParent = getParent(graph, sourceNode, sourcePath);
139
+ const targetParent = getParent(graph, targetNode, targetPath);
140
+
141
+ const sourceIndex = nextItems.findIndex(({ id }) => id === sourceNode.id);
142
+ const targetIndex = nextItems.findIndex(({ id }) => id === targetNode.id);
143
+ const migrationIndex =
144
+ instruction.type === 'make-child'
145
+ ? nextItems.length > 0
146
+ ? nextItems.length - 1
147
+ : 0
148
+ : instruction.type === 'reorder-below'
149
+ ? targetIndex + 1
150
+ : targetIndex;
151
+
152
+ switch (operation) {
153
+ case 'rearrange': {
154
+ arrayMove(nextItems, sourceIndex, targetIndex);
155
+ void sourceParent?.properties.onRearrangeChildren?.(nextItems.map((item) => item.node.data));
156
+ break;
157
+ }
280
158
 
281
- const overLevel = resolveItemLevel(overPosition, active.item.id, levelOffset);
159
+ case 'copy': {
160
+ const target = instruction.type === 'make-child' ? targetNode : targetParent;
161
+ void target?.properties.onCopy?.(sourceNode, migrationIndex);
162
+ break;
163
+ }
282
164
 
283
- const previousLevel = getLevel(previousItem.path);
165
+ case 'transfer': {
166
+ const target = instruction.type === 'make-child' ? targetNode : targetParent;
167
+ if (!target?.properties.onTransferStart || !sourceParent?.properties.onTransferEnd) {
168
+ break;
169
+ }
284
170
 
285
- if (operation === 'copy') {
286
- if (previousLevel === overLevel - 1) {
287
- void previousItem.node.properties.onCopy?.(activeNode, 0);
288
- return null;
289
- } else if (previousLevel === overLevel) {
290
- const parent = getParent(graph, previousItem.node, previousItem.path ?? []);
291
- void parent?.properties.onCopy?.(
292
- activeNode,
293
- getChildren(graph, parent).findIndex(({ id }) => id === previousItem.node.id) + 1,
294
- );
295
- return null;
296
- } else if (nextItem && nextItem.path) {
297
- const parent = getParent(graph, nextItem.node, nextItem.path ?? []);
298
- void parent?.properties.onCopy?.(
299
- activeNode,
300
- getChildren(graph, parent).findIndex(({ id }) => id === nextItem.node.id),
301
- );
302
- return null;
303
- }
304
- } else if (operation === 'transfer') {
305
- const onTransferEnd = activeParent?.properties.onTransferEnd;
306
- if (!onTransferEnd) {
307
- return undefined;
308
- } else if (previousLevel === overLevel - 1) {
309
- const onTransferStart = previousItem.node.properties.onTransferStart;
310
- if (onTransferStart) {
311
- void onTransferEnd(activeNode, previousItem.node);
312
- void onTransferStart(activeNode, 0);
313
- return null;
314
- }
315
- } else if (previousLevel === overLevel && previousItem.path) {
316
- const parent = getParent(graph, previousItem.node, previousItem.path);
317
- if (parent?.properties.onTransferStart) {
318
- void onTransferEnd(activeNode, previousItem.node);
319
- void parent.properties.onTransferStart(
320
- activeNode,
321
- getChildren(graph, parent).findIndex(({ id }) => id === previousItem.node.id) + 1,
322
- );
323
- return null;
324
- }
325
- } else if (nextItem && nextItem.path) {
326
- const parent = getParent(graph, nextItem.node, nextItem.path);
327
- if (parent?.properties.onTransferStart) {
328
- void onTransferEnd(activeNode, previousItem.node);
329
- void parent.properties.onTransferStart(
330
- activeNode,
331
- getChildren(graph, parent).findIndex(({ id }) => id === nextItem.node.id),
332
- );
333
- return null;
171
+ void target?.properties.onTransferStart(sourceNode, migrationIndex);
172
+ void sourceParent?.properties.onTransferEnd?.(sourceNode, target);
173
+ break;
334
174
  }
335
175
  }
336
176
  }
337
- }
338
- return undefined;
339
- },
340
- [root, items],
341
- );
342
-
343
- const handleDragEnd = useCallback(() => {
344
- onOpenItemIdsChange({ ...openItemIds });
345
- }, [onOpenItemIdsChange, openItemIds]);
177
+ },
178
+ });
179
+ }, [items]);
346
180
 
347
181
  return (
348
182
  <ElevationProvider elevation='chrome'>
@@ -351,26 +185,22 @@ export const NavTreeContainer = ({
351
185
  className='bs-full overflow-hidden row-span-3 grid grid-cols-1 grid-rows-[min-content_1fr_min-content]'
352
186
  >
353
187
  <Surface role='search-input' limit={1} />
354
- {/* TODO(thure): what gives this an inline `overflow: initial`? */}
188
+
189
+ {/* TODO(thure): What gives this an inline `overflow: initial`? */}
355
190
  <div role='none' className='!overflow-y-auto'>
356
191
  <NavTree
357
- id={root.id}
358
192
  items={items}
359
- current={activeIds}
360
- attended={attended}
361
- type={NODE_TYPE}
362
- open={openItemIds}
363
- onNavigate={handleNavigate}
364
- onItemOpenChange={handleItemOpenChange}
365
- onMove={handleMove}
366
- onDrop={handleDrop}
367
- onDragEnd={handleDragEnd}
368
- popoverAnchorId={popoverAnchorId}
369
- renderPresence={renderPresence}
370
- resolveItemLevel={resolveItemLevel}
193
+ open={open}
194
+ current={current}
371
195
  loadDescendents={loadDescendents}
196
+ renderPresence={renderPresence}
197
+ popoverAnchorId={popoverAnchorId}
198
+ canDrop={canDrop}
199
+ onSelect={handleSelect}
200
+ {...props}
372
201
  />
373
202
  </div>
203
+
374
204
  <NavTreeFooter />
375
205
  </div>
376
206
  </ElevationProvider>
@@ -35,7 +35,7 @@ const buttonStyles = 'pli-1.5 text-xs font-normal';
35
35
  const repo = 'https://github.com/dxos/dxos';
36
36
 
37
37
  export const NavTreeFooter = (props: { layoutPart?: LayoutPart }) => {
38
- const layoutPart = useDefaultValue(props.layoutPart, 'sidebar');
38
+ const layoutPart = useDefaultValue(props.layoutPart, () => 'sidebar');
39
39
  const config = useConfig();
40
40
  const { t } = useTranslation(NAVTREE_PLUGIN);
41
41
  const { navigationSidebarOpen } = useSidebars(NAVTREE_PLUGIN);