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