@dxos/plugin-explorer 0.8.2-main.fbd8ed0 → 0.8.2-staging.4d6ad0f

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 (143) hide show
  1. package/dist/lib/browser/ExplorerContainer-BBZ54DJS.mjs +37 -0
  2. package/dist/lib/browser/ExplorerContainer-BBZ54DJS.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-JL7ZGOFE.mjs → chunk-73GQ46YO.mjs} +415 -155
  4. package/dist/lib/{node-esm/chunk-MUHCE377.mjs.map → browser/chunk-73GQ46YO.mjs.map} +4 -4
  5. package/dist/lib/browser/{chunk-HKQSZX7S.mjs → chunk-73YTQHOT.mjs} +3 -3
  6. package/dist/lib/browser/{chunk-HKQSZX7S.mjs.map → chunk-73YTQHOT.mjs.map} +1 -1
  7. package/dist/lib/browser/chunk-M2BGAY6H.mjs +177 -0
  8. package/dist/lib/browser/chunk-M2BGAY6H.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-Z2SDLMQM.mjs → chunk-OBAFAA5V.mjs} +3 -3
  10. package/dist/lib/browser/{chunk-Z2SDLMQM.mjs.map → chunk-OBAFAA5V.mjs.map} +1 -1
  11. package/dist/lib/browser/chunk-SLB2F5AO.mjs +30 -0
  12. package/dist/lib/browser/chunk-SLB2F5AO.mjs.map +7 -0
  13. package/dist/lib/browser/index.mjs +15 -11
  14. package/dist/lib/browser/index.mjs.map +1 -1
  15. package/dist/lib/browser/{intent-resolver-7JF4DU2C.mjs → intent-resolver-FJDVBDE3.mjs} +3 -3
  16. package/dist/lib/browser/meta.json +1 -1
  17. package/dist/lib/browser/meta.mjs +1 -1
  18. package/dist/lib/browser/{react-surface-JLE6GLG6.mjs → react-surface-H3YDMXAQ.mjs} +5 -5
  19. package/dist/lib/browser/types/index.mjs +2 -2
  20. package/dist/lib/node/{ExplorerContainer-4RMWCWRX.cjs → ExplorerContainer-MVP2AM7R.cjs} +24 -16
  21. package/dist/lib/node/ExplorerContainer-MVP2AM7R.cjs.map +7 -0
  22. package/dist/lib/node/chunk-4T4LCT5R.cjs +52 -0
  23. package/dist/lib/node/chunk-4T4LCT5R.cjs.map +7 -0
  24. package/dist/lib/node/{chunk-R4TOGRPE.cjs → chunk-72H5HBTK.cjs} +414 -153
  25. package/dist/lib/node/{chunk-R4TOGRPE.cjs.map → chunk-72H5HBTK.cjs.map} +4 -4
  26. package/dist/lib/node/{chunk-VB3QE6XY.cjs → chunk-BCDVG2CH.cjs} +6 -6
  27. package/dist/lib/node/{chunk-VB3QE6XY.cjs.map → chunk-BCDVG2CH.cjs.map} +1 -1
  28. package/dist/lib/node/{chunk-6R5P3UVS.cjs → chunk-MLRYW4WQ.cjs} +7 -7
  29. package/dist/lib/node/{chunk-6R5P3UVS.cjs.map → chunk-MLRYW4WQ.cjs.map} +1 -1
  30. package/dist/lib/node/chunk-NELWWGBU.cjs +204 -0
  31. package/dist/lib/node/chunk-NELWWGBU.cjs.map +7 -0
  32. package/dist/lib/node/index.cjs +34 -31
  33. package/dist/lib/node/index.cjs.map +1 -1
  34. package/dist/lib/node/{intent-resolver-VDAHQEE7.cjs → intent-resolver-DRT67ZU4.cjs} +8 -8
  35. package/dist/lib/node/meta.cjs +3 -3
  36. package/dist/lib/node/meta.cjs.map +1 -1
  37. package/dist/lib/node/meta.json +1 -1
  38. package/dist/lib/node/{react-surface-F66QYWDR.cjs → react-surface-6ESLSM33.cjs} +11 -11
  39. package/dist/lib/node/types/index.cjs +4 -4
  40. package/dist/lib/node/types/index.cjs.map +1 -1
  41. package/dist/lib/node-esm/ExplorerContainer-APGUQI4M.mjs +38 -0
  42. package/dist/lib/node-esm/ExplorerContainer-APGUQI4M.mjs.map +7 -0
  43. package/dist/lib/node-esm/{chunk-MUHCE377.mjs → chunk-34X2VFQN.mjs} +415 -154
  44. package/dist/lib/{browser/chunk-JL7ZGOFE.mjs.map → node-esm/chunk-34X2VFQN.mjs.map} +4 -4
  45. package/dist/lib/node-esm/{chunk-PUFSCMN4.mjs → chunk-3CMBLK6W.mjs} +3 -3
  46. package/dist/lib/node-esm/{chunk-PUFSCMN4.mjs.map → chunk-3CMBLK6W.mjs.map} +1 -1
  47. package/dist/lib/node-esm/{chunk-QOJSLABX.mjs → chunk-N6VEANUZ.mjs} +3 -3
  48. package/dist/lib/node-esm/{chunk-QOJSLABX.mjs.map → chunk-N6VEANUZ.mjs.map} +1 -1
  49. package/dist/lib/node-esm/chunk-PVII2K2B.mjs +179 -0
  50. package/dist/lib/node-esm/chunk-PVII2K2B.mjs.map +7 -0
  51. package/dist/lib/node-esm/chunk-VSORIAHH.mjs +32 -0
  52. package/dist/lib/node-esm/chunk-VSORIAHH.mjs.map +7 -0
  53. package/dist/lib/node-esm/index.mjs +15 -11
  54. package/dist/lib/node-esm/index.mjs.map +1 -1
  55. package/dist/lib/node-esm/{intent-resolver-XWRVHP6H.mjs → intent-resolver-4RBV644N.mjs} +3 -3
  56. package/dist/lib/node-esm/meta.json +1 -1
  57. package/dist/lib/node-esm/meta.mjs +1 -1
  58. package/dist/lib/node-esm/{react-surface-4ZT5BQP6.mjs → react-surface-ZEVL3FXG.mjs} +5 -5
  59. package/dist/lib/node-esm/types/index.mjs +2 -2
  60. package/dist/types/src/components/ExplorerContainer.d.ts +4 -3
  61. package/dist/types/src/components/ExplorerContainer.d.ts.map +1 -1
  62. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +14 -0
  63. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +1 -0
  64. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +6 -0
  65. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +1 -0
  66. package/dist/types/src/components/Graph/ForceGraph.d.ts +8 -0
  67. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -0
  68. package/dist/types/src/components/Graph/{Graph.stories.d.ts → ForceGraph.stories.d.ts} +1 -1
  69. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -0
  70. package/dist/types/src/components/Graph/adapter.d.ts +21 -0
  71. package/dist/types/src/components/Graph/adapter.d.ts.map +1 -0
  72. package/dist/types/src/components/Graph/index.d.ts +2 -2
  73. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  74. package/dist/types/src/components/Graph/testing.d.ts +14 -0
  75. package/dist/types/src/components/Graph/testing.d.ts.map +1 -0
  76. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  77. package/dist/types/src/components/Tree/testing/generator.d.ts +8 -0
  78. package/dist/types/src/components/Tree/testing/generator.d.ts.map +1 -0
  79. package/dist/types/src/components/Tree/testing/index.d.ts +2 -0
  80. package/dist/types/src/components/Tree/testing/index.d.ts.map +1 -0
  81. package/dist/types/src/components/Tree/types/index.d.ts +3 -0
  82. package/dist/types/src/components/Tree/types/index.d.ts.map +1 -0
  83. package/dist/types/src/components/Tree/types/tree.d.ts +83 -0
  84. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -0
  85. package/dist/types/src/components/Tree/types/tree.test.d.ts +2 -0
  86. package/dist/types/src/components/Tree/types/tree.test.d.ts.map +1 -0
  87. package/dist/types/src/components/Tree/types/types.d.ts +8 -0
  88. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -0
  89. package/dist/types/src/components/index.d.ts +2 -2
  90. package/dist/types/src/hooks/index.d.ts +2 -0
  91. package/dist/types/src/hooks/index.d.ts.map +1 -0
  92. package/dist/types/src/hooks/useGraphModel.d.ts +4 -0
  93. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -0
  94. package/dist/types/src/index.d.ts +2 -1
  95. package/dist/types/src/index.d.ts.map +1 -1
  96. package/package.json +28 -28
  97. package/src/components/ExplorerContainer.tsx +11 -4
  98. package/src/components/Graph/D3ForceGraph.stories.tsx +64 -0
  99. package/src/components/Graph/D3ForceGraph.tsx +101 -0
  100. package/src/components/Graph/ForceGraph.stories.tsx +64 -0
  101. package/src/components/Graph/{Graph.tsx → ForceGraph.tsx} +19 -26
  102. package/src/components/Graph/adapter.ts +47 -0
  103. package/src/components/Graph/index.ts +2 -3
  104. package/src/components/Graph/testing.ts +57 -0
  105. package/src/components/Tree/Tree.stories.tsx +1 -1
  106. package/src/components/Tree/Tree.tsx +11 -18
  107. package/src/components/Tree/testing/generator.ts +46 -0
  108. package/src/components/Tree/testing/index.ts +5 -0
  109. package/src/components/Tree/types/index.ts +6 -0
  110. package/src/components/Tree/types/tree.test.ts +133 -0
  111. package/src/components/Tree/types/tree.ts +287 -0
  112. package/src/components/Tree/types/types.ts +41 -0
  113. package/src/hooks/index.ts +5 -0
  114. package/src/hooks/useGraphModel.ts +35 -0
  115. package/src/index.ts +2 -2
  116. package/src/meta.ts +2 -2
  117. package/dist/lib/browser/ExplorerContainer-6U4GS62Q.mjs +0 -27
  118. package/dist/lib/browser/ExplorerContainer-6U4GS62Q.mjs.map +0 -7
  119. package/dist/lib/browser/chunk-QRPUL5AH.mjs +0 -206
  120. package/dist/lib/browser/chunk-QRPUL5AH.mjs.map +0 -7
  121. package/dist/lib/node/ExplorerContainer-4RMWCWRX.cjs.map +0 -7
  122. package/dist/lib/node/chunk-4ZUNNUQD.cjs +0 -237
  123. package/dist/lib/node/chunk-4ZUNNUQD.cjs.map +0 -7
  124. package/dist/lib/node-esm/ExplorerContainer-3GOCCL7Q.mjs +0 -28
  125. package/dist/lib/node-esm/ExplorerContainer-3GOCCL7Q.mjs.map +0 -7
  126. package/dist/lib/node-esm/chunk-BJZBPCC3.mjs +0 -208
  127. package/dist/lib/node-esm/chunk-BJZBPCC3.mjs.map +0 -7
  128. package/dist/types/src/components/Graph/Graph.d.ts +0 -8
  129. package/dist/types/src/components/Graph/Graph.d.ts.map +0 -1
  130. package/dist/types/src/components/Graph/Graph.stories.d.ts.map +0 -1
  131. package/dist/types/src/components/Graph/graph-model.d.ts +0 -39
  132. package/dist/types/src/components/Graph/graph-model.d.ts.map +0 -1
  133. package/dist/types/src/components/Tree/types.d.ts +0 -8
  134. package/dist/types/src/components/Tree/types.d.ts.map +0 -1
  135. package/src/components/Graph/Graph.stories.tsx +0 -62
  136. package/src/components/Graph/graph-model.ts +0 -194
  137. package/src/components/Tree/types.ts +0 -40
  138. /package/dist/lib/browser/{intent-resolver-7JF4DU2C.mjs.map → intent-resolver-FJDVBDE3.mjs.map} +0 -0
  139. /package/dist/lib/browser/{react-surface-JLE6GLG6.mjs.map → react-surface-H3YDMXAQ.mjs.map} +0 -0
  140. /package/dist/lib/node/{intent-resolver-VDAHQEE7.cjs.map → intent-resolver-DRT67ZU4.cjs.map} +0 -0
  141. /package/dist/lib/node/{react-surface-F66QYWDR.cjs.map → react-surface-6ESLSM33.cjs.map} +0 -0
  142. /package/dist/lib/node-esm/{intent-resolver-XWRVHP6H.mjs.map → intent-resolver-4RBV644N.mjs.map} +0 -0
  143. /package/dist/lib/node-esm/{react-surface-4ZT5BQP6.mjs.map → react-surface-ZEVL3FXG.mjs.map} +0 -0
@@ -0,0 +1,101 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { type FC, useCallback, useEffect, useMemo, useRef } from 'react';
6
+
7
+ import { getTypename } from '@dxos/echo-schema';
8
+ import { SelectionModel } from '@dxos/graph';
9
+ import { type ThemedClassName } from '@dxos/react-ui';
10
+ import {
11
+ type GraphController,
12
+ GraphForceProjector,
13
+ type GraphLayoutNode,
14
+ type GraphProps,
15
+ SVG,
16
+ type SVGContext,
17
+ } from '@dxos/react-ui-graph';
18
+ import { getHashColor } from '@dxos/react-ui-theme';
19
+ import { type SpaceGraphNode, type SpaceGraphModel, type SpaceGraphEdge } from '@dxos/schema';
20
+
21
+ import '@dxos/react-ui-graph/styles/graph.css';
22
+
23
+ export type D3ForceGraphProps = ThemedClassName<
24
+ {
25
+ model?: SpaceGraphModel;
26
+ match?: RegExp;
27
+ selection?: SelectionModel;
28
+ grid?: boolean;
29
+ } & Pick<GraphProps, 'drag'>
30
+ >;
31
+
32
+ export const D3ForceGraph: FC<D3ForceGraphProps> = ({ classNames, model, selection: _selection, grid, ...props }) => {
33
+ const context = useRef<SVGContext>(null);
34
+ const projector = useMemo<GraphForceProjector | undefined>(() => {
35
+ if (context.current) {
36
+ return new GraphForceProjector(context.current, {
37
+ attributes: {
38
+ linkForce: (edge) => {
39
+ // TODO(burdon): Check type (currently assumes Employee property).
40
+ // Edge shouldn't contribute to force if it's not active.
41
+ return edge.data?.object?.active !== false;
42
+ },
43
+ },
44
+ forces: {
45
+ point: {
46
+ strength: 0.01,
47
+ },
48
+ },
49
+ });
50
+ }
51
+ }, [context.current]);
52
+
53
+ const graph = useRef<GraphController>(null);
54
+ const selection = useMemo(() => _selection ?? new SelectionModel(), [_selection]);
55
+ useEffect(() => graph.current?.repaint(), [selection.selected.value]);
56
+
57
+ const handleSelect = useCallback<NonNullable<GraphProps['onSelect']>>(
58
+ (node) => {
59
+ if (selection.contains(node.id)) {
60
+ selection.remove(node.id);
61
+ } else {
62
+ selection.add(node.id);
63
+ }
64
+ },
65
+ [selection],
66
+ );
67
+
68
+ return (
69
+ <SVG.Root ref={context} classNames={classNames}>
70
+ <SVG.Markers />
71
+ {grid && <SVG.Grid axis />}
72
+ <SVG.Zoom extent={[1 / 2, 2]}>
73
+ <SVG.Graph<SpaceGraphNode, SpaceGraphEdge>
74
+ {...props}
75
+ ref={graph}
76
+ model={model}
77
+ projector={projector}
78
+ labels={{
79
+ text: (node) => {
80
+ return node.data?.data.label ?? node.id;
81
+ },
82
+ }}
83
+ attributes={{
84
+ node: (node: GraphLayoutNode<SpaceGraphNode>) => {
85
+ const obj = node.data?.data.object;
86
+ return {
87
+ data: {
88
+ color: getHashColor(obj && getTypename(obj))?.color,
89
+ },
90
+ classes: {
91
+ 'dx-selected': selection.contains(node.id),
92
+ },
93
+ };
94
+ },
95
+ }}
96
+ onSelect={handleSelect}
97
+ />
98
+ </SVG.Zoom>
99
+ </SVG.Root>
100
+ );
101
+ };
@@ -0,0 +1,64 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta } from '@storybook/react';
8
+ import React, { useEffect, useState } from 'react';
9
+
10
+ import { faker } from '@dxos/random';
11
+ import { useClient } from '@dxos/react-client';
12
+ import { live } from '@dxos/react-client/echo';
13
+ import { type Space } from '@dxos/react-client/echo';
14
+ import { withClientProvider } from '@dxos/react-client/testing';
15
+ import { DataType } from '@dxos/schema';
16
+ import { type ValueGenerator } from '@dxos/schema/testing';
17
+ import { withLayout, withTheme, render } from '@dxos/storybook-utils';
18
+
19
+ import { ForceGraph } from './ForceGraph';
20
+ import { generate } from './testing';
21
+ import { useGraphModel } from '../../hooks';
22
+ import { ViewType } from '../../types';
23
+
24
+ const generator = faker as any as ValueGenerator;
25
+
26
+ faker.seed(1);
27
+
28
+ const DefaultStory = () => {
29
+ const client = useClient();
30
+ const [space, setSpace] = useState<Space>();
31
+ const [view, setView] = useState<ViewType>();
32
+ useEffect(() => {
33
+ const space = client.spaces.default;
34
+ void generate(space, generator);
35
+ const view = space.db.add(live(ViewType, { name: '', type: '' }));
36
+ setSpace(space);
37
+ setView(view);
38
+ }, []);
39
+
40
+ const model = useGraphModel(space);
41
+ if (!model || !space || !view) {
42
+ return null;
43
+ }
44
+
45
+ return <ForceGraph model={model} />;
46
+ };
47
+
48
+ const meta: Meta = {
49
+ title: 'plugins/plugin-explorer/ForceGraph',
50
+ component: ForceGraph,
51
+ render: render(DefaultStory),
52
+ decorators: [
53
+ withClientProvider({
54
+ createSpace: true,
55
+ types: [ViewType, DataType.HasRelationship, DataType.Organization, DataType.Project, DataType.Person],
56
+ }),
57
+ withTheme,
58
+ withLayout({ fullscreen: true }),
59
+ ],
60
+ };
61
+
62
+ export default meta;
63
+
64
+ export const Default = {};
@@ -3,30 +3,24 @@
3
3
  //
4
4
 
5
5
  import { forceLink, forceManyBody } from 'd3';
6
- import ForceGraph from 'force-graph';
6
+ import NativeForceGraph from 'force-graph';
7
7
  import React, { type FC, useEffect, useRef } from 'react';
8
8
  import { useResizeDetector } from 'react-resize-detector';
9
9
 
10
- import { type Space } from '@dxos/client/echo';
11
10
  import { filterObjectsSync, type SearchResult } from '@dxos/plugin-search';
12
- import { useAsyncState } from '@dxos/react-ui';
11
+ import { type SpaceGraphModel } from '@dxos/schema';
13
12
 
14
- import { SpaceGraphModel } from './graph-model';
13
+ import { GraphAdapter } from './adapter';
15
14
 
16
- export type GraphProps = {
17
- space: Space;
15
+ export type ForceGraphProps = {
16
+ model?: SpaceGraphModel;
18
17
  match?: RegExp;
19
18
  };
20
19
 
21
- export const Graph: FC<GraphProps> = ({ space, match }) => {
20
+ export const ForceGraph: FC<ForceGraphProps> = ({ model, match }) => {
22
21
  const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
23
22
  const rootRef = useRef<HTMLDivElement>(null);
24
- const forceGraph = useRef<ForceGraph>();
25
-
26
- const [model] = useAsyncState(
27
- async () => (space ? new SpaceGraphModel({ schema: true }).open(space) : undefined),
28
- [space],
29
- );
23
+ const forceGraph = useRef<NativeForceGraph>();
30
24
 
31
25
  const filteredRef = useRef<SearchResult[]>();
32
26
  filteredRef.current = filterObjectsSync(model?.objects ?? [], match);
@@ -35,17 +29,14 @@ export const Graph: FC<GraphProps> = ({ space, match }) => {
35
29
  if (rootRef.current) {
36
30
  // https://github.com/vasturiano/force-graph
37
31
  // https://github.com/vasturiano/3d-force-graph
38
- forceGraph.current = new ForceGraph(rootRef.current)
32
+ forceGraph.current = new NativeForceGraph(rootRef.current)
33
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#node-styling
39
34
  .nodeRelSize(6)
40
- .nodeLabel((node: any) => {
41
- if (node.type === 'schema') {
42
- return node.data.typename;
43
- }
44
-
45
- return node.id;
46
- })
35
+ .nodeLabel((node: any) => (node.type === 'schema' ? node.data.typename : node.data.label ?? node.id))
47
36
  .nodeAutoColorBy((node: any) => (node.type === 'schema' ? 'schema' : node.data.typename))
48
- .linkColor(() => 'rgba(255,255,255,0.25)');
37
+
38
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#link-styling
39
+ .linkAutoColorBy((link: any) => link.type);
49
40
  }
50
41
 
51
42
  return () => {
@@ -56,6 +47,7 @@ export const Graph: FC<GraphProps> = ({ space, match }) => {
56
47
 
57
48
  useEffect(() => {
58
49
  if (forceGraph.current && width && height && model) {
50
+ // https://github.com/vasturiano/force-graph?tab=readme-ov-file#container-layout
59
51
  forceGraph.current
60
52
  .pauseAnimation()
61
53
  .width(width)
@@ -63,17 +55,18 @@ export const Graph: FC<GraphProps> = ({ space, match }) => {
63
55
  .onEngineStop(() => {
64
56
  handleZoomToFit();
65
57
  })
58
+ .onNodeClick((node: any) => {
59
+ forceGraph.current?.emitParticle(node);
60
+ })
66
61
 
67
62
  // https://github.com/vasturiano/force-graph?tab=readme-ov-file#force-engine-d3-force-configuration
68
63
  // .d3Force('center', forceCenter().strength(0.9))
69
64
  .d3Force('link', forceLink().distance(160).strength(0.5))
70
65
  .d3Force('charge', forceManyBody().strength(-30))
71
- // .d3AlphaDecay(0.0228)
72
- // .d3VelocityDecay(0.4)
73
66
 
74
- .graphData(model.graph)
67
+ .graphData(new GraphAdapter(model))
75
68
  .warmupTicks(100)
76
- .cooldownTime(1000)
69
+ .cooldownTime(1_000)
77
70
  .resumeAnimation();
78
71
  }
79
72
  }, [model, width, height]);
@@ -0,0 +1,47 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Graph } from '@dxos/graph';
6
+
7
+ export type GraphNode = {
8
+ id?: string;
9
+ };
10
+
11
+ export type GraphLink = {
12
+ source?: string;
13
+ target?: string;
14
+ };
15
+
16
+ export type GraphData = {
17
+ nodes: GraphNode[];
18
+ links: GraphLink[];
19
+ };
20
+
21
+ export class GraphAdapter implements GraphData {
22
+ private readonly _nodes: GraphNode[] = [];
23
+ private readonly _links: GraphLink[] = [];
24
+
25
+ constructor(private readonly graph: Graph) {
26
+ this._nodes = graph.nodes.map((node) => ({
27
+ id: node.id,
28
+ type: node.type,
29
+ data: node.data,
30
+ }));
31
+
32
+ this._links = graph.edges.map((edge) => ({
33
+ type: edge.type,
34
+ source: edge.source,
35
+ target: edge.target,
36
+ data: edge.data,
37
+ }));
38
+ }
39
+
40
+ get nodes() {
41
+ return this._nodes;
42
+ }
43
+
44
+ get links() {
45
+ return this._links;
46
+ }
47
+ }
@@ -2,6 +2,5 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './graph-model';
6
-
7
- export * from './Graph';
5
+ export * from './D3ForceGraph';
6
+ export * from './ForceGraph';
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type AnyLiveObject, live, type Space } from '@dxos/client/echo';
6
+ import { Query, RelationSourceId, RelationTargetId } from '@dxos/echo-schema';
7
+ import { DataType } from '@dxos/schema';
8
+ import { createObjectFactory, type ValueGenerator, type TypeSpec } from '@dxos/schema/testing';
9
+ import { range } from '@dxos/util';
10
+
11
+ const getObject = (objects: AnyLiveObject[]) => objects[Math.floor(Math.random() * objects.length)];
12
+
13
+ const defaultTypes: TypeSpec[] = [
14
+ { type: DataType.Organization, count: 5 },
15
+ { type: DataType.Project, count: 5 },
16
+ { type: DataType.Person, count: 10 },
17
+ ];
18
+
19
+ export type GenerateOptions = {
20
+ spec?: TypeSpec[];
21
+ relations?: {
22
+ count: number;
23
+ kind: string;
24
+ };
25
+ };
26
+
27
+ const defaultRelations: GenerateOptions['relations'] = { count: 10, kind: 'friend' };
28
+
29
+ /**
30
+ * @deprecated Use @dxos/schema.
31
+ */
32
+ export const generate = async (
33
+ space: Space,
34
+ generator: ValueGenerator,
35
+ { spec = defaultTypes, relations = defaultRelations }: GenerateOptions = {},
36
+ ) => {
37
+ const createObjects = createObjectFactory(space.db, generator);
38
+ await createObjects(spec);
39
+
40
+ // Add relations between objects.
41
+ const { objects: contacts } = await space.db.query(Query.type(DataType.Person)).run();
42
+ for (const _ of range(relations.count)) {
43
+ const source = getObject(contacts);
44
+ const target = getObject(contacts);
45
+ if (source.id === target.id) {
46
+ continue;
47
+ }
48
+
49
+ space.db.add(
50
+ live(DataType.HasRelationship, {
51
+ kind: relations.kind,
52
+ [RelationSourceId]: source,
53
+ [RelationTargetId]: target,
54
+ }),
55
+ );
56
+ }
57
+ };
@@ -7,13 +7,13 @@ import '@dxos-theme';
7
7
  import { type Meta } from '@storybook/react';
8
8
  import React, { type FC, useEffect, useState } from 'react';
9
9
 
10
- import { TreeType, Tree as TreeModel } from '@dxos/plugin-outliner/types';
11
10
  import { faker } from '@dxos/random';
12
11
  import { useClient } from '@dxos/react-client';
13
12
  import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
14
13
  import { withLayout, withTheme } from '@dxos/storybook-utils';
15
14
 
16
15
  import { Tree, type TreeComponentProps } from './Tree';
16
+ import { TreeType, Tree as TreeModel } from './types';
17
17
 
18
18
  // TODO(burdon): Storybook for Graph/Tree/Plot (generics); incl. GraphModel.
19
19
  // TODO(burdon): Type for all Explorer components (Space, Object, Query, etc.) incl.
@@ -2,16 +2,15 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { useEffect, useState } from 'react';
6
- import { useResizeDetector } from 'react-resize-detector';
5
+ import React, { useEffect, useRef, useState } from 'react';
7
6
 
8
7
  import { type Space } from '@dxos/client/echo';
9
- import { createSvgContext, SVG, SVGRoot } from '@dxos/gem-core';
10
8
  import { useAsyncState } from '@dxos/react-ui';
9
+ import { SVG, type SVGContext } from '@dxos/react-ui-graph';
10
+ import { SpaceGraphModel } from '@dxos/schema';
11
11
 
12
12
  import { HierarchicalEdgeBundling, RadialTree, TidyTree } from './layout';
13
13
  import { mapGraphToTreeData, type TreeNode } from './types';
14
- import { SpaceGraphModel } from '../Graph';
15
14
 
16
15
  // TODO(burdon): Create dge bundling graph using d3.hierarchy.
17
16
  // https://observablehq.com/@d3/hierarchical-edge-bundling?intent=fork
@@ -63,10 +62,7 @@ export type TreeComponentProps<N = unknown> = {
63
62
 
64
63
  // TODO(burdon): Label accessor.
65
64
  export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: TreeComponentProps<N>) => {
66
- const [model] = useAsyncState(
67
- async () => (space ? new SpaceGraphModel().open(space, selected) : undefined),
68
- [space, selected],
69
- );
65
+ const [model] = useAsyncState(async () => (space ? new SpaceGraphModel().open(space) : undefined), [space, selected]);
70
66
 
71
67
  const [tree, setTree] = useState<TreeNode>();
72
68
  useEffect(() => {
@@ -76,11 +72,11 @@ export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: Tre
76
72
  }, true);
77
73
  }, [model]);
78
74
 
79
- const context = createSvgContext();
80
- const { ref, width = 0, height = 0 } = useResizeDetector();
75
+ const context = useRef<SVGContext>(null);
81
76
 
82
77
  useEffect(() => {
83
- if (width && height) {
78
+ if (context.current) {
79
+ const { width, height } = context.current.size!;
84
80
  const size = Math.min(width, height);
85
81
  const radius = size * 0.4;
86
82
  const options = {
@@ -98,17 +94,14 @@ export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: Tre
98
94
 
99
95
  if (tree) {
100
96
  const renderer = renderers.get(variant);
101
- renderer?.(context.ref.current!, tree, options);
97
+ renderer?.(context.current!.svg, tree, options);
102
98
  }
103
99
  }
104
- }, [tree, width, height]);
100
+ }, [context.current, tree]);
105
101
 
106
- // TODO(burdon): Provider should expand.
107
102
  return (
108
- <div ref={ref} className='flex grow overflow-hidden' onClick={() => onNodeClick?.()}>
109
- <SVGRoot context={context}>
110
- <SVG />
111
- </SVGRoot>
103
+ <div onClick={() => onNodeClick?.()}>
104
+ <SVG.Root ref={context} />
112
105
  </div>
113
106
  );
114
107
  };
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { ObjectId } from '@dxos/echo-schema';
6
+ import { range } from '@dxos/util';
7
+
8
+ import { Tree, type TreeNodeType } from '../types';
9
+
10
+ type NumberOrNumberArray = number | number[];
11
+
12
+ const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
13
+
14
+ /**
15
+ * Create hierarchical tree.
16
+ */
17
+ export const createTree = (spec: NumberOrNumberArray[] = [], createText?: () => string): Tree => {
18
+ const tree = new Tree();
19
+ tree.root.data = { text: 'root' };
20
+
21
+ const createNodes = (parent: TreeNodeType, spec: NumberOrNumberArray = 0): TreeNodeType[] => {
22
+ const count = Array.isArray(spec) ? random(spec[0], spec[1]) : spec;
23
+ return range(count, (i) => ({
24
+ id: ObjectId.random(),
25
+ children: [],
26
+ data: {
27
+ text: createText?.() ?? [parent.data.text, i + 1].join('.'),
28
+ },
29
+ }));
30
+ };
31
+
32
+ const createChildNodes = (parent: TreeNodeType, [count = 0, ...rest]: NumberOrNumberArray[]): TreeNodeType => {
33
+ const nodes = createNodes(parent, count);
34
+ nodes.forEach((n) => tree.addNode(parent, n));
35
+ if (rest.length) {
36
+ for (const node of nodes) {
37
+ createChildNodes(node, rest);
38
+ }
39
+ }
40
+
41
+ return parent;
42
+ };
43
+
44
+ createChildNodes(tree.root, spec);
45
+ return tree;
46
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './generator';
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './tree';
6
+ export * from './types';
@@ -0,0 +1,133 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { live, makeRef } from '@dxos/live-object';
8
+ import { faker } from '@dxos/random';
9
+ import { DataType } from '@dxos/schema';
10
+
11
+ import { type Tree } from './tree';
12
+ import { createTree } from '../testing';
13
+
14
+ faker.seed(0);
15
+
16
+ const print = (tree: Tree) => {
17
+ let count = 0;
18
+ tree.tranverse((node, i) => {
19
+ console.log(''.padStart(i * 2, ' '), node.data);
20
+ count++;
21
+ });
22
+
23
+ return count;
24
+ };
25
+
26
+ describe('tree', () => {
27
+ test('tree', ({ expect }) => {
28
+ {
29
+ const tree = createTree();
30
+ let count = 0;
31
+ tree.tranverse(() => {
32
+ count++;
33
+ });
34
+ expect(count).to.eq(tree.size);
35
+ expect(count).to.eq(1);
36
+ }
37
+ {
38
+ const tree = createTree([10]);
39
+ let count = 0;
40
+ tree.tranverse(() => {
41
+ count++;
42
+ });
43
+ expect(count).to.eq(tree.size);
44
+ expect(count).to.eq(1 + 10);
45
+ }
46
+ {
47
+ const tree = createTree([10, 3, 1]);
48
+ let count = 0;
49
+ tree.tranverse(() => {
50
+ count++;
51
+ });
52
+ expect(count).to.eq(tree.size);
53
+ expect(count).to.eq(1 + 10 * (1 + 3 * (1 + 1))); // 71
54
+ }
55
+ });
56
+
57
+ test('tree navigation', ({ expect }) => {
58
+ const tree = createTree([2, 3, 1]);
59
+ expect(tree.getParent(tree.root)).to.be.null;
60
+
61
+ const nodes = tree.getChildNodes(tree.root);
62
+ expect(nodes).to.have.length(2);
63
+
64
+ const first = nodes[0];
65
+ expect(first.children).to.have.length(3);
66
+ const parent = tree.getParent(first);
67
+ expect(parent).to.eq(tree.root);
68
+
69
+ const [c1, c2, c3] = tree.getChildNodes(first);
70
+ expect(tree.getParent(c1)).to.eq(first);
71
+ expect(tree.getParent(c2)).to.eq(first);
72
+ expect(tree.getParent(c3)).to.eq(first);
73
+
74
+ const [g1] = tree.getChildNodes(c1);
75
+ expect(tree.getParent(g1)).to.eq(c1);
76
+ });
77
+
78
+ /**
79
+ * root root root
80
+ * └── 1 └── 1 └── 1
81
+ * ├── 1.1 ├── 1.1 ├── 1.1
82
+ * ├── 1.2 <- indent │ ├── 1.2 <- unindent ├── 1.2
83
+ * ├── 1.3 <- indent │ └── 1.3 │ └── 1.3
84
+ * ├── 1.4 ├── 1.4 ├── 1.4
85
+ * └── 1.5 └── 1.5 └── 1.5
86
+ */
87
+ test('indent and unindent', async ({ expect }) => {
88
+ const tree = createTree([1, 5]);
89
+ const parent = tree.getNode(tree.root.children[0]);
90
+
91
+ {
92
+ const count = print(tree);
93
+ expect(count).to.eq(1 + 1 * (1 + 5));
94
+ }
95
+
96
+ // Indent
97
+ {
98
+ const child = tree.getChildNodes(parent);
99
+ tree.indentNode(child[1]);
100
+ tree.indentNode(child[2]);
101
+ expect(parent.children).to.have.length(3);
102
+ expect(child[0].children).to.have.length(2);
103
+ }
104
+
105
+ {
106
+ const count = print(tree);
107
+ expect(count).to.eq(1 + 1 * (1 + 5));
108
+ }
109
+
110
+ // Unindent
111
+ {
112
+ const child = tree.getChildNodes(parent);
113
+ const grandchild = tree.getChildNodes(child[0]);
114
+ tree.unindentNode(grandchild[0]);
115
+ expect(grandchild[0].children).to.have.length(1);
116
+ expect(parent.children).to.have.length(4);
117
+ }
118
+
119
+ {
120
+ const count = print(tree);
121
+ expect(count).to.eq(1 + 1 * (1 + 5));
122
+ }
123
+ });
124
+
125
+ test('task', ({ expect }) => {
126
+ const task = live(DataType.Task, { text: 'Test task.' });
127
+ expect(task.text).to.eq('Test task.');
128
+
129
+ const tree = createTree();
130
+ const node = tree.addNode(tree.root);
131
+ node.ref = makeRef(task);
132
+ });
133
+ });