@dxos/app-graph 0.8.3 → 0.8.4-main.1c7ec43d41
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 +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 +6 -13
- 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 +53 -42
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +1193 -126
- package/src/graph-builder.ts +753 -264
- 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 +178 -255
- 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/index.cjs +0 -816
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/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
|
@@ -2,40 +2,30 @@
|
|
|
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
|
-
|
|
16
|
-
type QueryResult,
|
|
17
|
-
type Space,
|
|
18
|
-
SpaceState,
|
|
19
|
-
Expando,
|
|
20
|
-
type Live,
|
|
21
|
-
Filter,
|
|
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 { 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
|
-
import {
|
|
28
|
-
import { Path, Tree } from '@dxos/react-ui-list';
|
|
29
|
-
import {
|
|
30
|
-
import { getSize, mx } from '@dxos/
|
|
31
|
-
import {
|
|
32
|
-
|
|
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,26 +247,31 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
245
247
|
);
|
|
246
248
|
};
|
|
247
249
|
|
|
248
|
-
|
|
250
|
+
const meta = {
|
|
249
251
|
title: 'sdk/app-graph/EchoGraph',
|
|
250
252
|
decorators: [
|
|
251
|
-
withTheme,
|
|
253
|
+
withTheme(),
|
|
252
254
|
withClientProvider({
|
|
253
255
|
createIdentity: true,
|
|
254
|
-
|
|
256
|
+
types: [TestSchema.Expando],
|
|
257
|
+
onCreateIdentity: async ({ client }) => {
|
|
255
258
|
await client.spaces.create();
|
|
256
259
|
await client.spaces.create();
|
|
257
260
|
},
|
|
258
261
|
}),
|
|
259
262
|
],
|
|
260
|
-
};
|
|
263
|
+
} satisfies Meta;
|
|
264
|
+
|
|
265
|
+
export default meta;
|
|
261
266
|
|
|
262
|
-
|
|
267
|
+
type Story = StoryObj<typeof meta>;
|
|
268
|
+
|
|
269
|
+
export const JsonView: Story = {
|
|
263
270
|
render: () => {
|
|
264
271
|
const client = useClient();
|
|
265
272
|
const registry = useContext(RegistryContext);
|
|
266
273
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
267
|
-
const data =
|
|
274
|
+
const data = useAtomValue(graph.json());
|
|
268
275
|
|
|
269
276
|
return (
|
|
270
277
|
<>
|
|
@@ -275,218 +282,134 @@ export const JsonView = {
|
|
|
275
282
|
},
|
|
276
283
|
};
|
|
277
284
|
|
|
278
|
-
export const TreeView = {
|
|
285
|
+
export const TreeView: Story = {
|
|
279
286
|
render: () => {
|
|
280
287
|
const client = useClient();
|
|
281
288
|
const registry = useContext(RegistryContext);
|
|
282
289
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
const getProps = useCallback(
|
|
294
|
-
(node: Node, path: string[]) => {
|
|
295
|
-
const children = graph
|
|
296
|
-
.getConnections(node.id, 'outbound')
|
|
297
|
-
.map((n) => {
|
|
298
|
-
// Break cycles.
|
|
299
|
-
const nextPath = [...path, node.id];
|
|
300
|
-
return nextPath.includes(n.id) ? undefined : (n as Node);
|
|
301
|
-
})
|
|
302
|
-
.filter(isNonNullable) as Node[];
|
|
303
|
-
const parentOf =
|
|
304
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
305
|
-
return {
|
|
306
|
-
id: node.id,
|
|
307
|
-
label: node.id,
|
|
308
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
309
|
-
parentOf,
|
|
310
|
-
};
|
|
311
|
-
},
|
|
312
|
-
[graph],
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
const isOpen = useCallback(
|
|
316
|
-
(_path: string[]) => {
|
|
317
|
-
const path = Path.create(..._path);
|
|
318
|
-
const object = state.get(path) ?? live({ open: true, current: false });
|
|
319
|
-
if (!state.has(path)) {
|
|
320
|
-
state.set(path, object);
|
|
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);
|
|
321
298
|
}
|
|
322
|
-
|
|
323
|
-
return object.open;
|
|
324
|
-
},
|
|
325
|
-
[state],
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
const isCurrent = useCallback(
|
|
329
|
-
(_path: string[]) => {
|
|
330
|
-
const path = Path.create(..._path);
|
|
331
|
-
const object = state.get(path) ?? live({ open: false, current: false });
|
|
332
|
-
if (!state.has(path)) {
|
|
333
|
-
state.set(path, object);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return object.current;
|
|
337
|
-
},
|
|
338
|
-
[state],
|
|
339
|
-
);
|
|
340
|
-
|
|
341
|
-
const onOpenChange = useCallback(
|
|
342
|
-
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
343
|
-
const path = Path.create(..._path);
|
|
344
|
-
const object = state.get(path);
|
|
345
|
-
object!.open = open;
|
|
346
|
-
},
|
|
347
|
-
[state],
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
const onSelect = useCallback(
|
|
351
|
-
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
352
|
-
const path = Path.create(..._path);
|
|
353
|
-
const object = state.get(path);
|
|
354
|
-
object!.current = current;
|
|
299
|
+
return atom;
|
|
355
300
|
},
|
|
356
|
-
[
|
|
301
|
+
[],
|
|
357
302
|
);
|
|
358
303
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
onOpenChange={onOpenChange}
|
|
369
|
-
onSelect={onSelect}
|
|
370
|
-
/>
|
|
371
|
-
</>
|
|
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
|
+
),
|
|
312
|
+
[graph],
|
|
372
313
|
);
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// TODO(wittjosiah): Remove.
|
|
377
|
-
export const TabTreeView = {
|
|
378
|
-
render: () => {
|
|
379
|
-
const client = useClient();
|
|
380
|
-
const registry = useContext(RegistryContext);
|
|
381
|
-
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
382
|
-
const state = useMemo(() => new Map<string, Live<{ open: boolean; current: boolean }>>(), []);
|
|
383
314
|
|
|
384
|
-
const
|
|
385
|
-
(
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
+
),
|
|
389
323
|
[graph],
|
|
390
324
|
);
|
|
391
325
|
|
|
392
|
-
const
|
|
393
|
-
(
|
|
394
|
-
|
|
395
|
-
.
|
|
396
|
-
.
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
+
}),
|
|
411
353
|
[graph],
|
|
412
354
|
);
|
|
413
355
|
|
|
414
|
-
const
|
|
415
|
-
(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return object.open;
|
|
423
|
-
},
|
|
424
|
-
[state],
|
|
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],
|
|
425
363
|
);
|
|
426
364
|
|
|
427
|
-
const
|
|
428
|
-
(
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
);
|
|
434
373
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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],
|
|
438
383
|
);
|
|
439
384
|
|
|
440
385
|
const onOpenChange = useCallback(
|
|
441
386
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
442
387
|
const path = Path.create(..._path);
|
|
443
|
-
const
|
|
444
|
-
|
|
388
|
+
const atom = stateRef.current.get(path);
|
|
389
|
+
if (atom) {
|
|
390
|
+
const prev = registry.get(atom);
|
|
391
|
+
registry.set(atom, { ...prev, open });
|
|
392
|
+
}
|
|
445
393
|
},
|
|
446
|
-
[
|
|
394
|
+
[registry],
|
|
447
395
|
);
|
|
448
396
|
|
|
449
397
|
const onSelect = useCallback(
|
|
450
398
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
451
399
|
const path = Path.create(..._path);
|
|
452
|
-
const
|
|
453
|
-
|
|
400
|
+
const atom = stateRef.current.get(path);
|
|
401
|
+
if (atom) {
|
|
402
|
+
const prev = registry.get(atom);
|
|
403
|
+
registry.set(atom, { ...prev, current });
|
|
404
|
+
}
|
|
454
405
|
},
|
|
455
|
-
[
|
|
406
|
+
[registry],
|
|
456
407
|
);
|
|
457
408
|
|
|
458
|
-
const spaces = useItems(graph.root);
|
|
459
|
-
|
|
460
409
|
return (
|
|
461
410
|
<>
|
|
462
411
|
<Controls />
|
|
463
|
-
<
|
|
464
|
-
<Tabs.Tablist>
|
|
465
|
-
{spaces.map((space) => {
|
|
466
|
-
return (
|
|
467
|
-
<Tabs.Tab key={space.id} value={space.id}>
|
|
468
|
-
{space.id}
|
|
469
|
-
</Tabs.Tab>
|
|
470
|
-
);
|
|
471
|
-
})}
|
|
472
|
-
</Tabs.Tablist>
|
|
473
|
-
{spaces.map((space) => {
|
|
474
|
-
return (
|
|
475
|
-
<Tabs.Tabpanel key={space.id} value={space.id}>
|
|
476
|
-
<Tree
|
|
477
|
-
id={space.id}
|
|
478
|
-
root={space}
|
|
479
|
-
useItems={useItems}
|
|
480
|
-
getProps={getProps}
|
|
481
|
-
isOpen={isOpen}
|
|
482
|
-
isCurrent={isCurrent}
|
|
483
|
-
onOpenChange={onOpenChange}
|
|
484
|
-
onSelect={onSelect}
|
|
485
|
-
/>
|
|
486
|
-
</Tabs.Tabpanel>
|
|
487
|
-
);
|
|
488
|
-
})}
|
|
489
|
-
</Tabs.Root>
|
|
412
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
490
413
|
</>
|
|
491
414
|
);
|
|
492
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
|
+
};
|