@dxos/app-graph 0.8.4-main.b97322e → 0.8.4-main.bcb3aa67d6
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 +27 -767
- 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 +27 -768
- 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 +117 -60
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +188 -218
- 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 +8 -10
- 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 +48 -36
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1067 -126
- package/src/graph-builder.ts +734 -264
- package/src/graph.test.ts +451 -123
- package/src/graph.ts +1054 -407
- 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 +173 -133
- package/src/stories/Tree.tsx +1 -1
- 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 -218
- 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
|
+
});
|
|
@@ -2,39 +2,31 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Query,
|
|
16
|
-
type QueryResult,
|
|
17
|
-
type Space,
|
|
18
|
-
SpaceState,
|
|
19
|
-
Expando,
|
|
20
|
-
type Live,
|
|
21
|
-
Filter,
|
|
22
|
-
} from '@dxos/client/echo';
|
|
23
|
-
import { Obj, Type } from '@dxos/echo';
|
|
5
|
+
import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
|
+
import * as Function from 'effect/Function';
|
|
8
|
+
import * as Option from 'effect/Option';
|
|
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 { TestSchema } from '@dxos/echo/testing';
|
|
14
|
+
import { AtomObj, AtomQuery } from '@dxos/echo-atom';
|
|
24
15
|
import { faker } from '@dxos/random';
|
|
25
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
26
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
18
|
+
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
19
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
20
|
+
import { Path, Tree, type TreeModel } from '@dxos/react-ui-list';
|
|
21
|
+
import { getSize, mx } from '@dxos/ui-theme';
|
|
22
|
+
import { safeParseInt } from '@dxos/util';
|
|
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';
|
|
32
28
|
|
|
33
29
|
import { JsonTree } from './Tree';
|
|
34
|
-
import { type ExpandableGraph, ROOT_ID } from '../graph';
|
|
35
|
-
import { GraphBuilder, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
|
|
36
|
-
import { type Node } from '../node';
|
|
37
|
-
import { rxFromQuery } from '../testing';
|
|
38
30
|
|
|
39
31
|
const DEFAULT_PERIOD = 500;
|
|
40
32
|
|
|
@@ -56,46 +48,47 @@ const actionWeights = {
|
|
|
56
48
|
[Action.RENAME_OBJECT]: 4,
|
|
57
49
|
};
|
|
58
50
|
|
|
59
|
-
const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
|
|
60
|
-
const spaceBuilderExtension =
|
|
51
|
+
const createGraph = (client: Client, registry: Registry.Registry): Graph.ExpandableGraph => {
|
|
52
|
+
const spaceBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
61
53
|
id: 'space',
|
|
62
54
|
connector: (node) =>
|
|
63
|
-
|
|
64
|
-
pipe(
|
|
55
|
+
Atom.make((get) =>
|
|
56
|
+
Function.pipe(
|
|
65
57
|
get(node),
|
|
66
|
-
Option.flatMap((node) => (node.id ===
|
|
58
|
+
Option.flatMap((node) => (node.id === Node.RootId ? Option.some(node) : Option.none())),
|
|
67
59
|
Option.map(() => {
|
|
68
|
-
const spaces = get(
|
|
60
|
+
const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
|
|
69
61
|
return spaces
|
|
70
|
-
.filter((space) => get(
|
|
71
|
-
.map((space) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
.filter((space: any) => get(CreateAtom.fromObservable(space.state)) === SpaceState.SPACE_READY)
|
|
63
|
+
.map((space) => {
|
|
64
|
+
const propertiesSnapshot = get(AtomObj.make(space.properties));
|
|
65
|
+
return {
|
|
66
|
+
id: space.id,
|
|
67
|
+
type: 'org.dxos.type.space',
|
|
68
|
+
properties: {
|
|
69
|
+
label: propertiesSnapshot.name,
|
|
70
|
+
},
|
|
71
|
+
data: space,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
77
74
|
}),
|
|
78
75
|
Option.getOrElse(() => []),
|
|
79
76
|
),
|
|
80
77
|
),
|
|
81
78
|
});
|
|
82
79
|
|
|
83
|
-
const objectBuilderExtension =
|
|
80
|
+
const objectBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
84
81
|
id: 'object',
|
|
85
82
|
connector: (node) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
pipe(
|
|
83
|
+
return Atom.make((get) =>
|
|
84
|
+
Function.pipe(
|
|
89
85
|
get(node),
|
|
90
86
|
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
91
87
|
Option.map((space) => {
|
|
92
|
-
|
|
93
|
-
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
94
|
-
}
|
|
95
|
-
const objects = get(rxFromQuery(query));
|
|
88
|
+
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
96
89
|
return objects.map((object) => ({
|
|
97
90
|
id: object.id,
|
|
98
|
-
type: 'dxos.
|
|
91
|
+
type: 'org.dxos.type.test',
|
|
99
92
|
properties: { label: object.name },
|
|
100
93
|
data: object,
|
|
101
94
|
}));
|
|
@@ -106,15 +99,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
|
|
|
106
99
|
},
|
|
107
100
|
});
|
|
108
101
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
102
|
+
const builder = GraphBuilder.make({ registry });
|
|
103
|
+
GraphBuilder.addExtension(builder, spaceBuilderExtension);
|
|
104
|
+
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
105
|
+
const graph = builder.graph;
|
|
112
106
|
graph.onNodeChanged.on(({ id }) => {
|
|
113
|
-
|
|
107
|
+
Graph.expand(graph, id, 'child');
|
|
114
108
|
});
|
|
115
|
-
|
|
109
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
116
110
|
(window as any).graph = graph;
|
|
117
|
-
|
|
118
111
|
return graph;
|
|
119
112
|
};
|
|
120
113
|
|
|
@@ -134,9 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
134
127
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
135
128
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
136
129
|
const spaceQueries = await Promise.all(
|
|
137
|
-
readySpaces.map((space) => space.db.query(Filter.type(Expando, { type: 'test' })).run()),
|
|
130
|
+
readySpaces.map((space) => space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run()),
|
|
138
131
|
);
|
|
139
|
-
const spaces = readySpaces.filter((space, index) => spaceQueries[index].
|
|
132
|
+
const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
|
|
140
133
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
141
134
|
};
|
|
142
135
|
|
|
@@ -153,19 +146,26 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
153
146
|
case Action.RENAME_SPACE: {
|
|
154
147
|
const space = getRandomSpace(client);
|
|
155
148
|
if (space) {
|
|
156
|
-
space.properties
|
|
149
|
+
Obj.change(space.properties, (obj) => {
|
|
150
|
+
obj.name = faker.commerce.productName();
|
|
151
|
+
});
|
|
157
152
|
}
|
|
158
153
|
break;
|
|
159
154
|
}
|
|
160
155
|
|
|
161
156
|
case Action.ADD_OBJECT:
|
|
162
|
-
getRandomSpace(client)?.db.add(
|
|
157
|
+
getRandomSpace(client)?.db.add(
|
|
158
|
+
Obj.make(TestSchema.Expando, {
|
|
159
|
+
type: 'test',
|
|
160
|
+
name: faker.commerce.productName(),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
163
|
break;
|
|
164
164
|
|
|
165
165
|
case Action.REMOVE_OBJECT: {
|
|
166
166
|
const space = await getSpaceWithObjects(client);
|
|
167
167
|
if (space) {
|
|
168
|
-
const
|
|
168
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
169
169
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
170
170
|
}
|
|
171
171
|
break;
|
|
@@ -174,8 +174,11 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
174
174
|
case Action.RENAME_OBJECT: {
|
|
175
175
|
const space = await getSpaceWithObjects(client);
|
|
176
176
|
if (space) {
|
|
177
|
-
const
|
|
178
|
-
objects[Math.floor(Math.random() * objects.length)]
|
|
177
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
178
|
+
const object = objects[Math.floor(Math.random() * objects.length)];
|
|
179
|
+
Obj.change(object, (obj) => {
|
|
180
|
+
obj.name = faker.commerce.productName();
|
|
181
|
+
});
|
|
179
182
|
}
|
|
180
183
|
break;
|
|
181
184
|
}
|
|
@@ -213,14 +216,13 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
213
216
|
<Input.Root>
|
|
214
217
|
<Input.TextInput
|
|
215
218
|
autoComplete='off'
|
|
216
|
-
|
|
217
|
-
classNames='w-[100px] text-right pie-[22px]'
|
|
219
|
+
classNames='w-[100px] text-right pe-[22px]'
|
|
218
220
|
placeholder='Interval'
|
|
219
221
|
value={actionInterval}
|
|
220
222
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
221
223
|
/>
|
|
222
224
|
</Input.Root>
|
|
223
|
-
<Icon icon='ph--timer--regular' classNames={mx('absolute
|
|
225
|
+
<Icon icon='ph--timer--regular' classNames={mx('absolute right-1 top-1 mt-[6px]', getSize(3))} />
|
|
224
226
|
</div>
|
|
225
227
|
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
226
228
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
@@ -246,28 +248,31 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
246
248
|
);
|
|
247
249
|
};
|
|
248
250
|
|
|
249
|
-
const meta
|
|
251
|
+
const meta = {
|
|
250
252
|
title: 'sdk/app-graph/EchoGraph',
|
|
251
253
|
decorators: [
|
|
254
|
+
withTheme(),
|
|
252
255
|
withClientProvider({
|
|
253
256
|
createIdentity: true,
|
|
254
|
-
|
|
257
|
+
types: [TestSchema.Expando],
|
|
258
|
+
onCreateIdentity: async ({ client }) => {
|
|
255
259
|
await client.spaces.create();
|
|
256
260
|
await client.spaces.create();
|
|
257
261
|
},
|
|
258
262
|
}),
|
|
259
|
-
withTheme,
|
|
260
263
|
],
|
|
261
|
-
};
|
|
264
|
+
} satisfies Meta;
|
|
262
265
|
|
|
263
266
|
export default meta;
|
|
264
267
|
|
|
265
|
-
|
|
268
|
+
type Story = StoryObj<typeof meta>;
|
|
269
|
+
|
|
270
|
+
export const JsonView: Story = {
|
|
266
271
|
render: () => {
|
|
267
272
|
const client = useClient();
|
|
268
273
|
const registry = useContext(RegistryContext);
|
|
269
274
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
270
|
-
const data =
|
|
275
|
+
const data = useAtomValue(graph.json());
|
|
271
276
|
|
|
272
277
|
return (
|
|
273
278
|
<>
|
|
@@ -278,99 +283,134 @@ export const JsonView = {
|
|
|
278
283
|
},
|
|
279
284
|
};
|
|
280
285
|
|
|
281
|
-
export const TreeView = {
|
|
286
|
+
export const TreeView: Story = {
|
|
282
287
|
render: () => {
|
|
283
288
|
const client = useClient();
|
|
284
289
|
const registry = useContext(RegistryContext);
|
|
285
290
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
(
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
292
|
+
|
|
293
|
+
const getOrCreateState = useMemo(
|
|
294
|
+
() => (pathKey: string) => {
|
|
295
|
+
let atom = stateRef.current.get(pathKey);
|
|
296
|
+
if (!atom) {
|
|
297
|
+
atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
|
|
298
|
+
stateRef.current.set(pathKey, atom);
|
|
299
|
+
}
|
|
300
|
+
return atom;
|
|
292
301
|
},
|
|
302
|
+
[],
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const childIdsFamily = useMemo(
|
|
306
|
+
() =>
|
|
307
|
+
Atom.family((id: string) =>
|
|
308
|
+
Atom.make((get) => {
|
|
309
|
+
const connections = get(graph.connections(id, 'child'));
|
|
310
|
+
return connections.map((connection) => connection.id);
|
|
311
|
+
}),
|
|
312
|
+
),
|
|
293
313
|
[graph],
|
|
294
314
|
);
|
|
295
315
|
|
|
296
|
-
const
|
|
297
|
-
(
|
|
298
|
-
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
})
|
|
305
|
-
.filter(isNonNullable) as Node[];
|
|
306
|
-
const parentOf =
|
|
307
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
308
|
-
return {
|
|
309
|
-
id: node.id,
|
|
310
|
-
label: node.id,
|
|
311
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
312
|
-
parentOf,
|
|
313
|
-
};
|
|
314
|
-
},
|
|
316
|
+
const itemFamily = useMemo(
|
|
317
|
+
() =>
|
|
318
|
+
Atom.family((id: string) =>
|
|
319
|
+
Atom.make((get) => {
|
|
320
|
+
const node = get(graph.node(id));
|
|
321
|
+
return Option.isSome(node) ? node.value : undefined;
|
|
322
|
+
}),
|
|
323
|
+
),
|
|
315
324
|
[graph],
|
|
316
325
|
);
|
|
317
326
|
|
|
318
|
-
const
|
|
319
|
-
(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
327
|
+
const itemPropsFamily = useMemo(
|
|
328
|
+
() =>
|
|
329
|
+
Atom.family((pathKey: string) => {
|
|
330
|
+
const path = pathKey.split('~');
|
|
331
|
+
const id = path[path.length - 1];
|
|
332
|
+
return Atom.make((get) => {
|
|
333
|
+
const nodeOpt = get(graph.node(id));
|
|
334
|
+
const node = Option.isSome(nodeOpt) ? nodeOpt.value : undefined;
|
|
335
|
+
if (!node) {
|
|
336
|
+
return { id, label: id };
|
|
337
|
+
}
|
|
338
|
+
const connections = get(graph.connections(node.id, 'child'));
|
|
339
|
+
const safeChildren = connections.filter((n) => !path.includes(n.id));
|
|
340
|
+
const parentOf =
|
|
341
|
+
safeChildren.length > 0
|
|
342
|
+
? safeChildren.map(({ id }) => id)
|
|
343
|
+
: node.properties.role === 'branch'
|
|
344
|
+
? []
|
|
345
|
+
: undefined;
|
|
346
|
+
return {
|
|
347
|
+
id: node.id,
|
|
348
|
+
label: node.id,
|
|
349
|
+
icon: node.type === 'org.dxos.type.space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
350
|
+
parentOf,
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}),
|
|
354
|
+
[graph],
|
|
355
|
+
);
|
|
325
356
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
357
|
+
const itemOpenFamily = useMemo(
|
|
358
|
+
() =>
|
|
359
|
+
Atom.family((pathKey: string) => {
|
|
360
|
+
const stateAtom = getOrCreateState(pathKey);
|
|
361
|
+
return Atom.make((get) => get(stateAtom).open);
|
|
362
|
+
}),
|
|
363
|
+
[getOrCreateState],
|
|
329
364
|
);
|
|
330
365
|
|
|
331
|
-
const
|
|
332
|
-
(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
366
|
+
const itemCurrentFamily = useMemo(
|
|
367
|
+
() =>
|
|
368
|
+
Atom.family((pathKey: string) => {
|
|
369
|
+
const stateAtom = getOrCreateState(pathKey);
|
|
370
|
+
return Atom.make((get) => get(stateAtom).current);
|
|
371
|
+
}),
|
|
372
|
+
[getOrCreateState],
|
|
373
|
+
);
|
|
338
374
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
375
|
+
const model: TreeModel<Node.Node> = useMemo(
|
|
376
|
+
() => ({
|
|
377
|
+
childIds: (parentId?: string) => childIdsFamily(parentId ?? Node.RootId),
|
|
378
|
+
item: (id: string) => itemFamily(id),
|
|
379
|
+
itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
|
|
380
|
+
itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
|
|
381
|
+
itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
|
|
382
|
+
}),
|
|
383
|
+
[childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
|
|
342
384
|
);
|
|
343
385
|
|
|
344
386
|
const onOpenChange = useCallback(
|
|
345
387
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
346
388
|
const path = Path.create(..._path);
|
|
347
|
-
const
|
|
348
|
-
|
|
389
|
+
const atom = stateRef.current.get(path);
|
|
390
|
+
if (atom) {
|
|
391
|
+
const prev = registry.get(atom);
|
|
392
|
+
registry.set(atom, { ...prev, open });
|
|
393
|
+
}
|
|
349
394
|
},
|
|
350
|
-
[
|
|
395
|
+
[registry],
|
|
351
396
|
);
|
|
352
397
|
|
|
353
398
|
const onSelect = useCallback(
|
|
354
399
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
355
400
|
const path = Path.create(..._path);
|
|
356
|
-
const
|
|
357
|
-
|
|
401
|
+
const atom = stateRef.current.get(path);
|
|
402
|
+
if (atom) {
|
|
403
|
+
const prev = registry.get(atom);
|
|
404
|
+
registry.set(atom, { ...prev, current });
|
|
405
|
+
}
|
|
358
406
|
},
|
|
359
|
-
[
|
|
407
|
+
[registry],
|
|
360
408
|
);
|
|
361
409
|
|
|
362
410
|
return (
|
|
363
411
|
<>
|
|
364
412
|
<Controls />
|
|
365
|
-
<Tree
|
|
366
|
-
id={ROOT_ID}
|
|
367
|
-
useItems={useItems}
|
|
368
|
-
getProps={getProps}
|
|
369
|
-
isOpen={isOpen}
|
|
370
|
-
isCurrent={isCurrent}
|
|
371
|
-
onOpenChange={onOpenChange}
|
|
372
|
-
onSelect={onSelect}
|
|
373
|
-
/>
|
|
413
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
374
414
|
</>
|
|
375
415
|
);
|
|
376
416
|
},
|
package/src/stories/Tree.tsx
CHANGED
|
@@ -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
|
+
};
|