@dxos/app-graph 0.8.3 → 0.8.4-main.1068cf700f
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/index.mjs +1135 -616
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1134 -616
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/atoms.d.ts +8 -0
- package/dist/types/src/atoms.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +113 -60
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +183 -209
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +6 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node-matcher.d.ts +218 -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 +32 -3
- package/dist/types/src/node.d.ts.map +1 -1
- 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/tsconfig.tsbuildinfo +1 -1
- package/package.json +37 -37
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +571 -97
- package/src/graph-builder.ts +600 -258
- package/src/graph.test.ts +300 -107
- package/src/graph.ts +971 -400
- package/src/index.ts +9 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +284 -0
- package/src/node.ts +40 -5
- package/src/stories/EchoGraph.stories.tsx +128 -233
- package/src/stories/Tree.tsx +2 -2
- 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/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,31 @@
|
|
|
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
|
-
Query,
|
|
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';
|
|
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 { TestSchema } from '@dxos/echo/testing';
|
|
14
|
+
import { AtomObj, AtomQuery } from '@dxos/echo-atom';
|
|
24
15
|
import { faker } from '@dxos/random';
|
|
25
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
26
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
27
|
-
import {
|
|
18
|
+
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
19
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
28
20
|
import { Path, Tree } from '@dxos/react-ui-list';
|
|
29
|
-
import {
|
|
30
|
-
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
31
|
-
import { withTheme } from '@dxos/storybook-utils';
|
|
21
|
+
import { getSize, mx } from '@dxos/ui-theme';
|
|
32
22
|
import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
|
|
33
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';
|
|
28
|
+
|
|
34
29
|
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
30
|
|
|
40
31
|
const DEFAULT_PERIOD = 500;
|
|
41
32
|
|
|
@@ -57,43 +48,44 @@ const actionWeights = {
|
|
|
57
48
|
[Action.RENAME_OBJECT]: 4,
|
|
58
49
|
};
|
|
59
50
|
|
|
60
|
-
const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
|
|
61
|
-
const spaceBuilderExtension =
|
|
51
|
+
const createGraph = (client: Client, registry: Registry.Registry): Graph.ExpandableGraph => {
|
|
52
|
+
const spaceBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
62
53
|
id: 'space',
|
|
63
54
|
connector: (node) =>
|
|
64
|
-
|
|
65
|
-
pipe(
|
|
55
|
+
Atom.make((get) =>
|
|
56
|
+
Function.pipe(
|
|
66
57
|
get(node),
|
|
67
|
-
Option.flatMap((node) => (node.id ===
|
|
58
|
+
Option.flatMap((node) => (node.id === Node.RootId ? Option.some(node) : Option.none())),
|
|
68
59
|
Option.map(() => {
|
|
69
|
-
const spaces = get(
|
|
60
|
+
const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
|
|
70
61
|
return spaces
|
|
71
|
-
.filter((space) => get(
|
|
72
|
-
.map((space) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
.filter((space: any) => get(CreateAtom.fromObservable(space.state)) === SpaceState.SPACE_READY)
|
|
63
|
+
.map((space) => {
|
|
64
|
+
const propertiesSnapshot = get(AtomObj.make(space.properties));
|
|
65
|
+
return {
|
|
66
|
+
id: space.id,
|
|
67
|
+
type: 'dxos.org/type/Space',
|
|
68
|
+
properties: {
|
|
69
|
+
label: propertiesSnapshot.name,
|
|
70
|
+
},
|
|
71
|
+
data: space,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
78
74
|
}),
|
|
79
75
|
Option.getOrElse(() => []),
|
|
80
76
|
),
|
|
81
77
|
),
|
|
82
78
|
});
|
|
83
79
|
|
|
84
|
-
const objectBuilderExtension =
|
|
80
|
+
const objectBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
85
81
|
id: 'object',
|
|
86
82
|
connector: (node) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
pipe(
|
|
83
|
+
return Atom.make((get) =>
|
|
84
|
+
Function.pipe(
|
|
90
85
|
get(node),
|
|
91
86
|
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
92
87
|
Option.map((space) => {
|
|
93
|
-
|
|
94
|
-
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
95
|
-
}
|
|
96
|
-
const objects = get(rxFromQuery(query));
|
|
88
|
+
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
97
89
|
return objects.map((object) => ({
|
|
98
90
|
id: object.id,
|
|
99
91
|
type: 'dxos.org/type/test',
|
|
@@ -107,15 +99,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
|
|
|
107
99
|
},
|
|
108
100
|
});
|
|
109
101
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
102
|
+
const builder = GraphBuilder.make({ registry });
|
|
103
|
+
GraphBuilder.addExtension(builder, spaceBuilderExtension);
|
|
104
|
+
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
105
|
+
const graph = builder.graph;
|
|
113
106
|
graph.onNodeChanged.on(({ id }) => {
|
|
114
|
-
|
|
107
|
+
Graph.expand(graph, id);
|
|
115
108
|
});
|
|
116
|
-
|
|
109
|
+
Graph.expand(graph, Node.RootId);
|
|
117
110
|
(window as any).graph = graph;
|
|
118
|
-
|
|
119
111
|
return graph;
|
|
120
112
|
};
|
|
121
113
|
|
|
@@ -135,9 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
135
127
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
136
128
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
137
129
|
const spaceQueries = await Promise.all(
|
|
138
|
-
readySpaces.map((space) => space.db.query(Filter.type(Expando, { type: 'test' })).run()),
|
|
130
|
+
readySpaces.map((space) => space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run()),
|
|
139
131
|
);
|
|
140
|
-
const spaces = readySpaces.filter((space, index) => spaceQueries[index].
|
|
132
|
+
const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
|
|
141
133
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
142
134
|
};
|
|
143
135
|
|
|
@@ -154,19 +146,26 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
154
146
|
case Action.RENAME_SPACE: {
|
|
155
147
|
const space = getRandomSpace(client);
|
|
156
148
|
if (space) {
|
|
157
|
-
space.properties
|
|
149
|
+
Obj.change(space.properties, (p) => {
|
|
150
|
+
p.name = faker.commerce.productName();
|
|
151
|
+
});
|
|
158
152
|
}
|
|
159
153
|
break;
|
|
160
154
|
}
|
|
161
155
|
|
|
162
156
|
case Action.ADD_OBJECT:
|
|
163
|
-
getRandomSpace(client)?.db.add(
|
|
157
|
+
getRandomSpace(client)?.db.add(
|
|
158
|
+
Obj.make(TestSchema.Expando, {
|
|
159
|
+
type: 'test',
|
|
160
|
+
name: faker.commerce.productName(),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
164
163
|
break;
|
|
165
164
|
|
|
166
165
|
case Action.REMOVE_OBJECT: {
|
|
167
166
|
const space = await getSpaceWithObjects(client);
|
|
168
167
|
if (space) {
|
|
169
|
-
const
|
|
168
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
170
169
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
171
170
|
}
|
|
172
171
|
break;
|
|
@@ -175,8 +174,11 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
175
174
|
case Action.RENAME_OBJECT: {
|
|
176
175
|
const space = await getSpaceWithObjects(client);
|
|
177
176
|
if (space) {
|
|
178
|
-
const
|
|
179
|
-
objects[Math.floor(Math.random() * objects.length)]
|
|
177
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
178
|
+
const object = objects[Math.floor(Math.random() * objects.length)];
|
|
179
|
+
Obj.change(object, (o) => {
|
|
180
|
+
o.name = faker.commerce.productName();
|
|
181
|
+
});
|
|
180
182
|
}
|
|
181
183
|
break;
|
|
182
184
|
}
|
|
@@ -205,23 +207,25 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
205
207
|
return (
|
|
206
208
|
<>
|
|
207
209
|
<div className='flex shrink-0 p-2 space-x-2'>
|
|
208
|
-
<
|
|
210
|
+
<IconButton
|
|
211
|
+
icon={generating ? 'ph--pause--regular' : 'ph--play--regular'}
|
|
212
|
+
label={generating ? 'Pause' : 'Play'}
|
|
213
|
+
onClick={() => setGenerating((generating) => !generating)}
|
|
214
|
+
/>
|
|
209
215
|
<div className='relative' title='mutation period'>
|
|
210
216
|
<Input.Root>
|
|
211
217
|
<Input.TextInput
|
|
212
218
|
autoComplete='off'
|
|
213
219
|
size={5}
|
|
214
|
-
classNames='
|
|
220
|
+
classNames='is-[100px] text-right pie-[22px]'
|
|
215
221
|
placeholder='Interval'
|
|
216
222
|
value={actionInterval}
|
|
217
223
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
218
224
|
/>
|
|
219
225
|
</Input.Root>
|
|
220
|
-
<
|
|
226
|
+
<Icon icon='ph--timer--regular' classNames={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
|
|
221
227
|
</div>
|
|
222
|
-
<
|
|
223
|
-
<Plus />
|
|
224
|
-
</Button>
|
|
228
|
+
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
225
229
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
226
230
|
<Select.TriggerButton placeholder='Select value' />
|
|
227
231
|
<Select.Portal>
|
|
@@ -245,26 +249,30 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
245
249
|
);
|
|
246
250
|
};
|
|
247
251
|
|
|
248
|
-
|
|
252
|
+
const meta = {
|
|
249
253
|
title: 'sdk/app-graph/EchoGraph',
|
|
250
254
|
decorators: [
|
|
251
|
-
withTheme,
|
|
255
|
+
withTheme(),
|
|
252
256
|
withClientProvider({
|
|
253
257
|
createIdentity: true,
|
|
254
|
-
|
|
258
|
+
onCreateIdentity: async ({ client }) => {
|
|
255
259
|
await client.spaces.create();
|
|
256
260
|
await client.spaces.create();
|
|
257
261
|
},
|
|
258
262
|
}),
|
|
259
263
|
],
|
|
260
|
-
};
|
|
264
|
+
} satisfies Meta;
|
|
265
|
+
|
|
266
|
+
export default meta;
|
|
261
267
|
|
|
262
|
-
|
|
268
|
+
type Story = StoryObj<typeof meta>;
|
|
269
|
+
|
|
270
|
+
export const JsonView: Story = {
|
|
263
271
|
render: () => {
|
|
264
272
|
const client = useClient();
|
|
265
273
|
const registry = useContext(RegistryContext);
|
|
266
274
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
267
|
-
const data =
|
|
275
|
+
const data = useAtomValue(graph.json());
|
|
268
276
|
|
|
269
277
|
return (
|
|
270
278
|
<>
|
|
@@ -275,31 +283,42 @@ export const JsonView = {
|
|
|
275
283
|
},
|
|
276
284
|
};
|
|
277
285
|
|
|
278
|
-
export const TreeView = {
|
|
286
|
+
export const TreeView: Story = {
|
|
279
287
|
render: () => {
|
|
280
288
|
const client = useClient();
|
|
281
289
|
const registry = useContext(RegistryContext);
|
|
282
290
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
283
|
-
const
|
|
291
|
+
const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
292
|
+
|
|
293
|
+
const getOrCreateState = useMemo(
|
|
294
|
+
() => (path: string) => {
|
|
295
|
+
let atom = stateRef.current.get(path);
|
|
296
|
+
if (!atom) {
|
|
297
|
+
atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
|
|
298
|
+
stateRef.current.set(path, atom);
|
|
299
|
+
}
|
|
300
|
+
return atom;
|
|
301
|
+
},
|
|
302
|
+
[],
|
|
303
|
+
);
|
|
284
304
|
|
|
285
305
|
const useItems = useCallback(
|
|
286
|
-
(node?: Node, options?: { disposition?: string; sort?: boolean }) => {
|
|
287
|
-
const connections =
|
|
306
|
+
(node?: Node.Node, options?: { disposition?: string; sort?: boolean }) => {
|
|
307
|
+
const connections = useAtomValue(graph.connections(node?.id ?? Node.RootId));
|
|
288
308
|
return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
|
|
289
309
|
},
|
|
290
310
|
[graph],
|
|
291
311
|
);
|
|
292
312
|
|
|
293
313
|
const getProps = useCallback(
|
|
294
|
-
(node: Node, path: string[]) => {
|
|
295
|
-
const children = graph
|
|
296
|
-
.getConnections(node.id, 'outbound')
|
|
314
|
+
(node: Node.Node, path: string[]) => {
|
|
315
|
+
const children = Graph.getConnections(graph, node.id, 'outbound')
|
|
297
316
|
.map((n) => {
|
|
298
317
|
// Break cycles.
|
|
299
318
|
const nextPath = [...path, node.id];
|
|
300
|
-
return nextPath.includes(n.id) ? undefined : (n as Node);
|
|
319
|
+
return nextPath.includes(n.id) ? undefined : (n as Node.Node);
|
|
301
320
|
})
|
|
302
|
-
.filter(isNonNullable) as Node[];
|
|
321
|
+
.filter(isNonNullable) as Node.Node[];
|
|
303
322
|
const parentOf =
|
|
304
323
|
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
305
324
|
return {
|
|
@@ -312,59 +331,54 @@ export const TreeView = {
|
|
|
312
331
|
[graph],
|
|
313
332
|
);
|
|
314
333
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
334
|
+
// Hook that subscribes to item state via Atom.
|
|
335
|
+
const useItemState = (_path: string[]) => {
|
|
336
|
+
const path = useMemo(() => Path.create(..._path), [_path.join('~')]);
|
|
337
|
+
const atom = getOrCreateState(path);
|
|
338
|
+
return useAtomValue(atom);
|
|
339
|
+
};
|
|
322
340
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
}
|
|
341
|
+
const useIsOpen = (_path: string[]) => {
|
|
342
|
+
return useItemState(_path).open;
|
|
343
|
+
};
|
|
335
344
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
);
|
|
345
|
+
const useIsCurrent = (_path: string[]) => {
|
|
346
|
+
return useItemState(_path).current;
|
|
347
|
+
};
|
|
340
348
|
|
|
341
349
|
const onOpenChange = useCallback(
|
|
342
350
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
343
351
|
const path = Path.create(..._path);
|
|
344
|
-
const
|
|
345
|
-
|
|
352
|
+
const atom = stateRef.current.get(path);
|
|
353
|
+
if (atom) {
|
|
354
|
+
const prev = registry.get(atom);
|
|
355
|
+
registry.set(atom, { ...prev, open });
|
|
356
|
+
}
|
|
346
357
|
},
|
|
347
|
-
[
|
|
358
|
+
[registry],
|
|
348
359
|
);
|
|
349
360
|
|
|
350
361
|
const onSelect = useCallback(
|
|
351
362
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
352
363
|
const path = Path.create(..._path);
|
|
353
|
-
const
|
|
354
|
-
|
|
364
|
+
const atom = stateRef.current.get(path);
|
|
365
|
+
if (atom) {
|
|
366
|
+
const prev = registry.get(atom);
|
|
367
|
+
registry.set(atom, { ...prev, current });
|
|
368
|
+
}
|
|
355
369
|
},
|
|
356
|
-
[
|
|
370
|
+
[registry],
|
|
357
371
|
);
|
|
358
372
|
|
|
359
373
|
return (
|
|
360
374
|
<>
|
|
361
375
|
<Controls />
|
|
362
376
|
<Tree
|
|
363
|
-
id={
|
|
377
|
+
id={Node.RootId}
|
|
364
378
|
useItems={useItems}
|
|
365
379
|
getProps={getProps}
|
|
366
|
-
|
|
367
|
-
|
|
380
|
+
useIsOpen={useIsOpen}
|
|
381
|
+
useIsCurrent={useIsCurrent}
|
|
368
382
|
onOpenChange={onOpenChange}
|
|
369
383
|
onSelect={onSelect}
|
|
370
384
|
/>
|
|
@@ -372,122 +386,3 @@ export const TreeView = {
|
|
|
372
386
|
);
|
|
373
387
|
},
|
|
374
388
|
};
|
|
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
|
-
|
|
384
|
-
const useItems = useCallback(
|
|
385
|
-
(node?: Node, options?: { disposition?: string; sort?: boolean }) => {
|
|
386
|
-
const connections = useRxValue(graph.connections(node?.id ?? ROOT_ID));
|
|
387
|
-
return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
|
|
388
|
-
},
|
|
389
|
-
[graph],
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
const getProps = useCallback(
|
|
393
|
-
(node: Node, path: string[]) => {
|
|
394
|
-
const children = graph
|
|
395
|
-
.getConnections(node.id, 'outbound')
|
|
396
|
-
.map((n) => {
|
|
397
|
-
// Break cycles.
|
|
398
|
-
const nextPath = [...path, node.id];
|
|
399
|
-
return nextPath.includes(n.id) ? undefined : (n as Node);
|
|
400
|
-
})
|
|
401
|
-
.filter(isNonNullable) as Node[];
|
|
402
|
-
const parentOf =
|
|
403
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
404
|
-
return {
|
|
405
|
-
id: node.id,
|
|
406
|
-
label: node.id,
|
|
407
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
408
|
-
parentOf,
|
|
409
|
-
};
|
|
410
|
-
},
|
|
411
|
-
[graph],
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const isOpen = useCallback(
|
|
415
|
-
(_path: string[]) => {
|
|
416
|
-
const path = Path.create(..._path);
|
|
417
|
-
const object = state.get(path) ?? live({ open: true, current: false });
|
|
418
|
-
if (!state.has(path)) {
|
|
419
|
-
state.set(path, object);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
return object.open;
|
|
423
|
-
},
|
|
424
|
-
[state],
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
const isCurrent = useCallback(
|
|
428
|
-
(_path: string[]) => {
|
|
429
|
-
const path = Path.create(..._path);
|
|
430
|
-
const object = state.get(path) ?? live({ open: false, current: false });
|
|
431
|
-
if (!state.has(path)) {
|
|
432
|
-
state.set(path, object);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return object.current;
|
|
436
|
-
},
|
|
437
|
-
[state],
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
const onOpenChange = useCallback(
|
|
441
|
-
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
442
|
-
const path = Path.create(..._path);
|
|
443
|
-
const object = state.get(path);
|
|
444
|
-
object!.open = open;
|
|
445
|
-
},
|
|
446
|
-
[state],
|
|
447
|
-
);
|
|
448
|
-
|
|
449
|
-
const onSelect = useCallback(
|
|
450
|
-
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
451
|
-
const path = Path.create(..._path);
|
|
452
|
-
const object = state.get(path);
|
|
453
|
-
object!.current = current;
|
|
454
|
-
},
|
|
455
|
-
[state],
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const spaces = useItems(graph.root);
|
|
459
|
-
|
|
460
|
-
return (
|
|
461
|
-
<>
|
|
462
|
-
<Controls />
|
|
463
|
-
<Tabs.Root defaultValue={spaces[0].id}>
|
|
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>
|
|
490
|
-
</>
|
|
491
|
-
);
|
|
492
|
-
},
|
|
493
|
-
};
|
package/src/stories/Tree.tsx
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import React, { type FC, type HTMLAttributes, useState } from 'react';
|
|
6
6
|
|
|
7
|
-
import { mx } from '@dxos/
|
|
7
|
+
import { mx } from '@dxos/ui-theme';
|
|
8
8
|
|
|
9
9
|
// TODO(burdon): Copied form devtools.
|
|
10
10
|
|
|
@@ -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 pli-2 border border-l-0 font-mono truncate', className)} {...props}>
|
|
76
76
|
{children}
|
|
77
77
|
</div>
|
|
78
78
|
);
|