@dxos/app-graph 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/chunk-WJJ5KEOH.mjs +1477 -0
- package/dist/lib/neutral/chunk-WJJ5KEOH.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +40 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/neutral/scheduler.mjs +15 -0
- package/dist/lib/neutral/scheduler.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +40 -0
- package/dist/lib/neutral/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 +112 -66
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +187 -221
- 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/scheduler.browser.d.ts +2 -0
- package/dist/types/src/scheduler.browser.d.ts.map +1 -0
- package/dist/types/src/scheduler.d.ts +8 -0
- package/dist/types/src/scheduler.d.ts.map +1 -0
- 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 +40 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +53 -40
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1146 -137
- package/src/graph-builder.ts +732 -290
- package/src/graph.test.ts +450 -122
- package/src/graph.ts +1050 -401
- 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/scheduler.browser.ts +5 -0
- package/src/scheduler.ts +17 -0
- package/src/stories/EchoGraph.stories.tsx +160 -123
- 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 +101 -0
- package/dist/lib/browser/index.mjs +0 -836
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -838
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- 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
|
+
});
|
package/src/scheduler.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
// CF Workers / Node fallback: there is no main thread to yield to.
|
|
6
|
+
// scheduleTask runs the callback on the next microtask; yieldOrContinue is a microtask hop.
|
|
7
|
+
|
|
8
|
+
type ScheduleOptions = { strategy?: 'smooth' | 'interactive' | 'idle'; signal?: AbortSignal };
|
|
9
|
+
|
|
10
|
+
export const scheduleTask = async <T>(callback: () => T | Promise<T>, _options?: ScheduleOptions): Promise<T> => {
|
|
11
|
+
await Promise.resolve();
|
|
12
|
+
return callback();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const yieldOrContinue = async (_priority: 'smooth' | 'interactive' | 'idle'): Promise<void> => {
|
|
16
|
+
await Promise.resolve();
|
|
17
|
+
};
|
|
@@ -2,38 +2,29 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Registry, RegistryContext,
|
|
5
|
+
import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
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 { type Space, SpaceState, isSpace } from '@dxos/client/echo';
|
|
12
|
+
import { Filter, 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, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
|
|
34
|
-
import { type Node } from '../node';
|
|
35
|
-
import { rxFromQuery } 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,46 +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
|
-
|
|
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
|
+
});
|
|
77
73
|
}),
|
|
78
74
|
Option.getOrElse(() => []),
|
|
79
75
|
),
|
|
80
76
|
),
|
|
81
77
|
});
|
|
82
78
|
|
|
83
|
-
const objectBuilderExtension =
|
|
79
|
+
const objectBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
84
80
|
id: 'object',
|
|
85
81
|
connector: (node) => {
|
|
86
|
-
|
|
87
|
-
return Rx.make((get) =>
|
|
82
|
+
return Atom.make((get) =>
|
|
88
83
|
Function.pipe(
|
|
89
84
|
get(node),
|
|
90
85
|
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
91
86
|
Option.map((space) => {
|
|
92
|
-
|
|
93
|
-
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
94
|
-
}
|
|
95
|
-
const objects = get(rxFromQuery(query));
|
|
87
|
+
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
96
88
|
return objects.map((object) => ({
|
|
97
89
|
id: object.id,
|
|
98
|
-
type: 'dxos.
|
|
90
|
+
type: 'org.dxos.type.test',
|
|
99
91
|
properties: { label: object.name },
|
|
100
92
|
data: object,
|
|
101
93
|
}));
|
|
@@ -106,15 +98,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
|
|
|
106
98
|
},
|
|
107
99
|
});
|
|
108
100
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
const builder = GraphBuilder.make({ registry });
|
|
102
|
+
GraphBuilder.addExtension(builder, spaceBuilderExtension);
|
|
103
|
+
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
104
|
+
const graph = builder.graph;
|
|
112
105
|
graph.onNodeChanged.on(({ id }) => {
|
|
113
|
-
|
|
106
|
+
Graph.expand(graph, id, 'child');
|
|
114
107
|
});
|
|
115
|
-
|
|
108
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
116
109
|
(window as any).graph = graph;
|
|
117
|
-
|
|
118
110
|
return graph;
|
|
119
111
|
};
|
|
120
112
|
|
|
@@ -134,9 +126,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
134
126
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
135
127
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
136
128
|
const spaceQueries = await Promise.all(
|
|
137
|
-
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()),
|
|
138
130
|
);
|
|
139
|
-
const spaces = readySpaces.filter((space, index) => spaceQueries[index].
|
|
131
|
+
const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
|
|
140
132
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
141
133
|
};
|
|
142
134
|
|
|
@@ -153,19 +145,26 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
153
145
|
case Action.RENAME_SPACE: {
|
|
154
146
|
const space = getRandomSpace(client);
|
|
155
147
|
if (space) {
|
|
156
|
-
space.properties
|
|
148
|
+
Obj.update(space.properties, (obj) => {
|
|
149
|
+
obj.name = random.commerce.productName();
|
|
150
|
+
});
|
|
157
151
|
}
|
|
158
152
|
break;
|
|
159
153
|
}
|
|
160
154
|
|
|
161
155
|
case Action.ADD_OBJECT:
|
|
162
|
-
getRandomSpace(client)?.db.add(
|
|
156
|
+
getRandomSpace(client)?.db.add(
|
|
157
|
+
Obj.make(TestSchema.Expando, {
|
|
158
|
+
type: 'test',
|
|
159
|
+
name: random.commerce.productName(),
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
163
162
|
break;
|
|
164
163
|
|
|
165
164
|
case Action.REMOVE_OBJECT: {
|
|
166
165
|
const space = await getSpaceWithObjects(client);
|
|
167
166
|
if (space) {
|
|
168
|
-
const
|
|
167
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
169
168
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
170
169
|
}
|
|
171
170
|
break;
|
|
@@ -174,8 +173,11 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
174
173
|
case Action.RENAME_OBJECT: {
|
|
175
174
|
const space = await getSpaceWithObjects(client);
|
|
176
175
|
if (space) {
|
|
177
|
-
const
|
|
178
|
-
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.update(object, (object) => {
|
|
179
|
+
object.name = random.commerce.productName();
|
|
180
|
+
});
|
|
179
181
|
}
|
|
180
182
|
break;
|
|
181
183
|
}
|
|
@@ -213,14 +215,13 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
213
215
|
<Input.Root>
|
|
214
216
|
<Input.TextInput
|
|
215
217
|
autoComplete='off'
|
|
216
|
-
|
|
217
|
-
classNames='w-[100px] text-right pie-[22px]'
|
|
218
|
+
classNames='w-[100px] text-right pe-[22px]'
|
|
218
219
|
placeholder='Interval'
|
|
219
220
|
value={actionInterval}
|
|
220
221
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
221
222
|
/>
|
|
222
223
|
</Input.Root>
|
|
223
|
-
<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))} />
|
|
224
225
|
</div>
|
|
225
226
|
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
226
227
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
@@ -249,16 +250,17 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
249
250
|
const meta = {
|
|
250
251
|
title: 'sdk/app-graph/EchoGraph',
|
|
251
252
|
decorators: [
|
|
252
|
-
withTheme,
|
|
253
|
+
withTheme(),
|
|
253
254
|
withClientProvider({
|
|
254
255
|
createIdentity: true,
|
|
256
|
+
types: [TestSchema.Expando],
|
|
255
257
|
onCreateIdentity: async ({ client }) => {
|
|
256
258
|
await client.spaces.create();
|
|
257
259
|
await client.spaces.create();
|
|
258
260
|
},
|
|
259
261
|
}),
|
|
260
262
|
],
|
|
261
|
-
} satisfies Meta
|
|
263
|
+
} satisfies Meta;
|
|
262
264
|
|
|
263
265
|
export default meta;
|
|
264
266
|
|
|
@@ -269,7 +271,7 @@ export const JsonView: Story = {
|
|
|
269
271
|
const client = useClient();
|
|
270
272
|
const registry = useContext(RegistryContext);
|
|
271
273
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
272
|
-
const data =
|
|
274
|
+
const data = useAtomValue(graph.json());
|
|
273
275
|
|
|
274
276
|
return (
|
|
275
277
|
<>
|
|
@@ -285,94 +287,129 @@ export const TreeView: Story = {
|
|
|
285
287
|
const client = useClient();
|
|
286
288
|
const registry = useContext(RegistryContext);
|
|
287
289
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
(
|
|
292
|
-
|
|
293
|
-
|
|
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;
|
|
294
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
|
+
),
|
|
295
312
|
[graph],
|
|
296
313
|
);
|
|
297
314
|
|
|
298
|
-
const
|
|
299
|
-
(
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})
|
|
307
|
-
.filter(isNonNullable) as Node[];
|
|
308
|
-
const parentOf =
|
|
309
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
310
|
-
return {
|
|
311
|
-
id: node.id,
|
|
312
|
-
label: node.id,
|
|
313
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
314
|
-
parentOf,
|
|
315
|
-
};
|
|
316
|
-
},
|
|
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
|
+
),
|
|
317
323
|
[graph],
|
|
318
324
|
);
|
|
319
325
|
|
|
320
|
-
const
|
|
321
|
-
(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
);
|
|
327
355
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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],
|
|
331
363
|
);
|
|
332
364
|
|
|
333
|
-
const
|
|
334
|
-
(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
);
|
|
340
373
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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],
|
|
344
383
|
);
|
|
345
384
|
|
|
346
385
|
const onOpenChange = useCallback(
|
|
347
386
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
348
387
|
const path = Path.create(..._path);
|
|
349
|
-
const
|
|
350
|
-
|
|
388
|
+
const atom = stateRef.current.get(path);
|
|
389
|
+
if (atom) {
|
|
390
|
+
const prev = registry.get(atom);
|
|
391
|
+
registry.set(atom, { ...prev, open });
|
|
392
|
+
}
|
|
351
393
|
},
|
|
352
|
-
[
|
|
394
|
+
[registry],
|
|
353
395
|
);
|
|
354
396
|
|
|
355
397
|
const onSelect = useCallback(
|
|
356
398
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
357
399
|
const path = Path.create(..._path);
|
|
358
|
-
const
|
|
359
|
-
|
|
400
|
+
const atom = stateRef.current.get(path);
|
|
401
|
+
if (atom) {
|
|
402
|
+
const prev = registry.get(atom);
|
|
403
|
+
registry.set(atom, { ...prev, current });
|
|
404
|
+
}
|
|
360
405
|
},
|
|
361
|
-
[
|
|
406
|
+
[registry],
|
|
362
407
|
);
|
|
363
408
|
|
|
364
409
|
return (
|
|
365
410
|
<>
|
|
366
411
|
<Controls />
|
|
367
|
-
<Tree
|
|
368
|
-
id={ROOT_ID}
|
|
369
|
-
useItems={useItems}
|
|
370
|
-
getProps={getProps}
|
|
371
|
-
isOpen={isOpen}
|
|
372
|
-
isCurrent={isCurrent}
|
|
373
|
-
onOpenChange={onOpenChange}
|
|
374
|
-
onSelect={onSelect}
|
|
375
|
-
/>
|
|
412
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
376
413
|
</>
|
|
377
414
|
);
|
|
378
415
|
},
|
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
|
+
};
|