@dxos/app-graph 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe
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/browser/chunk-W47H2NND.mjs +1604 -0
- package/dist/lib/browser/chunk-W47H2NND.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +17 -1276
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +39 -0
- package/dist/lib/browser/testing/index.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-LYZWNJ7J.mjs +1605 -0
- package/dist/lib/node-esm/chunk-LYZWNJ7J.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +17 -1276
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +40 -0
- package/dist/lib/node-esm/testing/index.mjs.map +7 -0
- package/dist/types/src/graph-builder.d.ts +11 -7
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +13 -17
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +44 -18
- package/dist/types/src/node-matcher.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +21 -5
- package/dist/types/src/node.d.ts.map +1 -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 +39 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -26
- package/src/graph-builder.test.ts +569 -102
- package/src/graph-builder.ts +202 -74
- package/src/graph.test.ts +187 -52
- package/src/graph.ts +177 -98
- package/src/index.ts +1 -0
- package/src/node-matcher.ts +59 -29
- package/src/node.ts +46 -5
- package/src/stories/EchoGraph.stories.tsx +93 -65
- 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 +95 -0
package/src/node-matcher.ts
CHANGED
|
@@ -69,7 +69,7 @@ export const whenId =
|
|
|
69
69
|
* ```ts
|
|
70
70
|
* GraphBuilder.createExtension({
|
|
71
71
|
* id: 'space-settings-extension',
|
|
72
|
-
* match: NodeMatcher.whenNodeType('dxos.
|
|
72
|
+
* match: NodeMatcher.whenNodeType('org.dxos.plugin.space.settings'),
|
|
73
73
|
* connector: (node) => Effect.succeed([...]),
|
|
74
74
|
* });
|
|
75
75
|
* ```
|
|
@@ -106,10 +106,13 @@ export const whenNodeType =
|
|
|
106
106
|
* });
|
|
107
107
|
* ```
|
|
108
108
|
*
|
|
109
|
-
* @
|
|
109
|
+
* Can be composed directly with {@link whenAll}/{@link whenAny}/{@link whenNot} while
|
|
110
|
+
* preserving the typed entity data in the result.
|
|
111
|
+
*
|
|
112
|
+
* @see {@link whenEchoTypeMatches} - Returns the node instead of data for legacy composition.
|
|
110
113
|
*/
|
|
111
114
|
export const whenEchoType =
|
|
112
|
-
<T extends Type.
|
|
115
|
+
<T extends Type.AnyEntity>(type: T): NodeMatcher<Entity.Entity<Schema.Schema.Type<T>>> =>
|
|
113
116
|
(node: Node.Node): Option.Option<Entity.Entity<Schema.Schema.Type<T>>> =>
|
|
114
117
|
Obj.instanceOf(type, node.data) ? Option.some(node.data) : Option.none();
|
|
115
118
|
|
|
@@ -124,17 +127,20 @@ export const whenEchoType =
|
|
|
124
127
|
* @example
|
|
125
128
|
* ```ts
|
|
126
129
|
* GraphBuilder.createExtension({
|
|
127
|
-
* id: 'object-
|
|
130
|
+
* id: 'object-properties',
|
|
128
131
|
* match: NodeMatcher.whenEchoObject,
|
|
129
132
|
* connector: (object) => {
|
|
130
133
|
* // `object` is typed as Obj.Unknown
|
|
131
134
|
* const id = Obj.getDXN(object).toString();
|
|
132
|
-
* return Effect.succeed([{ id: `${id}
|
|
135
|
+
* return Effect.succeed([{ id: `${id}.settings`, ... }]);
|
|
133
136
|
* },
|
|
134
137
|
* });
|
|
135
138
|
* ```
|
|
136
139
|
*
|
|
137
|
-
* @
|
|
140
|
+
* Can be composed directly with {@link whenAll}/{@link whenAny}/{@link whenNot} while
|
|
141
|
+
* preserving the `Obj.Unknown` data type in the result.
|
|
142
|
+
*
|
|
143
|
+
* @see {@link whenEchoObjectMatches} - Returns the node instead of data for legacy composition.
|
|
138
144
|
*/
|
|
139
145
|
export const whenEchoObject = (node: Node.Node): Option.Option<Obj.Unknown> =>
|
|
140
146
|
Obj.isObject(node.data) ? Option.some(node.data) : Option.none();
|
|
@@ -145,38 +151,53 @@ export const whenEchoObject = (node: Node.Node): Option.Option<Obj.Unknown> =>
|
|
|
145
151
|
|
|
146
152
|
/**
|
|
147
153
|
* Composes multiple matchers with AND logic - all matchers must match for success.
|
|
148
|
-
*
|
|
154
|
+
* The result data type is the intersection of all matchers' data types.
|
|
155
|
+
* Filter matchers like {@link whenNot} return `unknown`, making them transparent
|
|
156
|
+
* in the intersection (since `T & unknown = T`).
|
|
149
157
|
*
|
|
150
158
|
* @param matchers - The matchers to combine. All must return Option.some for success.
|
|
151
|
-
* @returns A matcher
|
|
159
|
+
* @returns A matcher whose data type is the intersection of all input matchers' data types.
|
|
160
|
+
* Returns the first matcher's value when all match, Option.none() otherwise.
|
|
152
161
|
*
|
|
153
162
|
* @example
|
|
154
163
|
* ```ts
|
|
155
|
-
* // Match ECHO objects that are NOT Channels
|
|
164
|
+
* // Match ECHO objects that are NOT Channels — result is NodeMatcher<Obj.Unknown>.
|
|
156
165
|
* const whenCommentable = NodeMatcher.whenAll(
|
|
157
|
-
* NodeMatcher.
|
|
166
|
+
* NodeMatcher.whenEchoObject,
|
|
158
167
|
* NodeMatcher.whenNot(NodeMatcher.whenEchoTypeMatches(Channel.Channel)),
|
|
159
168
|
* );
|
|
160
169
|
* ```
|
|
161
170
|
*/
|
|
162
|
-
export const whenAll
|
|
163
|
-
(
|
|
164
|
-
(
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
export const whenAll: {
|
|
172
|
+
<A>(a: NodeMatcher<A>, b: NodeMatcher<unknown>): NodeMatcher<A>;
|
|
173
|
+
<A>(a: NodeMatcher<unknown>, b: NodeMatcher<A>): NodeMatcher<A>;
|
|
174
|
+
<A, B>(a: NodeMatcher<A>, b: NodeMatcher<B>): NodeMatcher<A & B>;
|
|
175
|
+
<A, B, C>(a: NodeMatcher<A>, b: NodeMatcher<B>, c: NodeMatcher<C>): NodeMatcher<A & B & C>;
|
|
176
|
+
<A, B, C, D>(a: NodeMatcher<A>, b: NodeMatcher<B>, c: NodeMatcher<C>, d: NodeMatcher<D>): NodeMatcher<A & B & C & D>;
|
|
177
|
+
(...matchers: NodeMatcher<any>[]): NodeMatcher<any>;
|
|
178
|
+
} =
|
|
179
|
+
(...matchers: NodeMatcher<any>[]): NodeMatcher<any> =>
|
|
180
|
+
(node: Node.Node) => {
|
|
181
|
+
let first: Option.Option<any> = Option.none();
|
|
182
|
+
for (const candidate of matchers) {
|
|
183
|
+
const result = candidate(node);
|
|
167
184
|
if (Option.isNone(result)) {
|
|
168
185
|
return Option.none();
|
|
169
186
|
}
|
|
187
|
+
if (Option.isNone(first)) {
|
|
188
|
+
first = result;
|
|
189
|
+
}
|
|
170
190
|
}
|
|
171
|
-
return
|
|
191
|
+
return first;
|
|
172
192
|
};
|
|
173
193
|
|
|
174
194
|
/**
|
|
175
195
|
* Composes multiple matchers with OR logic - at least one matcher must match.
|
|
176
|
-
*
|
|
196
|
+
* The result data type is the union of all matchers' data types.
|
|
177
197
|
*
|
|
178
198
|
* @param matchers - The matchers to combine. At least one must return Option.some.
|
|
179
|
-
* @returns A matcher
|
|
199
|
+
* @returns A matcher whose data type is the union of all input matchers' data types.
|
|
200
|
+
* Returns the first matching matcher's value, or Option.none() if none match.
|
|
180
201
|
*
|
|
181
202
|
* @example
|
|
182
203
|
* ```ts
|
|
@@ -187,13 +208,18 @@ export const whenAll =
|
|
|
187
208
|
* );
|
|
188
209
|
* ```
|
|
189
210
|
*/
|
|
190
|
-
export const whenAny
|
|
191
|
-
(
|
|
192
|
-
(
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
export const whenAny: {
|
|
212
|
+
<A, B>(a: NodeMatcher<A>, b: NodeMatcher<B>): NodeMatcher<A | B>;
|
|
213
|
+
<A, B, C>(a: NodeMatcher<A>, b: NodeMatcher<B>, c: NodeMatcher<C>): NodeMatcher<A | B | C>;
|
|
214
|
+
<A, B, C, D>(a: NodeMatcher<A>, b: NodeMatcher<B>, c: NodeMatcher<C>, d: NodeMatcher<D>): NodeMatcher<A | B | C | D>;
|
|
215
|
+
(...matchers: NodeMatcher<any>[]): NodeMatcher<any>;
|
|
216
|
+
} =
|
|
217
|
+
(...matchers: NodeMatcher<any>[]): NodeMatcher<any> =>
|
|
218
|
+
(node: Node.Node) => {
|
|
219
|
+
for (const candidate of matchers) {
|
|
220
|
+
const result = candidate(node);
|
|
195
221
|
if (Option.isSome(result)) {
|
|
196
|
-
return
|
|
222
|
+
return result;
|
|
197
223
|
}
|
|
198
224
|
}
|
|
199
225
|
return Option.none();
|
|
@@ -229,7 +255,7 @@ export const whenAny =
|
|
|
229
255
|
* @see {@link whenEchoType} - Use instead when you need the typed entity directly.
|
|
230
256
|
*/
|
|
231
257
|
export const whenEchoTypeMatches =
|
|
232
|
-
<T extends Type.
|
|
258
|
+
<T extends Type.AnyEntity>(type: T): NodeMatcher =>
|
|
233
259
|
(node: Node.Node): Option.Option<Node.Node> =>
|
|
234
260
|
Obj.instanceOf(type, node.data) ? Option.some(node) : Option.none();
|
|
235
261
|
|
|
@@ -262,15 +288,19 @@ export const whenEchoObjectMatches = (node: Node.Node): Option.Option<Node.Node>
|
|
|
262
288
|
* Negates a matcher - matches when the given matcher does NOT match.
|
|
263
289
|
* Useful for exclusion patterns like "any object EXCEPT type X".
|
|
264
290
|
*
|
|
291
|
+
* Returns `NodeMatcher<unknown>` because negation is a filter — it doesn't provide
|
|
292
|
+
* typed data. This makes it transparent in {@link whenAll} intersections
|
|
293
|
+
* (since `T & unknown = T`).
|
|
294
|
+
*
|
|
265
295
|
* @param matcher - The matcher to negate.
|
|
266
296
|
* @returns A matcher that returns Option.some(node) if the input matcher returns none,
|
|
267
297
|
* and Option.none() if the input matcher returns some.
|
|
268
298
|
*
|
|
269
299
|
* @example
|
|
270
300
|
* ```ts
|
|
271
|
-
* // Match any ECHO object that is NOT a Channel
|
|
301
|
+
* // Match any ECHO object that is NOT a Channel — result is NodeMatcher<Obj.Unknown>.
|
|
272
302
|
* const whenCommentable = NodeMatcher.whenAll(
|
|
273
|
-
* NodeMatcher.
|
|
303
|
+
* NodeMatcher.whenEchoObject,
|
|
274
304
|
* NodeMatcher.whenNot(NodeMatcher.whenEchoTypeMatches(Channel.Channel)),
|
|
275
305
|
* );
|
|
276
306
|
*
|
|
@@ -279,6 +309,6 @@ export const whenEchoObjectMatches = (node: Node.Node): Option.Option<Node.Node>
|
|
|
279
309
|
* ```
|
|
280
310
|
*/
|
|
281
311
|
export const whenNot =
|
|
282
|
-
(matcher: NodeMatcher): NodeMatcher =>
|
|
283
|
-
(node: Node.Node): Option.Option<
|
|
312
|
+
(matcher: NodeMatcher<any>): NodeMatcher<unknown> =>
|
|
313
|
+
(node: Node.Node): Option.Option<unknown> =>
|
|
284
314
|
Option.isNone(matcher(node)) ? Option.some(node) : Option.none();
|
package/src/node.ts
CHANGED
|
@@ -15,17 +15,17 @@ export const RootId = 'root';
|
|
|
15
15
|
/**
|
|
16
16
|
* Root node type.
|
|
17
17
|
*/
|
|
18
|
-
export const RootType = 'dxos.
|
|
18
|
+
export const RootType = 'org.dxos.type.graphRoot';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Action node type.
|
|
22
22
|
*/
|
|
23
|
-
export const ActionType = 'dxos.
|
|
23
|
+
export const ActionType = 'org.dxos.type.graphAction';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Action group node type.
|
|
27
27
|
*/
|
|
28
|
-
export const ActionGroupType = 'dxos.
|
|
28
|
+
export const ActionGroupType = 'org.dxos.type.graphActionGroup';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Represents a node in the graph.
|
|
@@ -69,7 +69,19 @@ export type NodeFilter<TData = any, TProperties extends Record<string, any> = Re
|
|
|
69
69
|
connectedNode: Node,
|
|
70
70
|
) => node is Node<TData, TProperties>;
|
|
71
71
|
|
|
72
|
-
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);
|
|
73
85
|
|
|
74
86
|
export const isGraphNode = (data: unknown): data is Node =>
|
|
75
87
|
data && typeof data === 'object' && 'id' in data && 'properties' in data && data.properties
|
|
@@ -84,7 +96,7 @@ export type NodeArg<TData, TProperties extends Record<string, any> = Record<stri
|
|
|
84
96
|
nodes?: NodeArg<unknown>[];
|
|
85
97
|
|
|
86
98
|
/** Will automatically add specified edges. */
|
|
87
|
-
edges?: [string,
|
|
99
|
+
edges?: [string, RelationInput][];
|
|
88
100
|
};
|
|
89
101
|
|
|
90
102
|
//
|
|
@@ -95,6 +107,9 @@ export type InvokeProps = {
|
|
|
95
107
|
/** Node the invoked action is connected to. */
|
|
96
108
|
parent?: Node;
|
|
97
109
|
|
|
110
|
+
/** Path from root to the node in the current tree context. */
|
|
111
|
+
path?: string[];
|
|
112
|
+
|
|
98
113
|
caller?: string;
|
|
99
114
|
};
|
|
100
115
|
|
|
@@ -135,3 +150,29 @@ export const isActionGroup = (data: unknown): data is ActionGroup =>
|
|
|
135
150
|
export type ActionLike = Action | ActionGroup;
|
|
136
151
|
|
|
137
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
|
+
});
|
|
@@ -10,22 +10,21 @@ import React, { type PropsWithChildren, useCallback, useContext, useEffect, useM
|
|
|
10
10
|
|
|
11
11
|
import { Filter, type Space, SpaceState, isSpace } from '@dxos/client/echo';
|
|
12
12
|
import { Obj, Query } from '@dxos/echo';
|
|
13
|
-
import { TestSchema } from '@dxos/echo/testing';
|
|
14
13
|
import { AtomObj, AtomQuery } from '@dxos/echo-atom';
|
|
15
|
-
import {
|
|
14
|
+
import { TestSchema } from '@dxos/echo/testing';
|
|
15
|
+
import { random } from '@dxos/random';
|
|
16
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
17
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
18
18
|
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
19
|
+
import { Path, Tree, type TreeModel } from '@dxos/react-ui-list';
|
|
19
20
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
20
|
-
import { Path, Tree } from '@dxos/react-ui-list';
|
|
21
21
|
import { getSize, mx } from '@dxos/ui-theme';
|
|
22
|
-
import {
|
|
22
|
+
import { safeParseInt } from '@dxos/util';
|
|
23
23
|
|
|
24
24
|
import * as CreateAtom from '../atoms';
|
|
25
25
|
import * as Graph from '../graph';
|
|
26
26
|
import * as GraphBuilder from '../graph-builder';
|
|
27
27
|
import * as Node from '../node';
|
|
28
|
-
|
|
29
28
|
import { JsonTree } from './Tree';
|
|
30
29
|
|
|
31
30
|
const DEFAULT_PERIOD = 500;
|
|
@@ -64,7 +63,7 @@ const createGraph = (client: Client, registry: Registry.Registry): Graph.Expanda
|
|
|
64
63
|
const propertiesSnapshot = get(AtomObj.make(space.properties));
|
|
65
64
|
return {
|
|
66
65
|
id: space.id,
|
|
67
|
-
type: 'dxos.
|
|
66
|
+
type: 'org.dxos.type.space',
|
|
68
67
|
properties: {
|
|
69
68
|
label: propertiesSnapshot.name,
|
|
70
69
|
},
|
|
@@ -88,7 +87,7 @@ const createGraph = (client: Client, registry: Registry.Registry): Graph.Expanda
|
|
|
88
87
|
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
89
88
|
return objects.map((object) => ({
|
|
90
89
|
id: object.id,
|
|
91
|
-
type: 'dxos.
|
|
90
|
+
type: 'org.dxos.type.test',
|
|
92
91
|
properties: { label: object.name },
|
|
93
92
|
data: object,
|
|
94
93
|
}));
|
|
@@ -104,9 +103,9 @@ const createGraph = (client: Client, registry: Registry.Registry): Graph.Expanda
|
|
|
104
103
|
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
105
104
|
const graph = builder.graph;
|
|
106
105
|
graph.onNodeChanged.on(({ id }) => {
|
|
107
|
-
Graph.expand(graph, id);
|
|
106
|
+
Graph.expand(graph, id, 'child');
|
|
108
107
|
});
|
|
109
|
-
Graph.expand(graph, Node.RootId);
|
|
108
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
110
109
|
(window as any).graph = graph;
|
|
111
110
|
return graph;
|
|
112
111
|
};
|
|
@@ -146,8 +145,8 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
146
145
|
case Action.RENAME_SPACE: {
|
|
147
146
|
const space = getRandomSpace(client);
|
|
148
147
|
if (space) {
|
|
149
|
-
Obj.change(space.properties, (
|
|
150
|
-
|
|
148
|
+
Obj.change(space.properties, (obj) => {
|
|
149
|
+
obj.name = random.commerce.productName();
|
|
151
150
|
});
|
|
152
151
|
}
|
|
153
152
|
break;
|
|
@@ -157,7 +156,7 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
157
156
|
getRandomSpace(client)?.db.add(
|
|
158
157
|
Obj.make(TestSchema.Expando, {
|
|
159
158
|
type: 'test',
|
|
160
|
-
name:
|
|
159
|
+
name: random.commerce.productName(),
|
|
161
160
|
}),
|
|
162
161
|
);
|
|
163
162
|
break;
|
|
@@ -176,8 +175,8 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
176
175
|
if (space) {
|
|
177
176
|
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
178
177
|
const object = objects[Math.floor(Math.random() * objects.length)];
|
|
179
|
-
Obj.change(object, (
|
|
180
|
-
|
|
178
|
+
Obj.change(object, (object) => {
|
|
179
|
+
object.name = random.commerce.productName();
|
|
181
180
|
});
|
|
182
181
|
}
|
|
183
182
|
break;
|
|
@@ -216,14 +215,13 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
216
215
|
<Input.Root>
|
|
217
216
|
<Input.TextInput
|
|
218
217
|
autoComplete='off'
|
|
219
|
-
|
|
220
|
-
classNames='is-[100px] text-right pie-[22px]'
|
|
218
|
+
classNames='w-[100px] text-right pe-[22px]'
|
|
221
219
|
placeholder='Interval'
|
|
222
220
|
value={actionInterval}
|
|
223
221
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
224
222
|
/>
|
|
225
223
|
</Input.Root>
|
|
226
|
-
<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))} />
|
|
227
225
|
</div>
|
|
228
226
|
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
229
227
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
@@ -252,9 +250,10 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
252
250
|
const meta = {
|
|
253
251
|
title: 'sdk/app-graph/EchoGraph',
|
|
254
252
|
decorators: [
|
|
255
|
-
withTheme,
|
|
253
|
+
withTheme(),
|
|
256
254
|
withClientProvider({
|
|
257
255
|
createIdentity: true,
|
|
256
|
+
types: [TestSchema.Expando],
|
|
258
257
|
onCreateIdentity: async ({ client }) => {
|
|
259
258
|
await client.spaces.create();
|
|
260
259
|
await client.spaces.create();
|
|
@@ -291,60 +290,97 @@ export const TreeView: Story = {
|
|
|
291
290
|
const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
292
291
|
|
|
293
292
|
const getOrCreateState = useMemo(
|
|
294
|
-
() => (
|
|
295
|
-
let atom = stateRef.current.get(
|
|
293
|
+
() => (pathKey: string) => {
|
|
294
|
+
let atom = stateRef.current.get(pathKey);
|
|
296
295
|
if (!atom) {
|
|
297
296
|
atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
|
|
298
|
-
stateRef.current.set(
|
|
297
|
+
stateRef.current.set(pathKey, atom);
|
|
299
298
|
}
|
|
300
299
|
return atom;
|
|
301
300
|
},
|
|
302
301
|
[],
|
|
303
302
|
);
|
|
304
303
|
|
|
305
|
-
const
|
|
306
|
-
(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
+
),
|
|
310
312
|
[graph],
|
|
311
313
|
);
|
|
312
314
|
|
|
313
|
-
const
|
|
314
|
-
(
|
|
315
|
-
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.filter(isNonNullable) as Node.Node[];
|
|
322
|
-
const parentOf =
|
|
323
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
324
|
-
return {
|
|
325
|
-
id: node.id,
|
|
326
|
-
label: node.id,
|
|
327
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
328
|
-
parentOf,
|
|
329
|
-
};
|
|
330
|
-
},
|
|
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
|
+
),
|
|
331
323
|
[graph],
|
|
332
324
|
);
|
|
333
325
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
+
);
|
|
340
355
|
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
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],
|
|
363
|
+
);
|
|
344
364
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
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
|
+
);
|
|
373
|
+
|
|
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],
|
|
383
|
+
);
|
|
348
384
|
|
|
349
385
|
const onOpenChange = useCallback(
|
|
350
386
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
@@ -373,15 +409,7 @@ export const TreeView: Story = {
|
|
|
373
409
|
return (
|
|
374
410
|
<>
|
|
375
411
|
<Controls />
|
|
376
|
-
<Tree
|
|
377
|
-
id={Node.RootId}
|
|
378
|
-
useItems={useItems}
|
|
379
|
-
getProps={getProps}
|
|
380
|
-
useIsOpen={useIsOpen}
|
|
381
|
-
useIsCurrent={useIsCurrent}
|
|
382
|
-
onOpenChange={onOpenChange}
|
|
383
|
-
onSelect={onSelect}
|
|
384
|
-
/>
|
|
412
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
385
413
|
</>
|
|
386
414
|
);
|
|
387
415
|
},
|
package/src/stories/Tree.tsx
CHANGED
|
@@ -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
|
+
};
|