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