@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.
Files changed (34) hide show
  1. package/dist/lib/browser/index.mjs +190 -187
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  5. package/dist/types/src/components/NavTree.d.ts +7 -5
  6. package/dist/types/src/components/NavTree.d.ts.map +1 -1
  7. package/dist/types/src/components/NavTree.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/NavTreeContainer.d.ts +2 -6
  9. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  10. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +1 -0
  11. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +1 -1
  12. package/dist/types/src/stories/Graph.stories.d.ts +2 -1
  13. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -1
  14. package/dist/types/src/testing/index.d.ts +1 -1
  15. package/dist/types/src/testing/index.d.ts.map +1 -1
  16. package/dist/types/src/testing/update-state.d.ts +10 -0
  17. package/dist/types/src/testing/update-state.d.ts.map +1 -0
  18. package/dist/types/src/types.d.ts +12 -3
  19. package/dist/types/src/types.d.ts.map +1 -1
  20. package/dist/types/src/util.d.ts +1 -20
  21. package/dist/types/src/util.d.ts.map +1 -1
  22. package/package.json +28 -28
  23. package/src/NavTreePlugin.tsx +103 -90
  24. package/src/components/NavTree.stories.tsx +73 -69
  25. package/src/components/NavTree.tsx +30 -20
  26. package/src/components/NavTreeContainer.tsx +71 -47
  27. package/src/stories/Graph.stories.tsx +68 -96
  28. package/src/testing/index.ts +1 -1
  29. package/src/testing/update-state.ts +70 -0
  30. package/src/types.ts +18 -3
  31. package/src/util.ts +2 -94
  32. package/dist/types/src/testing/visitor.d.ts +0 -18
  33. package/dist/types/src/testing/visitor.d.ts.map +0 -1
  34. package/src/testing/visitor.ts +0 -152
@@ -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, type NodeFilter } from '@dxos/app-graph';
24
- import { invariant } from '@dxos/invariant';
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 { LocalStorageStore } from '@dxos/local-storage';
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 NavTreeItem } from './types';
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
- type NavTreeState = {
58
- root?: NavTreeItemGraphNode;
59
- flatTree: NavTreeItem[];
60
- open: string[];
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
- const itemCache = new Map<string, NavTreeItem>();
77
+ let unsubscribe: UnsubscribeCallback | undefined;
67
78
 
68
- const state = new LocalStorageStore<NavTreeState>('dxos.org/settings/navtree', {
69
- root: undefined,
70
- get flatTree() {
71
- if (!this.root || !graph) {
72
- return [];
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
- return treeItemsFromRootNode(graph, this.root, this.open, getItem);
76
- },
77
- // TODO(thure): Do this dynamically.
78
- open: ['root', 'root~dxos.org/plugin/space-spaces', 'root~dxos.org/plugin/files'],
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
- const getItem = (node: NavTreeItemGraphNode, parent: readonly string[], filter?: NodeFilter) => {
82
- invariant(graph);
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
- const cachedItem = itemCache.get(node.id);
104
- // TODO(wittjosiah): This is not a good enough check.
105
- // Consider better ways to doing reactive transformations which retain referential equality.
106
- if (cachedItem && JSON.stringify(item) === JSON.stringify(cachedItem)) {
107
- return cachedItem;
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 handleOpenChange = (item: NavTreeItem, open: boolean) => {
115
- const path = Path.create(...item.path);
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
- if (open) {
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
- void expandOpenGraphNodes(graph, state.values.open);
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
- state.prop({ key: 'open', type: LocalStorageStore.json<string[]>() });
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
- state.values.root = graph.root as NavTreeItemGraphNode;
149
- void expandOpenGraphNodes(graph, state.values.open);
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: NavTreeItem, type: string) => {
208
+ parse: ({ item }: TreeData, type: string) => {
194
209
  switch (type) {
195
210
  case 'node':
196
- return item.node;
211
+ return item;
197
212
  case 'object':
198
- return item.node.data;
213
+ return item.data;
199
214
  case 'view-object':
200
- return { id: `${item.id}-view`, object: item.node.data };
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
- items={state.values.flatTree}
220
- current={data.activeIds as string[]}
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 (state.values.root && data.activeNode) {
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
- const additionalOpenItems = [...Array(path.length)].reduce((acc: string[], _, index) => {
266
- const itemId = Path.create(...path.slice(0, index));
267
- if (itemId.length > 0 && !state.values.open.includes(itemId)) {
268
- acc.push(itemId);
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
- return acc;
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 { isItem } from '@dxos/react-ui-list';
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, type NavTreeProps } from './NavTree';
20
- import { createTree, flattenTree, updateState } from '../testing';
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
- type State = {
26
- tree: NodeArg<any>;
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
- onOpenChange: (item: NavTreeItem, open: boolean) => {
78
- const path = Path.create(...item.path);
79
- if (open) {
80
- state.open.push(path);
81
- } else {
82
- const index = state.open.indexOf(path);
83
- if (index > -1) {
84
- state.open.splice(index, 1);
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
- onSelect: (item: NavTreeItem, current: boolean) => {
89
- if (current) {
90
- state.current.push(item.id);
91
- } else {
92
- const index = state.current.indexOf(item.id);
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 NavTreeItem } from '../types';
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<TreeProps<NavTreeItem>, 'draggable' | 'gridTemplateColumns' | 'renderColumns'> &
19
- Pick<NavTreeColumnsProps, 'loadDescendents' | 'renderPresence' | 'popoverAnchorId'>;
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<NavTreeItem>['renderColumns']>>(
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
- item={item}
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<NavTreeItem>
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
- item: NavTreeItem;
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<{ item: NavTreeItem }>;
60
+ renderPresence?: FC<{ node: Node }>;
54
61
  popoverAnchorId?: string;
55
62
  };
56
63
 
57
64
  const NavTreeColumns = ({
58
- item,
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 = item.path.length - 2;
76
+ const level = path.length - 2;
68
77
 
69
- const [primaryAction, ...secondaryActions] = item.actions.toSorted((a, b) =>
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 : item.actions)
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}/${item.id}` ? Popover.Anchor : Fragment;
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?.(item.node);
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={item.node}
107
+ parent={node}
98
108
  monolithic={isAction(primaryAction)}
99
- menuActions={isAction(primaryAction) ? [primaryAction] : item.groupedActions[primaryAction.id]}
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={item.node}
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 item={item} />}
133
+ {Presence && <Presence node={node} />}
124
134
  </>
125
135
  );
126
136
  };