@dxos/app-graph 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced
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-32XXJE6M.mjs +1477 -0
- package/dist/lib/neutral/chunk-32XXJE6M.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 +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/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 +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 +40 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +54 -43
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1193 -126
- package/src/graph-builder.ts +754 -265
- package/src/graph.test.ts +451 -123
- package/src/graph.ts +1057 -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 +83 -7
- package/src/scheduler.browser.ts +5 -0
- package/src/scheduler.ts +17 -0
- package/src/stories/EchoGraph.stories.tsx +180 -140
- 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 -778
- 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 -780
- 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,7 +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';
|
|
7
|
+
|
|
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';
|
|
6
29
|
|
|
7
30
|
/**
|
|
8
31
|
* Represents a node in the graph.
|
|
@@ -46,7 +69,19 @@ export type NodeFilter<TData = any, TProperties extends Record<string, any> = Re
|
|
|
46
69
|
connectedNode: Node,
|
|
47
70
|
) => node is Node<TData, TProperties>;
|
|
48
71
|
|
|
49
|
-
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);
|
|
50
85
|
|
|
51
86
|
export const isGraphNode = (data: unknown): data is Node =>
|
|
52
87
|
data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
|
|
@@ -61,30 +96,45 @@ export type NodeArg<TData, TProperties extends Record<string, any> = Record<stri
|
|
|
61
96
|
nodes?: NodeArg<unknown>[];
|
|
62
97
|
|
|
63
98
|
/** Will automatically add specified edges. */
|
|
64
|
-
edges?: [string,
|
|
99
|
+
edges?: [string, RelationInput][];
|
|
65
100
|
};
|
|
66
101
|
|
|
67
102
|
//
|
|
68
103
|
// Actions
|
|
69
104
|
//
|
|
70
105
|
|
|
71
|
-
export type
|
|
106
|
+
export type InvokeProps = {
|
|
72
107
|
/** Node the invoked action is connected to. */
|
|
73
108
|
parent?: Node;
|
|
74
109
|
|
|
110
|
+
/** Path from root to the node in the current tree context. */
|
|
111
|
+
path?: string[];
|
|
112
|
+
|
|
75
113
|
caller?: string;
|
|
76
114
|
};
|
|
77
115
|
|
|
78
|
-
|
|
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>;
|
|
79
127
|
|
|
80
128
|
export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<
|
|
81
129
|
Omit<Node<ActionData, TProperties>, 'properties'> & {
|
|
82
130
|
properties: Readonly<TProperties>;
|
|
131
|
+
/** Captured context from extension creation. Provided automatically at action execution. */
|
|
132
|
+
_actionContext?: ActionContext;
|
|
83
133
|
}
|
|
84
134
|
>;
|
|
85
135
|
|
|
86
136
|
export const isAction = (data: unknown): data is Action =>
|
|
87
|
-
isGraphNode(data) ? typeof data.data === 'function' : false;
|
|
137
|
+
isGraphNode(data) ? typeof data.data === 'function' && data.type === ActionType : false;
|
|
88
138
|
|
|
89
139
|
export const actionGroupSymbol = Symbol('ActionGroup');
|
|
90
140
|
|
|
@@ -95,8 +145,34 @@ export type ActionGroup<TProperties extends Record<string, any> = Record<string,
|
|
|
95
145
|
>;
|
|
96
146
|
|
|
97
147
|
export const isActionGroup = (data: unknown): data is ActionGroup =>
|
|
98
|
-
isGraphNode(data) ? data.data === actionGroupSymbol : false;
|
|
148
|
+
isGraphNode(data) ? data.data === actionGroupSymbol && data.type === ActionGroupType : false;
|
|
99
149
|
|
|
100
150
|
export type ActionLike = Action | ActionGroup;
|
|
101
151
|
|
|
102
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,40 +2,30 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import { type
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Query,
|
|
17
|
-
type QueryResult,
|
|
18
|
-
type Space,
|
|
19
|
-
SpaceState,
|
|
20
|
-
Expando,
|
|
21
|
-
type Live,
|
|
22
|
-
Filter,
|
|
23
|
-
} from '@dxos/client/echo';
|
|
24
|
-
import { Obj, Type } from '@dxos/echo';
|
|
25
|
-
import { faker } from '@dxos/random';
|
|
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 { 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';
|
|
26
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
27
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
28
|
-
import {
|
|
29
|
-
import { Path, Tree } from '@dxos/react-ui-list';
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
18
|
+
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
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';
|
|
34
28
|
import { JsonTree } from './Tree';
|
|
35
|
-
import { type ExpandableGraph, ROOT_ID } from '../graph';
|
|
36
|
-
import { GraphBuilder, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
|
|
37
|
-
import { type Node } from '../node';
|
|
38
|
-
import { rxFromQuery } from '../testing';
|
|
39
29
|
|
|
40
30
|
const DEFAULT_PERIOD = 500;
|
|
41
31
|
|
|
@@ -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
|
}
|
|
@@ -205,23 +206,24 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
205
206
|
return (
|
|
206
207
|
<>
|
|
207
208
|
<div className='flex shrink-0 p-2 space-x-2'>
|
|
208
|
-
<
|
|
209
|
+
<IconButton
|
|
210
|
+
icon={generating ? 'ph--pause--regular' : 'ph--play--regular'}
|
|
211
|
+
label={generating ? 'Pause' : 'Play'}
|
|
212
|
+
onClick={() => setGenerating((generating) => !generating)}
|
|
213
|
+
/>
|
|
209
214
|
<div className='relative' title='mutation period'>
|
|
210
215
|
<Input.Root>
|
|
211
216
|
<Input.TextInput
|
|
212
217
|
autoComplete='off'
|
|
213
|
-
|
|
214
|
-
classNames='w-[100px] text-right pie-[22px]'
|
|
218
|
+
classNames='w-[100px] text-right pe-[22px]'
|
|
215
219
|
placeholder='Interval'
|
|
216
220
|
value={actionInterval}
|
|
217
221
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
218
222
|
/>
|
|
219
223
|
</Input.Root>
|
|
220
|
-
<
|
|
224
|
+
<Icon icon='ph--timer--regular' classNames={mx('absolute right-1 top-1 mt-[6px]', getSize(3))} />
|
|
221
225
|
</div>
|
|
222
|
-
<
|
|
223
|
-
<Plus />
|
|
224
|
-
</Button>
|
|
226
|
+
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
225
227
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
226
228
|
<Select.TriggerButton placeholder='Select value' />
|
|
227
229
|
<Select.Portal>
|
|
@@ -245,28 +247,31 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
245
247
|
);
|
|
246
248
|
};
|
|
247
249
|
|
|
248
|
-
const meta
|
|
250
|
+
const meta = {
|
|
249
251
|
title: 'sdk/app-graph/EchoGraph',
|
|
250
252
|
decorators: [
|
|
253
|
+
withTheme(),
|
|
251
254
|
withClientProvider({
|
|
252
255
|
createIdentity: true,
|
|
253
|
-
|
|
256
|
+
types: [TestSchema.Expando],
|
|
257
|
+
onCreateIdentity: async ({ client }) => {
|
|
254
258
|
await client.spaces.create();
|
|
255
259
|
await client.spaces.create();
|
|
256
260
|
},
|
|
257
261
|
}),
|
|
258
|
-
withTheme,
|
|
259
262
|
],
|
|
260
|
-
};
|
|
263
|
+
} satisfies Meta;
|
|
261
264
|
|
|
262
265
|
export default meta;
|
|
263
266
|
|
|
264
|
-
|
|
267
|
+
type Story = StoryObj<typeof meta>;
|
|
268
|
+
|
|
269
|
+
export const JsonView: Story = {
|
|
265
270
|
render: () => {
|
|
266
271
|
const client = useClient();
|
|
267
272
|
const registry = useContext(RegistryContext);
|
|
268
273
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
269
|
-
const data =
|
|
274
|
+
const data = useAtomValue(graph.json());
|
|
270
275
|
|
|
271
276
|
return (
|
|
272
277
|
<>
|
|
@@ -277,99 +282,134 @@ export const JsonView = {
|
|
|
277
282
|
},
|
|
278
283
|
};
|
|
279
284
|
|
|
280
|
-
export const TreeView = {
|
|
285
|
+
export const TreeView: Story = {
|
|
281
286
|
render: () => {
|
|
282
287
|
const client = useClient();
|
|
283
288
|
const registry = useContext(RegistryContext);
|
|
284
289
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
(
|
|
289
|
-
|
|
290
|
-
|
|
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;
|
|
291
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
|
+
),
|
|
292
312
|
[graph],
|
|
293
313
|
);
|
|
294
314
|
|
|
295
|
-
const
|
|
296
|
-
(
|
|
297
|
-
|
|
298
|
-
.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
})
|
|
304
|
-
.filter(isNonNullable) as Node[];
|
|
305
|
-
const parentOf =
|
|
306
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
307
|
-
return {
|
|
308
|
-
id: node.id,
|
|
309
|
-
label: node.id,
|
|
310
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
311
|
-
parentOf,
|
|
312
|
-
};
|
|
313
|
-
},
|
|
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
|
+
),
|
|
314
323
|
[graph],
|
|
315
324
|
);
|
|
316
325
|
|
|
317
|
-
const
|
|
318
|
-
(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
);
|
|
324
355
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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],
|
|
328
363
|
);
|
|
329
364
|
|
|
330
|
-
const
|
|
331
|
-
(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
);
|
|
337
373
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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],
|
|
341
383
|
);
|
|
342
384
|
|
|
343
385
|
const onOpenChange = useCallback(
|
|
344
386
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
345
387
|
const path = Path.create(..._path);
|
|
346
|
-
const
|
|
347
|
-
|
|
388
|
+
const atom = stateRef.current.get(path);
|
|
389
|
+
if (atom) {
|
|
390
|
+
const prev = registry.get(atom);
|
|
391
|
+
registry.set(atom, { ...prev, open });
|
|
392
|
+
}
|
|
348
393
|
},
|
|
349
|
-
[
|
|
394
|
+
[registry],
|
|
350
395
|
);
|
|
351
396
|
|
|
352
397
|
const onSelect = useCallback(
|
|
353
398
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
354
399
|
const path = Path.create(..._path);
|
|
355
|
-
const
|
|
356
|
-
|
|
400
|
+
const atom = stateRef.current.get(path);
|
|
401
|
+
if (atom) {
|
|
402
|
+
const prev = registry.get(atom);
|
|
403
|
+
registry.set(atom, { ...prev, current });
|
|
404
|
+
}
|
|
357
405
|
},
|
|
358
|
-
[
|
|
406
|
+
[registry],
|
|
359
407
|
);
|
|
360
408
|
|
|
361
409
|
return (
|
|
362
410
|
<>
|
|
363
411
|
<Controls />
|
|
364
|
-
<Tree
|
|
365
|
-
id={ROOT_ID}
|
|
366
|
-
useItems={useItems}
|
|
367
|
-
getProps={getProps}
|
|
368
|
-
isOpen={isOpen}
|
|
369
|
-
isCurrent={isCurrent}
|
|
370
|
-
onOpenChange={onOpenChange}
|
|
371
|
-
onSelect={onSelect}
|
|
372
|
-
/>
|
|
412
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
373
413
|
</>
|
|
374
414
|
);
|
|
375
415
|
},
|
package/src/stories/Tree.tsx
CHANGED