@dxos/app-graph 0.8.4-main.72ec0f3 → 0.8.4-main.74a063c4e0
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.
- package/dist/lib/browser/chunk-AKBGYELG.mjs +1603 -0
- package/dist/lib/browser/chunk-AKBGYELG.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +29 -827
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs +1604 -0
- package/dist/lib/node-esm/chunk-HR5S4XYH.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +29 -828
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/atoms.d.ts +8 -0
- package/dist/types/src/atoms.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +111 -65
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +179 -213
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +7 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +244 -0
- package/dist/types/src/node-matcher.d.ts.map +1 -0
- package/dist/types/src/node-matcher.test.d.ts +2 -0
- package/dist/types/src/node-matcher.test.d.ts.map +1 -0
- package/dist/types/src/node.d.ts +50 -5
- package/dist/types/src/node.d.ts.map +1 -1
- package/dist/types/src/stories/EchoGraph.stories.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +2 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts +31 -0
- package/dist/types/src/testing/setup-graph-builder.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +39 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +47 -35
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +995 -112
- package/src/graph-builder.ts +705 -284
- package/src/graph.test.ts +448 -120
- package/src/graph.ts +1019 -413
- package/src/index.ts +10 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +314 -0
- package/src/node.ts +82 -8
- package/src/stories/EchoGraph.stories.tsx +151 -121
- package/src/stories/Tree.tsx +2 -2
- package/src/testing/index.ts +5 -0
- package/src/testing/setup-graph-builder.ts +41 -0
- package/src/util.ts +95 -0
- package/dist/types/src/experimental/graph-projections.test.d.ts +0 -25
- package/dist/types/src/experimental/graph-projections.test.d.ts.map +0 -1
- package/dist/types/src/signals-integration.test.d.ts +0 -2
- package/dist/types/src/signals-integration.test.d.ts.map +0 -1
- package/dist/types/src/testing.d.ts +0 -5
- package/dist/types/src/testing.d.ts.map +0 -1
- package/src/experimental/graph-projections.test.ts +0 -56
- package/src/signals-integration.test.ts +0 -219
- package/src/testing.ts +0 -20
package/src/node.ts
CHANGED
|
@@ -2,9 +2,30 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import type * as Context from 'effect/Context';
|
|
6
|
+
import type * as Effect from 'effect/Effect';
|
|
6
7
|
|
|
7
|
-
import {
|
|
8
|
+
import { type MakeOptional } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Root node ID.
|
|
12
|
+
*/
|
|
13
|
+
export const RootId = 'root';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Root node type.
|
|
17
|
+
*/
|
|
18
|
+
export const RootType = 'org.dxos.type.graphRoot';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Action node type.
|
|
22
|
+
*/
|
|
23
|
+
export const ActionType = 'org.dxos.type.graphAction';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Action group node type.
|
|
27
|
+
*/
|
|
28
|
+
export const ActionGroupType = 'org.dxos.type.graphActionGroup';
|
|
8
29
|
|
|
9
30
|
/**
|
|
10
31
|
* Represents a node in the graph.
|
|
@@ -48,7 +69,19 @@ export type NodeFilter<TData = any, TProperties extends Record<string, any> = Re
|
|
|
48
69
|
connectedNode: Node,
|
|
49
70
|
) => node is Node<TData, TProperties>;
|
|
50
71
|
|
|
51
|
-
export type
|
|
72
|
+
export type RelationDirection = 'outbound' | 'inbound';
|
|
73
|
+
|
|
74
|
+
export type Relation = Readonly<{
|
|
75
|
+
kind: string;
|
|
76
|
+
direction: RelationDirection;
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
export type RelationInput = Relation | string;
|
|
80
|
+
|
|
81
|
+
export const relation = (kind: string, direction: RelationDirection = 'outbound'): Relation => ({ kind, direction });
|
|
82
|
+
// TODO(wittjosiah): Consider moving these helpers out of the core API.
|
|
83
|
+
export const childRelation = (direction: RelationDirection = 'outbound'): Relation => relation('child', direction);
|
|
84
|
+
export const actionRelation = (direction: RelationDirection = 'outbound'): Relation => relation('action', direction);
|
|
52
85
|
|
|
53
86
|
export const isGraphNode = (data: unknown): data is Node =>
|
|
54
87
|
data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
|
|
@@ -63,30 +96,45 @@ export type NodeArg<TData, TProperties extends Record<string, any> = Record<stri
|
|
|
63
96
|
nodes?: NodeArg<unknown>[];
|
|
64
97
|
|
|
65
98
|
/** Will automatically add specified edges. */
|
|
66
|
-
edges?: [string,
|
|
99
|
+
edges?: [string, RelationInput][];
|
|
67
100
|
};
|
|
68
101
|
|
|
69
102
|
//
|
|
70
103
|
// Actions
|
|
71
104
|
//
|
|
72
105
|
|
|
73
|
-
export type
|
|
106
|
+
export type InvokeProps = {
|
|
74
107
|
/** Node the invoked action is connected to. */
|
|
75
108
|
parent?: Node;
|
|
76
109
|
|
|
110
|
+
/** Path from root to the node in the current tree context. */
|
|
111
|
+
path?: string[];
|
|
112
|
+
|
|
77
113
|
caller?: string;
|
|
78
114
|
};
|
|
79
115
|
|
|
80
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Action data is an Effect-returning function.
|
|
118
|
+
* The Effect is provided with captured context at execution time.
|
|
119
|
+
*/
|
|
120
|
+
export type ActionData<R = never> = (params?: InvokeProps) => Effect.Effect<any, Error, R>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Context captured at extension creation time.
|
|
124
|
+
* Automatically provided to action Effects at execution.
|
|
125
|
+
*/
|
|
126
|
+
export type ActionContext = Context.Context<any>;
|
|
81
127
|
|
|
82
128
|
export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
|
|
83
129
|
Omit<Node<ActionData, TProperties>, 'properties'> & {
|
|
84
130
|
properties: Readonly<TProperties>;
|
|
131
|
+
/** Captured context from extension creation. Provided automatically at action execution. */
|
|
132
|
+
_actionContext?: ActionContext;
|
|
85
133
|
}
|
|
86
134
|
>;
|
|
87
135
|
|
|
88
136
|
export const isAction = (data: unknown): data is Action =>
|
|
89
|
-
isGraphNode(data) ? typeof data.data === 'function' && data.type ===
|
|
137
|
+
isGraphNode(data) ? typeof data.data === 'function' && data.type === ActionType : false;
|
|
90
138
|
|
|
91
139
|
export const actionGroupSymbol = Symbol('ActionGroup');
|
|
92
140
|
|
|
@@ -97,8 +145,34 @@ export type ActionGroup<TProperties extends Record<string, any> = Record<string,
|
|
|
97
145
|
>;
|
|
98
146
|
|
|
99
147
|
export const isActionGroup = (data: unknown): data is ActionGroup =>
|
|
100
|
-
isGraphNode(data) ? data.data === actionGroupSymbol && data.type ===
|
|
148
|
+
isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ActionGroupType : false;
|
|
101
149
|
|
|
102
150
|
export type ActionLike = Action | ActionGroup;
|
|
103
151
|
|
|
104
152
|
export const isActionLike = (data: unknown): data is Action | ActionGroup => isAction(data) || isActionGroup(data);
|
|
153
|
+
|
|
154
|
+
//
|
|
155
|
+
// Node Factories
|
|
156
|
+
//
|
|
157
|
+
|
|
158
|
+
/** Typed factory for constructing a NodeArg. Provides auto-complete and type validation. */
|
|
159
|
+
export const make = <TData = any, TProperties extends Record<string, any> = Record<string, any>>(
|
|
160
|
+
arg: NodeArg<TData, TProperties>,
|
|
161
|
+
): NodeArg<TData, TProperties> => arg;
|
|
162
|
+
|
|
163
|
+
/** Create an action node. Automatically sets `type: ActionType`. */
|
|
164
|
+
export const makeAction = <R = never>(
|
|
165
|
+
arg: Omit<NodeArg<ActionData<R>>, 'type' | 'nodes' | 'edges'>,
|
|
166
|
+
): NodeArg<ActionData<R>> => ({
|
|
167
|
+
...arg,
|
|
168
|
+
type: ActionType,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/** Create an action group node. Automatically sets `type` and `data`. */
|
|
172
|
+
export const makeActionGroup = (
|
|
173
|
+
arg: Omit<NodeArg<typeof actionGroupSymbol>, 'type' | 'data' | 'nodes' | 'edges'>,
|
|
174
|
+
): NodeArg<typeof actionGroupSymbol> => ({
|
|
175
|
+
...arg,
|
|
176
|
+
type: ActionGroupType,
|
|
177
|
+
data: actionGroupSymbol,
|
|
178
|
+
});
|
|
@@ -6,34 +6,25 @@ import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom
|
|
|
6
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
7
|
import * as Function from 'effect/Function';
|
|
8
8
|
import * as Option from 'effect/Option';
|
|
9
|
-
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type QueryResult,
|
|
17
|
-
type Space,
|
|
18
|
-
SpaceState,
|
|
19
|
-
isSpace,
|
|
20
|
-
live,
|
|
21
|
-
} from '@dxos/client/echo';
|
|
22
|
-
import { Obj, Type } from '@dxos/echo';
|
|
23
|
-
import { faker } from '@dxos/random';
|
|
9
|
+
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import { Filter, type Space, SpaceState, isSpace } from '@dxos/client/echo';
|
|
12
|
+
import { Obj, Query } from '@dxos/echo';
|
|
13
|
+
import { AtomObj, AtomQuery } from '@dxos/echo-atom';
|
|
14
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
15
|
+
import { random } from '@dxos/random';
|
|
24
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
25
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
26
18
|
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
19
|
+
import { Path, Tree, type TreeModel } from '@dxos/react-ui-list';
|
|
27
20
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
|
|
31
|
-
|
|
32
|
-
import { type ExpandableGraph, ROOT_ID } from '../graph';
|
|
33
|
-
import { GraphBuilder, atomFromObservable, createExtension } from '../graph-builder';
|
|
34
|
-
import { type Node } from '../node';
|
|
35
|
-
import { atomFromQuery } from '../testing';
|
|
21
|
+
import { getSize, mx } from '@dxos/ui-theme';
|
|
22
|
+
import { safeParseInt } from '@dxos/util';
|
|
36
23
|
|
|
24
|
+
import * as CreateAtom from '../atoms';
|
|
25
|
+
import * as Graph from '../graph';
|
|
26
|
+
import * as GraphBuilder from '../graph-builder';
|
|
27
|
+
import * as Node from '../node';
|
|
37
28
|
import { JsonTree } from './Tree';
|
|
38
29
|
|
|
39
30
|
const DEFAULT_PERIOD = 500;
|
|
@@ -56,48 +47,47 @@ const actionWeights = {
|
|
|
56
47
|
[Action.RENAME_OBJECT]: 4,
|
|
57
48
|
};
|
|
58
49
|
|
|
59
|
-
const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
|
|
60
|
-
const spaceBuilderExtension =
|
|
50
|
+
const createGraph = (client: Client, registry: Registry.Registry): Graph.ExpandableGraph => {
|
|
51
|
+
const spaceBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
61
52
|
id: 'space',
|
|
62
53
|
connector: (node) =>
|
|
63
54
|
Atom.make((get) =>
|
|
64
55
|
Function.pipe(
|
|
65
56
|
get(node),
|
|
66
|
-
Option.flatMap((node) => (node.id ===
|
|
57
|
+
Option.flatMap((node) => (node.id === Node.RootId ? Option.some(node) : Option.none())),
|
|
67
58
|
Option.map(() => {
|
|
68
|
-
const spaces = get(
|
|
59
|
+
const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
|
|
69
60
|
return spaces
|
|
70
|
-
.filter((space) => get(
|
|
71
|
-
.map((space) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
61
|
+
.filter((space: any) => get(CreateAtom.fromObservable(space.state)) === SpaceState.SPACE_READY)
|
|
62
|
+
.map((space) => {
|
|
63
|
+
const propertiesSnapshot = get(AtomObj.make(space.properties));
|
|
64
|
+
return {
|
|
65
|
+
id: space.id,
|
|
66
|
+
type: 'org.dxos.type.space',
|
|
67
|
+
properties: {
|
|
68
|
+
label: propertiesSnapshot.name,
|
|
69
|
+
},
|
|
70
|
+
data: space,
|
|
71
|
+
};
|
|
72
|
+
});
|
|
79
73
|
}),
|
|
80
74
|
Option.getOrElse(() => []),
|
|
81
75
|
),
|
|
82
76
|
),
|
|
83
77
|
});
|
|
84
78
|
|
|
85
|
-
const objectBuilderExtension =
|
|
79
|
+
const objectBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
86
80
|
id: 'object',
|
|
87
81
|
connector: (node) => {
|
|
88
|
-
let query: QueryResult<Live<Expando>> | undefined;
|
|
89
82
|
return Atom.make((get) =>
|
|
90
83
|
Function.pipe(
|
|
91
84
|
get(node),
|
|
92
85
|
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
93
86
|
Option.map((space) => {
|
|
94
|
-
|
|
95
|
-
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
96
|
-
}
|
|
97
|
-
const objects = get(atomFromQuery(query));
|
|
87
|
+
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
98
88
|
return objects.map((object) => ({
|
|
99
89
|
id: object.id,
|
|
100
|
-
type: 'dxos.
|
|
90
|
+
type: 'org.dxos.type.test',
|
|
101
91
|
properties: { label: object.name },
|
|
102
92
|
data: object,
|
|
103
93
|
}));
|
|
@@ -108,15 +98,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
|
|
|
108
98
|
},
|
|
109
99
|
});
|
|
110
100
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
101
|
+
const builder = GraphBuilder.make({ registry });
|
|
102
|
+
GraphBuilder.addExtension(builder, spaceBuilderExtension);
|
|
103
|
+
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
104
|
+
const graph = builder.graph;
|
|
114
105
|
graph.onNodeChanged.on(({ id }) => {
|
|
115
|
-
|
|
106
|
+
Graph.expand(graph, id, 'child');
|
|
116
107
|
});
|
|
117
|
-
|
|
108
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
118
109
|
(window as any).graph = graph;
|
|
119
|
-
|
|
120
110
|
return graph;
|
|
121
111
|
};
|
|
122
112
|
|
|
@@ -136,9 +126,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
136
126
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
137
127
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
138
128
|
const spaceQueries = await Promise.all(
|
|
139
|
-
readySpaces.map((space) => space.db.query(Filter.type(Expando, { type: 'test' })).run()),
|
|
129
|
+
readySpaces.map((space) => space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run()),
|
|
140
130
|
);
|
|
141
|
-
const spaces = readySpaces.filter((space, index) => spaceQueries[index].
|
|
131
|
+
const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
|
|
142
132
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
143
133
|
};
|
|
144
134
|
|
|
@@ -155,16 +145,18 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
155
145
|
case Action.RENAME_SPACE: {
|
|
156
146
|
const space = getRandomSpace(client);
|
|
157
147
|
if (space) {
|
|
158
|
-
space.properties
|
|
148
|
+
Obj.change(space.properties, (obj) => {
|
|
149
|
+
obj.name = random.commerce.productName();
|
|
150
|
+
});
|
|
159
151
|
}
|
|
160
152
|
break;
|
|
161
153
|
}
|
|
162
154
|
|
|
163
155
|
case Action.ADD_OBJECT:
|
|
164
156
|
getRandomSpace(client)?.db.add(
|
|
165
|
-
Obj.make(
|
|
157
|
+
Obj.make(TestSchema.Expando, {
|
|
166
158
|
type: 'test',
|
|
167
|
-
name:
|
|
159
|
+
name: random.commerce.productName(),
|
|
168
160
|
}),
|
|
169
161
|
);
|
|
170
162
|
break;
|
|
@@ -172,7 +164,7 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
172
164
|
case Action.REMOVE_OBJECT: {
|
|
173
165
|
const space = await getSpaceWithObjects(client);
|
|
174
166
|
if (space) {
|
|
175
|
-
const
|
|
167
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
176
168
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
177
169
|
}
|
|
178
170
|
break;
|
|
@@ -181,8 +173,11 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
181
173
|
case Action.RENAME_OBJECT: {
|
|
182
174
|
const space = await getSpaceWithObjects(client);
|
|
183
175
|
if (space) {
|
|
184
|
-
const
|
|
185
|
-
objects[Math.floor(Math.random() * objects.length)]
|
|
176
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
177
|
+
const object = objects[Math.floor(Math.random() * objects.length)];
|
|
178
|
+
Obj.change(object, (obj) => {
|
|
179
|
+
obj.name = random.commerce.productName();
|
|
180
|
+
});
|
|
186
181
|
}
|
|
187
182
|
break;
|
|
188
183
|
}
|
|
@@ -220,14 +215,13 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
220
215
|
<Input.Root>
|
|
221
216
|
<Input.TextInput
|
|
222
217
|
autoComplete='off'
|
|
223
|
-
|
|
224
|
-
classNames='is-[100px] text-right pie-[22px]'
|
|
218
|
+
classNames='w-[100px] text-right pe-[22px]'
|
|
225
219
|
placeholder='Interval'
|
|
226
220
|
value={actionInterval}
|
|
227
221
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
228
222
|
/>
|
|
229
223
|
</Input.Root>
|
|
230
|
-
<Icon icon='ph--timer--regular' classNames={mx('absolute
|
|
224
|
+
<Icon icon='ph--timer--regular' classNames={mx('absolute right-1 top-1 mt-[6px]', getSize(3))} />
|
|
231
225
|
</div>
|
|
232
226
|
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
233
227
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
@@ -256,9 +250,10 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
256
250
|
const meta = {
|
|
257
251
|
title: 'sdk/app-graph/EchoGraph',
|
|
258
252
|
decorators: [
|
|
259
|
-
withTheme,
|
|
253
|
+
withTheme(),
|
|
260
254
|
withClientProvider({
|
|
261
255
|
createIdentity: true,
|
|
256
|
+
types: [TestSchema.Expando],
|
|
262
257
|
onCreateIdentity: async ({ client }) => {
|
|
263
258
|
await client.spaces.create();
|
|
264
259
|
await client.spaces.create();
|
|
@@ -292,94 +287,129 @@ export const TreeView: Story = {
|
|
|
292
287
|
const client = useClient();
|
|
293
288
|
const registry = useContext(RegistryContext);
|
|
294
289
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
(
|
|
299
|
-
|
|
300
|
-
|
|
290
|
+
const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
291
|
+
|
|
292
|
+
const getOrCreateState = useMemo(
|
|
293
|
+
() => (pathKey: string) => {
|
|
294
|
+
let atom = stateRef.current.get(pathKey);
|
|
295
|
+
if (!atom) {
|
|
296
|
+
atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
|
|
297
|
+
stateRef.current.set(pathKey, atom);
|
|
298
|
+
}
|
|
299
|
+
return atom;
|
|
301
300
|
},
|
|
301
|
+
[],
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const childIdsFamily = useMemo(
|
|
305
|
+
() =>
|
|
306
|
+
Atom.family((id: string) =>
|
|
307
|
+
Atom.make((get) => {
|
|
308
|
+
const connections = get(graph.connections(id, 'child'));
|
|
309
|
+
return connections.map((connection) => connection.id);
|
|
310
|
+
}),
|
|
311
|
+
),
|
|
302
312
|
[graph],
|
|
303
313
|
);
|
|
304
314
|
|
|
305
|
-
const
|
|
306
|
-
(
|
|
307
|
-
|
|
308
|
-
.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
})
|
|
314
|
-
.filter(isNonNullable) as Node[];
|
|
315
|
-
const parentOf =
|
|
316
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
317
|
-
return {
|
|
318
|
-
id: node.id,
|
|
319
|
-
label: node.id,
|
|
320
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
321
|
-
parentOf,
|
|
322
|
-
};
|
|
323
|
-
},
|
|
315
|
+
const itemFamily = useMemo(
|
|
316
|
+
() =>
|
|
317
|
+
Atom.family((id: string) =>
|
|
318
|
+
Atom.make((get) => {
|
|
319
|
+
const node = get(graph.node(id));
|
|
320
|
+
return Option.isSome(node) ? node.value : undefined;
|
|
321
|
+
}),
|
|
322
|
+
),
|
|
324
323
|
[graph],
|
|
325
324
|
);
|
|
326
325
|
|
|
327
|
-
const
|
|
328
|
-
(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
326
|
+
const itemPropsFamily = useMemo(
|
|
327
|
+
() =>
|
|
328
|
+
Atom.family((pathKey: string) => {
|
|
329
|
+
const path = pathKey.split('~');
|
|
330
|
+
const id = path[path.length - 1];
|
|
331
|
+
return Atom.make((get) => {
|
|
332
|
+
const nodeOpt = get(graph.node(id));
|
|
333
|
+
const node = Option.isSome(nodeOpt) ? nodeOpt.value : undefined;
|
|
334
|
+
if (!node) {
|
|
335
|
+
return { id, label: id };
|
|
336
|
+
}
|
|
337
|
+
const connections = get(graph.connections(node.id, 'child'));
|
|
338
|
+
const safeChildren = connections.filter((n) => !path.includes(n.id));
|
|
339
|
+
const parentOf =
|
|
340
|
+
safeChildren.length > 0
|
|
341
|
+
? safeChildren.map(({ id }) => id)
|
|
342
|
+
: node.properties.role === 'branch'
|
|
343
|
+
? []
|
|
344
|
+
: undefined;
|
|
345
|
+
return {
|
|
346
|
+
id: node.id,
|
|
347
|
+
label: node.id,
|
|
348
|
+
icon: node.type === 'org.dxos.type.space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
349
|
+
parentOf,
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
}),
|
|
353
|
+
[graph],
|
|
354
|
+
);
|
|
334
355
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
356
|
+
const itemOpenFamily = useMemo(
|
|
357
|
+
() =>
|
|
358
|
+
Atom.family((pathKey: string) => {
|
|
359
|
+
const stateAtom = getOrCreateState(pathKey);
|
|
360
|
+
return Atom.make((get) => get(stateAtom).open);
|
|
361
|
+
}),
|
|
362
|
+
[getOrCreateState],
|
|
338
363
|
);
|
|
339
364
|
|
|
340
|
-
const
|
|
341
|
-
(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
365
|
+
const itemCurrentFamily = useMemo(
|
|
366
|
+
() =>
|
|
367
|
+
Atom.family((pathKey: string) => {
|
|
368
|
+
const stateAtom = getOrCreateState(pathKey);
|
|
369
|
+
return Atom.make((get) => get(stateAtom).current);
|
|
370
|
+
}),
|
|
371
|
+
[getOrCreateState],
|
|
372
|
+
);
|
|
347
373
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
374
|
+
const model: TreeModel<Node.Node> = useMemo(
|
|
375
|
+
() => ({
|
|
376
|
+
childIds: (parentId?: string) => childIdsFamily(parentId ?? Node.RootId),
|
|
377
|
+
item: (id: string) => itemFamily(id),
|
|
378
|
+
itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
|
|
379
|
+
itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
|
|
380
|
+
itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
|
|
381
|
+
}),
|
|
382
|
+
[childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
|
|
351
383
|
);
|
|
352
384
|
|
|
353
385
|
const onOpenChange = useCallback(
|
|
354
386
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
355
387
|
const path = Path.create(..._path);
|
|
356
|
-
const
|
|
357
|
-
|
|
388
|
+
const atom = stateRef.current.get(path);
|
|
389
|
+
if (atom) {
|
|
390
|
+
const prev = registry.get(atom);
|
|
391
|
+
registry.set(atom, { ...prev, open });
|
|
392
|
+
}
|
|
358
393
|
},
|
|
359
|
-
[
|
|
394
|
+
[registry],
|
|
360
395
|
);
|
|
361
396
|
|
|
362
397
|
const onSelect = useCallback(
|
|
363
398
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
364
399
|
const path = Path.create(..._path);
|
|
365
|
-
const
|
|
366
|
-
|
|
400
|
+
const atom = stateRef.current.get(path);
|
|
401
|
+
if (atom) {
|
|
402
|
+
const prev = registry.get(atom);
|
|
403
|
+
registry.set(atom, { ...prev, current });
|
|
404
|
+
}
|
|
367
405
|
},
|
|
368
|
-
[
|
|
406
|
+
[registry],
|
|
369
407
|
);
|
|
370
408
|
|
|
371
409
|
return (
|
|
372
410
|
<>
|
|
373
411
|
<Controls />
|
|
374
|
-
<Tree
|
|
375
|
-
id={ROOT_ID}
|
|
376
|
-
useItems={useItems}
|
|
377
|
-
getProps={getProps}
|
|
378
|
-
isOpen={isOpen}
|
|
379
|
-
isCurrent={isCurrent}
|
|
380
|
-
onOpenChange={onOpenChange}
|
|
381
|
-
onSelect={onSelect}
|
|
382
|
-
/>
|
|
412
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
383
413
|
</>
|
|
384
414
|
);
|
|
385
415
|
},
|
package/src/stories/Tree.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type HTMLAttributes, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { mx } from '@dxos/
|
|
7
|
+
import { mx } from '@dxos/ui-theme';
|
|
8
8
|
|
|
9
9
|
// TODO(burdon): Copied form devtools.
|
|
10
10
|
|
|
@@ -72,7 +72,7 @@ const Scalar: FC<{ value: any }> = ({ value }) => {
|
|
|
72
72
|
|
|
73
73
|
const Box: FC<HTMLAttributes<HTMLDivElement>> = ({ children, className, ...props }) => {
|
|
74
74
|
return (
|
|
75
|
-
<div className={mx('flex
|
|
75
|
+
<div className={mx('flex px-2 border border-l-0 font-mono truncate', className)} {...props}>
|
|
76
76
|
{children}
|
|
77
77
|
</div>
|
|
78
78
|
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Registry } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Option from 'effect/Option';
|
|
7
|
+
|
|
8
|
+
import * as Graph from '../graph';
|
|
9
|
+
import * as GraphBuilder from '../graph-builder';
|
|
10
|
+
import * as Node from '../node';
|
|
11
|
+
|
|
12
|
+
export type SetupGraphBuilderOptions = {
|
|
13
|
+
registry?: Registry.Registry;
|
|
14
|
+
extensions?: GraphBuilder.BuilderExtensions;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const setupGraphBuilder = ({ registry = Registry.make(), extensions }: SetupGraphBuilderOptions = {}) => {
|
|
18
|
+
const builder = GraphBuilder.make({ registry });
|
|
19
|
+
const graph = builder.graph;
|
|
20
|
+
|
|
21
|
+
if (extensions) {
|
|
22
|
+
GraphBuilder.addExtension(builder, extensions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
registry,
|
|
27
|
+
builder,
|
|
28
|
+
graph,
|
|
29
|
+
addExtensions: (nextExtensions: GraphBuilder.BuilderExtensions) => {
|
|
30
|
+
GraphBuilder.addExtension(builder, nextExtensions);
|
|
31
|
+
},
|
|
32
|
+
expand: async (id: string, relation: Node.RelationInput = 'child') => {
|
|
33
|
+
Graph.expand(graph, id, relation);
|
|
34
|
+
await GraphBuilder.flush(builder);
|
|
35
|
+
},
|
|
36
|
+
flush: () => GraphBuilder.flush(builder),
|
|
37
|
+
getConnections: (id: string, relation: Node.RelationInput = 'child') =>
|
|
38
|
+
registry.get(graph.connections(id, relation)),
|
|
39
|
+
getNode: (id: string) => Graph.getNode(graph, id).pipe(Option.getOrNull),
|
|
40
|
+
};
|
|
41
|
+
};
|