@dxos/plugin-navtree 0.6.13 → 0.6.14-main.1366248

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 +611 -398
  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 +99 -41
  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
@@ -4,22 +4,27 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
8
+ import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
7
9
  import { batch } from '@preact/signals-core';
8
- import React, { useMemo, useState } from 'react';
10
+ import { type Meta } from '@storybook/react';
11
+ import React, { useEffect } from 'react';
9
12
 
10
- import { Graph } from '@dxos/app-graph';
11
- import { registerSignalRuntime } from '@dxos/echo-signals/react';
13
+ import { Graph, ROOT_ID, type NodeFilter } from '@dxos/app-graph';
14
+ import { create } from '@dxos/echo-schema';
15
+ import { registerSignalsRuntime } from '@dxos/echo-signals/react';
16
+ import { invariant } from '@dxos/invariant';
12
17
  import { faker } from '@dxos/random';
13
- import { Treegrid } from '@dxos/react-ui';
14
- import { Mosaic, Path, type MosaicDropEvent, type MosaicMoveEvent, type MosaicOperation } from '@dxos/react-ui-mosaic';
15
- import { NavTree, type NavTreeItemNode, type OpenItemIds } from '@dxos/react-ui-navtree';
18
+ import { isItem } from '@dxos/react-ui-list';
19
+ import { Path } from '@dxos/react-ui-mosaic';
16
20
  import { withLayout, withTheme } from '@dxos/storybook-utils';
17
- import { arrayMove } from '@dxos/util';
18
21
 
19
- import { type NavTreeItemGraphNode, treeItemsFromRootNode } from '../util';
22
+ import { NavTree, type NavTreeProps } from '../components';
23
+ import { type NavTreeItem } from '../types';
24
+ import { getActions, getChildren, type NavTreeItemGraphNode, treeItemsFromRootNode } from '../util';
20
25
 
21
26
  faker.seed(3);
22
- registerSignalRuntime();
27
+ registerSignalsRuntime();
23
28
 
24
29
  const createGraph = () => {
25
30
  const graph = new Graph();
@@ -45,104 +50,108 @@ const createGraph = () => {
45
50
  return graph;
46
51
  };
47
52
 
48
- const ROOT_ID = 'root';
53
+ type State = {
54
+ flatTree: NavTreeItem[];
55
+ open: string[];
56
+ current: string[];
57
+ };
58
+
49
59
  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
+ });
50
68
 
51
- const StorybookNavTree = ({ id = ROOT_ID }: { id?: string }) => {
52
- const [openItemIds, setopenItemIds] = useState<OpenItemIds>({});
53
- const items = useMemo(
54
- () => treeItemsFromRootNode(graph, graph.root as NavTreeItemGraphNode, openItemIds),
55
- [openItemIds],
56
- );
57
- const handleopenItemIdsChange = (item: NavTreeItemNode, nextOpen: boolean) => {
58
- const itemId = item.path?.join(Treegrid.PATH_SEPARATOR) ?? 'never';
59
- setopenItemIds((prevOpenItemIds) => {
60
- if (nextOpen) {
61
- return { ...prevOpenItemIds, [itemId]: true };
62
- } else {
63
- const { [itemId]: _, ...nextOpenItemIds } = prevOpenItemIds;
64
- return nextOpenItemIds;
65
- }
66
- });
67
- };
68
-
69
- const handleOver = ({ active, over }: MosaicMoveEvent<number>): MosaicOperation => {
70
- // Reject all operations that don’t match the graph’s root id
71
- if (Path.first(active.path) !== id || Path.first(over.path) !== Path.first(active.path)) {
72
- return 'reject';
73
- }
74
- // Rearrange if rearrange is supported and active and over are siblings
75
- else if (Path.siblings(over.path, active.path)) {
76
- return graph.findNode(Path.last(Path.parent(over.path)))?.properties.onRearrangeChildren ? 'rearrange' : 'reject';
77
- }
78
- // Rearrange if rearrange is supported and active is or would be a child of over
79
- else if (Path.hasChild(over.path, active.path)) {
80
- return graph.findNode(Path.last(over.path))?.properties.onRearrangeChildren ? 'rearrange' : 'reject';
81
- }
82
- // Check if transfer is supported
83
- else {
84
- const overNode = graph.findNode(Path.last(over.path));
85
- const activeNode = graph.findNode(Path.last(active.path));
86
- if (overNode && activeNode) {
87
- return 'transfer';
88
- } else {
89
- return 'reject';
90
- }
91
- }
92
- };
93
-
94
- const handleDrop = ({ operation, active, over }: MosaicDropEvent<number>) => {
95
- batch(() => {
96
- const activeNode = graph.findNode(active.item.id);
97
- const overNode = graph.findNode(over.item.id);
98
- const activeParent = activeNode && graph.nodes(activeNode, { relation: 'inbound' })[0];
99
- const overParent = overNode && graph.nodes(overNode, { relation: 'inbound' })[0];
100
- if (
101
- activeNode &&
102
- overNode &&
103
- activeParent &&
104
- overParent &&
105
- activeParent.id === overParent.id &&
106
- activeNode.id !== overNode.id &&
107
- operation === 'rearrange'
108
- ) {
109
- const ids = graph.nodes(activeParent).map((node) => node.id);
110
- const activeIndex = ids.indexOf(activeNode.id);
111
- const overIndex = ids.indexOf(overNode.id);
112
- (graph as any)._sortEdges(
113
- activeParent.id,
114
- 'outbound',
115
- arrayMove(ids, activeIndex, overIndex > -1 ? overIndex : ids.length - 1),
116
- );
117
- } else if (activeNode && activeParent && overParent && operation === 'transfer') {
118
- (graph as any)._removeEdges([{ source: activeParent.id, target: activeNode.id }]);
119
- (graph as any)._addEdges([{ source: overNode.id, target: activeNode.id }]);
120
- }
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
+ },
121
119
  });
122
- };
123
-
124
- return (
125
- <NavTree
126
- id='storybook navtree'
127
- items={items}
128
- onItemOpenChange={handleopenItemIdsChange}
129
- onDrop={handleDrop}
130
- onOver={handleOver}
131
- />
132
- );
120
+ }, []);
121
+
122
+ return <NavTree items={items} open={state.open} current={state.current} {...args} />;
133
123
  };
134
124
 
135
- export default {
136
- title: 'react-ui-navtree/Graph',
125
+ export const Default = {};
126
+
127
+ const meta: Meta = {
128
+ title: 'plugins/plugin-navtree/Graph',
137
129
  component: NavTree,
138
- decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
130
+ render: Story,
131
+ decorators: [withTheme, withLayout({ tooltips: true })],
132
+ 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
+ }
142
+ }
143
+ },
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
+ }
152
+ }
153
+ },
154
+ },
139
155
  };
140
156
 
141
- export const Default = {
142
- render: ({ debug }: { debug?: boolean }) => (
143
- <Mosaic.Root debug={debug}>
144
- <StorybookNavTree />
145
- <Mosaic.DragOverlay />
146
- </Mosaic.Root>
147
- ),
148
- };
157
+ export default meta;
@@ -0,0 +1,199 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type NodeArg } from '@dxos/app-graph';
6
+ import { create, S, type ReactiveObject, TypedObject } from '@dxos/echo-schema';
7
+ import { faker } from '@dxos/random';
8
+
9
+ // TODO(burdon): Reconcile with @dxos/plugin-debug, @dxos/react-ui/testing.
10
+ // TODO(burdon): Bug when adding stale objects to space (e.g., static objects already added in previous story invocation).
11
+
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
+ // TODO(burdon): Commit to using ECHO to generate all test data? Or convert from raw data?
19
+ export type TestItem = { id: string; type: string } & Record<string, any>;
20
+
21
+ type ObjectDataGenerator = {
22
+ createSchema?: () => S.Schema<any>;
23
+ createData: () => any;
24
+ };
25
+
26
+ type ObjectFactory<T extends ReactiveObject<any>> = {
27
+ schema?: S.Schema<any>; // TODO(burdon): Support both typed and expando schema.
28
+ createObject: () => T;
29
+ };
30
+
31
+ type ObjectFactoryMap = { [type: string]: ObjectFactory<any> };
32
+
33
+ const createFactory = ({ createSchema, createData }: ObjectDataGenerator) => {
34
+ const schema = createSchema?.();
35
+ return {
36
+ schema,
37
+ createObject: () => (schema ? create(schema, createData()) : create(createData())),
38
+ };
39
+ };
40
+
41
+ // TODO(burdon): Handle restricted values.
42
+ export const Status = ['pending', 'active', 'done'];
43
+ export const Priority = [1, 2, 3, 4, 5];
44
+
45
+ export const defaultGenerators: { [type: string]: ObjectDataGenerator } = {
46
+ document: {
47
+ createData: () => ({
48
+ title: faker.lorem.sentence(3),
49
+ body: faker.lorem.sentences({ min: 1, max: faker.number.int({ min: 1, max: 3 }) }),
50
+ }),
51
+ },
52
+
53
+ image: {
54
+ createData: () => ({
55
+ title: faker.lorem.sentence(3),
56
+ image: faker.helpers.arrayElement(data.images),
57
+ body: faker.datatype.boolean() ? faker.lorem.sentences() : undefined,
58
+ }),
59
+ },
60
+
61
+ project: {
62
+ createSchema: () =>
63
+ class ProjectType extends TypedObject({ typename: 'dxos.test.Project', version: '0.1.0' })({
64
+ title: S.String,
65
+ repo: S.String,
66
+ status: S.String,
67
+ priority: S.Number,
68
+ }) {},
69
+ createData: () => ({
70
+ title: faker.commerce.productName(),
71
+ repo: faker.datatype.boolean({ probability: 0.3 }) ? faker.internet.url() : undefined,
72
+ status: faker.helpers.arrayElement(Status),
73
+ priority: faker.helpers.arrayElement(Priority),
74
+ }),
75
+ },
76
+ };
77
+
78
+ /**
79
+ * Typed object generator.
80
+ */
81
+ export class TestObjectGenerator {
82
+ public readonly factories: ObjectFactoryMap;
83
+
84
+ constructor({ types, factories }: { types?: string[]; factories?: ObjectFactoryMap } = {}) {
85
+ this.factories =
86
+ factories ??
87
+ (types ?? Object.keys(defaultGenerators)).reduce<ObjectFactoryMap>((acc, type) => {
88
+ acc[type] = createFactory(defaultGenerators[type]);
89
+ return acc;
90
+ }, {});
91
+ }
92
+
93
+ get schema(): S.Schema<any>[] {
94
+ return Object.values(this.factories).map((f) => f.schema!);
95
+ }
96
+
97
+ createObject({ types }: { types?: string[] } = {}) {
98
+ const type = faker.helpers.arrayElement(types ?? Object.keys(this.factories));
99
+ const factory = this.factories[type];
100
+ return factory?.createObject();
101
+ }
102
+
103
+ createObjects({ types, length }: { types?: string[]; length: number }) {
104
+ return range(() => this.createObject({ types }), length);
105
+ }
106
+ }
107
+
108
+ // https://unsplash.com
109
+ // TODO(burdon): Use https://picsum.photos?
110
+ const data = {
111
+ images: [
112
+ '/images/image-1.png',
113
+ '/images/image-2.png',
114
+ '/images/image-3.png',
115
+ '/images/image-4.png',
116
+ '/images/image-5.png',
117
+ '/images/image-6.png',
118
+ ],
119
+ };
120
+
121
+ export const createTree = () => {
122
+ const generator = new TestObjectGenerator({ types: ['document'] });
123
+
124
+ const initialContent = {
125
+ id: 'root',
126
+ data: null,
127
+ type: 'root',
128
+ properties: {
129
+ label: 'Root',
130
+ icon: 'ph--circle--regular',
131
+ },
132
+ nodes: [...Array(4)].map(() => {
133
+ const l0 = generator.createObject();
134
+ return {
135
+ id: faker.string.uuid(),
136
+ data: null,
137
+ type: 'collection',
138
+ properties: {
139
+ label: l0.title,
140
+ icon: 'ph--horse--regular',
141
+ },
142
+ nodes: [
143
+ ...[...Array(4)].map(() => {
144
+ const l1 = generator.createObject();
145
+ return {
146
+ id: faker.string.uuid(),
147
+ data: null,
148
+ type: 'document',
149
+ properties: {
150
+ label: l1.title,
151
+ icon: 'ph--butterfly--regular',
152
+ },
153
+ nodes: [
154
+ {
155
+ id: `${faker.string.uuid()}__a1`,
156
+ data: () => {},
157
+ type: 'action',
158
+ properties: {
159
+ label: faker.lorem.words(2),
160
+ icon: 'ph--boat--regular',
161
+ },
162
+ },
163
+ {
164
+ id: `${faker.string.uuid()}__a2`,
165
+ data: () => {},
166
+ type: 'action',
167
+ properties: {
168
+ label: faker.lorem.words(2),
169
+ icon: 'ph--train-simple--regular',
170
+ },
171
+ },
172
+ ],
173
+ } satisfies NodeArg<any>;
174
+ }),
175
+ {
176
+ id: `${faker.string.uuid()}__a1`,
177
+ data: () => {},
178
+ type: 'action',
179
+ properties: {
180
+ label: faker.lorem.words(2),
181
+ icon: 'ph--boat--regular',
182
+ },
183
+ },
184
+ {
185
+ id: `${faker.string.uuid()}__a2`,
186
+ data: () => {},
187
+ type: 'action',
188
+ properties: {
189
+ label: faker.lorem.words(2),
190
+ icon: 'ph--train-simple--regular',
191
+ },
192
+ },
193
+ ],
194
+ } satisfies NodeArg<any>;
195
+ }),
196
+ } satisfies NodeArg<any>;
197
+
198
+ return initialContent;
199
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './generator';
6
+ export * from './visitor';
@@ -0,0 +1,152 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
6
+
7
+ import { isActionLike, type Node, type NodeArg } from '@dxos/app-graph';
8
+ import { log } from '@dxos/log';
9
+ import { Path } from '@dxos/react-ui-mosaic';
10
+
11
+ import { type NavTreeItem } from '../types';
12
+
13
+ export const flattenTree = (tree: NodeArg<any>, open: string[]): NavTreeItem[] => {
14
+ return Array.from(
15
+ visitor({
16
+ node: tree,
17
+ getItem,
18
+ isOpen: ({ path }) => open.includes(Path.create(...path)),
19
+ }),
20
+ );
21
+ };
22
+
23
+ export function* visitor({
24
+ node,
25
+ getItem,
26
+ isOpen,
27
+ }: {
28
+ node: NodeArg<any>;
29
+ getItem: (node: NodeArg<any>, parent?: readonly string[]) => NavTreeItem;
30
+ isOpen?: (node: NavTreeItem) => boolean;
31
+ }): Generator<NavTreeItem> {
32
+ const stack: [NodeArg<any>, NavTreeItem][] = [[node, getItem(node)]];
33
+ while (stack.length > 0) {
34
+ const [node, item] = stack.pop()!;
35
+ if (item.path.length > 1) {
36
+ yield item;
37
+ }
38
+
39
+ const children = Array.from(node.nodes ?? []);
40
+ if (item.path.length === 1 || isOpen?.(item)) {
41
+ for (let i = children.length - 1; i >= 0; i--) {
42
+ const child = children[i];
43
+ stack.push([child, getItem(child, item.path)]);
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ // Ensures that the same item is not created multiple times, causing the tree to be re-rendered.
50
+ const itemsCache: Record<string, NavTreeItem> = {};
51
+
52
+ export const getItem = (arg: NodeArg<any>, parent?: readonly string[]): NavTreeItem => {
53
+ const cachedItem = itemsCache[arg.id];
54
+ if (cachedItem) {
55
+ return cachedItem;
56
+ }
57
+
58
+ const children = arg.nodes?.filter((node) => !isActionLike(node)) ?? [];
59
+ const actions = arg.nodes?.filter((node) => isActionLike(node)) ?? [];
60
+ const node: Node = {
61
+ id: arg.id,
62
+ type: arg.type,
63
+ properties: arg.properties ?? {},
64
+ data: arg.data ?? null,
65
+ };
66
+
67
+ const item = {
68
+ id: arg.id,
69
+ label: arg.properties!.label,
70
+ icon: arg.properties!.icon,
71
+ path: parent ? [...parent, arg.id] : [arg.id],
72
+ ...(children.length > 0 && {
73
+ parentOf: children.map(({ id }) => id),
74
+ }),
75
+ node,
76
+ actions,
77
+ groupedActions: {},
78
+ };
79
+ itemsCache[node.id] = item;
80
+ return item;
81
+ };
82
+
83
+ export const invalidateCache = (id: string) => {
84
+ delete itemsCache[id];
85
+ };
86
+
87
+ const removeItem = (tree: NodeArg<any>, source: NavTreeItem) => {
88
+ const parent = getNode(tree, source.path.slice(1, -1));
89
+ const index = parent.nodes!.findIndex(({ id }) => id === source.id);
90
+ const item = parent.nodes![index];
91
+ parent.nodes!.splice(index, 1);
92
+ return item;
93
+ };
94
+
95
+ const getNode = (tree: NodeArg<any>, path: string[]) => {
96
+ let node = tree;
97
+ for (const part of path) {
98
+ node = node.nodes!.find(({ id }) => id === part)!;
99
+ }
100
+ return node;
101
+ };
102
+
103
+ // TODO(wittjosiah): Reconcile with react-ui-list/Tree/testing.
104
+ export const updateState = ({
105
+ state,
106
+ instruction,
107
+ source,
108
+ target,
109
+ }: {
110
+ state: NodeArg<any>;
111
+ instruction: Instruction;
112
+ source: NavTreeItem;
113
+ target: NavTreeItem;
114
+ }) => {
115
+ switch (instruction.type) {
116
+ case 'reorder-above': {
117
+ invalidateCache(source.id);
118
+ const item = removeItem(state, source);
119
+ const parent = getNode(state, target.path.slice(1, -1));
120
+ const index = parent.nodes!.findIndex(({ id }) => id === target.id);
121
+ invalidateCache(target.id);
122
+ parent.nodes!.splice(index, 0, item);
123
+ break;
124
+ }
125
+
126
+ case 'reorder-below': {
127
+ invalidateCache(source.id);
128
+ const item = removeItem(state, source);
129
+ const parent = getNode(state, target.path.slice(1, -1));
130
+ const index = parent.nodes!.findIndex(({ id }) => id === target.id);
131
+ invalidateCache(target.id);
132
+ parent.nodes!.splice(index + 1, 0, item);
133
+ break;
134
+ }
135
+
136
+ case 'make-child': {
137
+ invalidateCache(source.id);
138
+ const item = removeItem(state, source);
139
+ const parent = getNode(state, target.path.slice(1));
140
+ invalidateCache(target.id);
141
+ parent.nodes!.push(item);
142
+ break;
143
+ }
144
+
145
+ case 'instruction-blocked':
146
+ break;
147
+
148
+ default:
149
+ log.warn('Unsupported instruction', instruction);
150
+ break;
151
+ }
152
+ };
@@ -2,8 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { translations as navtreeTranslations } from '@dxos/react-ui-navtree';
6
-
7
5
  import { NAVTREE_PLUGIN } from './meta';
8
6
 
9
7
  export default [
@@ -21,8 +19,10 @@ export default [
21
19
  'released message': 'This version released {{released}}.',
22
20
  'see release label': 'See release on GitHub',
23
21
  'powered by dxos message': 'Powered by <dxos>DXOS</dxos>',
22
+ 'tree item actions label': 'More actions',
23
+ 'tree item searchlist input placeholder': 'Search for actions…',
24
+ 'empty branch message': ' ',
24
25
  },
25
26
  },
26
27
  },
27
- ...navtreeTranslations,
28
28
  ];
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Action, type ActionLike, type Node } from '@dxos/app-graph';
6
+ import { type Label } from '@dxos/react-ui';
7
+ import { type ItemType } from '@dxos/react-ui-list';
8
+
9
+ export type NavTreeItem = ItemType & {
10
+ node: Node;
11
+ actions: ActionLike[];
12
+ groupedActions: Record<string, Action[]>;
13
+ };
14
+
15
+ type SharedProperties = {
16
+ label: Label;
17
+ icon?: string;
18
+ disabled?: boolean;
19
+ testId?: string;
20
+ className?: string;
21
+ headingClassName?: string;
22
+ };
23
+
24
+ export type NodeProperties = SharedProperties & {
25
+ role?: string;
26
+ error?: string;
27
+ modified?: boolean;
28
+ palette?: string;
29
+ };
30
+
31
+ export type ActionProperties = SharedProperties & {
32
+ disposition?: string;
33
+ hidden?: boolean;
34
+ caller?: string;
35
+ menuType?: 'searchList' | 'dropdown';
36
+ keyBinding?: string | KeyBinding;
37
+ };
38
+
39
+ /**
40
+ * Platform-specific key binding.
41
+ */
42
+ export type KeyBinding = {
43
+ // NOTE: Keys come from `getHostPlatform` in `@dxos/util`.
44
+ // TODO(thure): Dedupe (similar in react-ui-deck)
45
+ windows?: string;
46
+ macos?: string;
47
+ ios?: string;
48
+ linux?: string;
49
+ unknown?: string;
50
+ };