@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.
Files changed (37) hide show
  1. package/dist/lib/browser/index.mjs +226 -200
  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/generator.d.ts +0 -1
  15. package/dist/types/src/testing/generator.d.ts.map +1 -1
  16. package/dist/types/src/testing/index.d.ts +1 -1
  17. package/dist/types/src/testing/index.d.ts.map +1 -1
  18. package/dist/types/src/testing/update-state.d.ts +10 -0
  19. package/dist/types/src/testing/update-state.d.ts.map +1 -0
  20. package/dist/types/src/types.d.ts +12 -3
  21. package/dist/types/src/types.d.ts.map +1 -1
  22. package/dist/types/src/util.d.ts +1 -20
  23. package/dist/types/src/util.d.ts.map +1 -1
  24. package/package.json +28 -28
  25. package/src/NavTreePlugin.tsx +103 -90
  26. package/src/components/NavTree.stories.tsx +73 -69
  27. package/src/components/NavTree.tsx +30 -20
  28. package/src/components/NavTreeContainer.tsx +97 -56
  29. package/src/stories/Graph.stories.tsx +68 -96
  30. package/src/testing/generator.ts +2 -7
  31. package/src/testing/index.ts +1 -1
  32. package/src/testing/update-state.ts +70 -0
  33. package/src/types.ts +18 -3
  34. package/src/util.ts +2 -94
  35. package/dist/types/src/testing/visitor.d.ts +0 -18
  36. package/dist/types/src/testing/visitor.d.ts.map +0 -1
  37. 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 React, { useCallback, useEffect } from 'react';
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 { isItem } from '@dxos/react-ui-list';
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 NavTreeItem } from '../types';
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 = ({ item }: { item: NavTreeItem }) => (
34
- <Surface role='presence--glyph' data={{ object: item.node.data, id: item.node.id }} />
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 = ({ items, current, open, popoverAnchorId, ...props }: NavTreeContainerProps) => {
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: NavTreeItem, target: NavTreeItem) => {
66
- if (isSpace(target.node.data)) {
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(source.node.data) || isSpace(source.node.data);
69
- } else if (isEchoObject(target.node.data)) {
70
- return isEchoObject(source.node.data);
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, actions, path }: NavTreeItem) => {
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 [parentId] = path.slice(1);
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
- // 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],
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?.find((action) => action.properties?.disposition === 'default');
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
- [items, dispatch, isLg, closeNavigationSidebar],
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 }) => isItem(source.data),
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.node as NavTreeItemGraphNode;
125
- const targetNode = target.data.node as NavTreeItemGraphNode;
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 sourceIndex = nextItems.findIndex(({ id }) => id === sourceNode.id);
142
- const targetIndex = nextItems.findIndex(({ id }) => id === targetNode.id);
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
- ? nextItems.length > 0
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.map((item) => item.node.data));
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
- }, [items]);
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
- items={items}
195
- open={open}
196
- current={current}
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 NodeFilter } from '@dxos/app-graph';
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 { isItem } from '@dxos/react-ui-list';
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, type NavTreeProps } from '../components';
23
- import { type NavTreeItem } from '../types';
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 itemCache = new Map<string, NavTreeItem>();
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
- onOpenChange: (item: NavTreeItem, open: boolean) => {
134
- const path = Path.create(...item.path);
135
- if (open) {
136
- state.open.push(path);
137
- } else {
138
- const index = state.open.indexOf(path);
139
- if (index > -1) {
140
- state.open.splice(index, 1);
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
- onSelect: (item: NavTreeItem, current: boolean) => {
145
- if (current) {
146
- state.current.push(item.id);
147
- } else {
148
- const index = state.current.indexOf(item.id);
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
  };
@@ -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 }), length);
99
+ return range(length, () => this.createObject({ types }));
105
100
  }
106
101
  }
107
102
 
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './generator';
6
- export * from './visitor';
6
+ export * from './update-state';
@@ -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 ItemType } from '@dxos/react-ui-list';
7
+ import { type MaybePromise } from '@dxos/util';
8
8
 
9
- export type NavTreeItem = ItemType & {
10
- node: Node;
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
  };