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