@dxos/app-graph 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae
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 +1350 -686
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +1349 -686
- 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 +112 -66
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +187 -221
- 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 +42 -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/util.d.ts +24 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -34
- package/src/atoms.ts +25 -0
- package/src/graph-builder.test.ts +626 -119
- package/src/graph-builder.ts +667 -288
- package/src/graph.test.ts +429 -121
- package/src/graph.ts +1041 -403
- package/src/index.ts +9 -3
- package/src/node-matcher.test.ts +301 -0
- package/src/node-matcher.ts +282 -0
- package/src/node.ts +53 -8
- package/src/stories/EchoGraph.stories.tsx +158 -119
- package/src/stories/Tree.tsx +1 -1
- package/src/util.ts +55 -0
- 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,37 +2,29 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type Registry, RegistryContext,
|
|
5
|
+
import { Atom, type Registry, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
6
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
7
7
|
import * as Function from 'effect/Function';
|
|
8
8
|
import * as Option from 'effect/Option';
|
|
9
|
-
import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Query,
|
|
16
|
-
type QueryResult,
|
|
17
|
-
type Space,
|
|
18
|
-
SpaceState,
|
|
19
|
-
isSpace,
|
|
20
|
-
live,
|
|
21
|
-
} from '@dxos/client/echo';
|
|
22
|
-
import { Obj, Type } from '@dxos/echo';
|
|
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';
|
|
23
15
|
import { faker } from '@dxos/random';
|
|
24
16
|
import { type Client, useClient } from '@dxos/react-client';
|
|
25
17
|
import { withClientProvider } from '@dxos/react-client/testing';
|
|
26
18
|
import { Icon, IconButton, Input, Select } from '@dxos/react-ui';
|
|
27
19
|
import { withTheme } from '@dxos/react-ui/testing';
|
|
28
|
-
import { Path, Tree } from '@dxos/react-ui-list';
|
|
29
|
-
import { getSize, mx } from '@dxos/
|
|
30
|
-
import {
|
|
20
|
+
import { Path, Tree, type TreeModel } from '@dxos/react-ui-list';
|
|
21
|
+
import { getSize, mx } from '@dxos/ui-theme';
|
|
22
|
+
import { safeParseInt } from '@dxos/util';
|
|
31
23
|
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
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';
|
|
36
28
|
|
|
37
29
|
import { JsonTree } from './Tree';
|
|
38
30
|
|
|
@@ -56,43 +48,44 @@ const actionWeights = {
|
|
|
56
48
|
[Action.RENAME_OBJECT]: 4,
|
|
57
49
|
};
|
|
58
50
|
|
|
59
|
-
const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
|
|
60
|
-
const spaceBuilderExtension =
|
|
51
|
+
const createGraph = (client: Client, registry: Registry.Registry): Graph.ExpandableGraph => {
|
|
52
|
+
const spaceBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
61
53
|
id: 'space',
|
|
62
54
|
connector: (node) =>
|
|
63
|
-
|
|
55
|
+
Atom.make((get) =>
|
|
64
56
|
Function.pipe(
|
|
65
57
|
get(node),
|
|
66
|
-
Option.flatMap((node) => (node.id ===
|
|
58
|
+
Option.flatMap((node) => (node.id === Node.RootId ? Option.some(node) : Option.none())),
|
|
67
59
|
Option.map(() => {
|
|
68
|
-
const spaces = get(
|
|
60
|
+
const spaces = get(CreateAtom.fromObservable(client.spaces)) ?? [];
|
|
69
61
|
return spaces
|
|
70
|
-
.filter((space) => get(
|
|
71
|
-
.map((space) =>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
});
|
|
77
74
|
}),
|
|
78
75
|
Option.getOrElse(() => []),
|
|
79
76
|
),
|
|
80
77
|
),
|
|
81
78
|
});
|
|
82
79
|
|
|
83
|
-
const objectBuilderExtension =
|
|
80
|
+
const objectBuilderExtension = GraphBuilder.createExtensionRaw({
|
|
84
81
|
id: 'object',
|
|
85
82
|
connector: (node) => {
|
|
86
|
-
|
|
87
|
-
return Rx.make((get) =>
|
|
83
|
+
return Atom.make((get) =>
|
|
88
84
|
Function.pipe(
|
|
89
85
|
get(node),
|
|
90
86
|
Option.flatMap((node) => (isSpace(node.data) ? Option.some(node.data) : Option.none())),
|
|
91
87
|
Option.map((space) => {
|
|
92
|
-
|
|
93
|
-
query = space.db.query(Query.type(Expando, { type: 'test' }));
|
|
94
|
-
}
|
|
95
|
-
const objects = get(rxFromQuery(query));
|
|
88
|
+
const objects = get(AtomQuery.make(space.db, Query.type(TestSchema.Expando, { type: 'test' })));
|
|
96
89
|
return objects.map((object) => ({
|
|
97
90
|
id: object.id,
|
|
98
91
|
type: 'dxos.org/type/test',
|
|
@@ -106,15 +99,15 @@ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGra
|
|
|
106
99
|
},
|
|
107
100
|
});
|
|
108
101
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
102
|
+
const builder = GraphBuilder.make({ registry });
|
|
103
|
+
GraphBuilder.addExtension(builder, spaceBuilderExtension);
|
|
104
|
+
GraphBuilder.addExtension(builder, objectBuilderExtension);
|
|
105
|
+
const graph = builder.graph;
|
|
112
106
|
graph.onNodeChanged.on(({ id }) => {
|
|
113
|
-
|
|
107
|
+
Graph.expand(graph, id, 'child');
|
|
114
108
|
});
|
|
115
|
-
|
|
109
|
+
Graph.expand(graph, Node.RootId, 'child');
|
|
116
110
|
(window as any).graph = graph;
|
|
117
|
-
|
|
118
111
|
return graph;
|
|
119
112
|
};
|
|
120
113
|
|
|
@@ -134,9 +127,9 @@ const getRandomSpace = (client: Client): Space | undefined => {
|
|
|
134
127
|
const getSpaceWithObjects = async (client: Client): Promise<Space | undefined> => {
|
|
135
128
|
const readySpaces = client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY);
|
|
136
129
|
const spaceQueries = await Promise.all(
|
|
137
|
-
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()),
|
|
138
131
|
);
|
|
139
|
-
const spaces = readySpaces.filter((space, index) => spaceQueries[index].
|
|
132
|
+
const spaces = readySpaces.filter((space, index) => spaceQueries[index].length > 0);
|
|
140
133
|
return spaces[Math.floor(Math.random() * spaces.length)];
|
|
141
134
|
};
|
|
142
135
|
|
|
@@ -153,19 +146,26 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
153
146
|
case Action.RENAME_SPACE: {
|
|
154
147
|
const space = getRandomSpace(client);
|
|
155
148
|
if (space) {
|
|
156
|
-
space.properties
|
|
149
|
+
Obj.change(space.properties, (p) => {
|
|
150
|
+
p.name = faker.commerce.productName();
|
|
151
|
+
});
|
|
157
152
|
}
|
|
158
153
|
break;
|
|
159
154
|
}
|
|
160
155
|
|
|
161
156
|
case Action.ADD_OBJECT:
|
|
162
|
-
getRandomSpace(client)?.db.add(
|
|
157
|
+
getRandomSpace(client)?.db.add(
|
|
158
|
+
Obj.make(TestSchema.Expando, {
|
|
159
|
+
type: 'test',
|
|
160
|
+
name: faker.commerce.productName(),
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
163
|
break;
|
|
164
164
|
|
|
165
165
|
case Action.REMOVE_OBJECT: {
|
|
166
166
|
const space = await getSpaceWithObjects(client);
|
|
167
167
|
if (space) {
|
|
168
|
-
const
|
|
168
|
+
const objects = await space.db.query(Filter.type(TestSchema.Expando, { type: 'test' })).run();
|
|
169
169
|
space.db.remove(objects[Math.floor(Math.random() * objects.length)]);
|
|
170
170
|
}
|
|
171
171
|
break;
|
|
@@ -174,8 +174,11 @@ const runAction = async (client: Client, action: Action) => {
|
|
|
174
174
|
case Action.RENAME_OBJECT: {
|
|
175
175
|
const space = await getSpaceWithObjects(client);
|
|
176
176
|
if (space) {
|
|
177
|
-
const
|
|
178
|
-
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
|
+
});
|
|
179
182
|
}
|
|
180
183
|
break;
|
|
181
184
|
}
|
|
@@ -214,13 +217,13 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
214
217
|
<Input.TextInput
|
|
215
218
|
autoComplete='off'
|
|
216
219
|
size={5}
|
|
217
|
-
classNames='w-[100px] text-right
|
|
220
|
+
classNames='w-[100px] text-right pe-[22px]'
|
|
218
221
|
placeholder='Interval'
|
|
219
222
|
value={actionInterval}
|
|
220
223
|
onChange={({ target: { value } }) => setActionInterval(value)}
|
|
221
224
|
/>
|
|
222
225
|
</Input.Root>
|
|
223
|
-
<Icon icon='ph--timer--regular' classNames={mx('absolute
|
|
226
|
+
<Icon icon='ph--timer--regular' classNames={mx('absolute right-1 top-1 mt-[6px]', getSize(3))} />
|
|
224
227
|
</div>
|
|
225
228
|
<IconButton icon='ph--plus--regular' label='Add' onClick={() => action && runAction(client, action)} />
|
|
226
229
|
<Select.Root value={action?.toString()} onValueChange={(action) => setAction(action as unknown as Action)}>
|
|
@@ -249,16 +252,17 @@ const Controls = ({ children }: PropsWithChildren) => {
|
|
|
249
252
|
const meta = {
|
|
250
253
|
title: 'sdk/app-graph/EchoGraph',
|
|
251
254
|
decorators: [
|
|
252
|
-
withTheme,
|
|
255
|
+
withTheme(),
|
|
253
256
|
withClientProvider({
|
|
254
257
|
createIdentity: true,
|
|
258
|
+
types: [TestSchema.Expando],
|
|
255
259
|
onCreateIdentity: async ({ client }) => {
|
|
256
260
|
await client.spaces.create();
|
|
257
261
|
await client.spaces.create();
|
|
258
262
|
},
|
|
259
263
|
}),
|
|
260
264
|
],
|
|
261
|
-
} satisfies Meta
|
|
265
|
+
} satisfies Meta;
|
|
262
266
|
|
|
263
267
|
export default meta;
|
|
264
268
|
|
|
@@ -269,7 +273,7 @@ export const JsonView: Story = {
|
|
|
269
273
|
const client = useClient();
|
|
270
274
|
const registry = useContext(RegistryContext);
|
|
271
275
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
272
|
-
const data =
|
|
276
|
+
const data = useAtomValue(graph.json());
|
|
273
277
|
|
|
274
278
|
return (
|
|
275
279
|
<>
|
|
@@ -285,94 +289,129 @@ export const TreeView: Story = {
|
|
|
285
289
|
const client = useClient();
|
|
286
290
|
const registry = useContext(RegistryContext);
|
|
287
291
|
const graph = useMemo(() => createGraph(client, registry), [client, registry]);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
(
|
|
292
|
-
|
|
293
|
-
|
|
292
|
+
const stateRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
|
|
293
|
+
|
|
294
|
+
const getOrCreateState = useMemo(
|
|
295
|
+
() => (pathKey: string) => {
|
|
296
|
+
let atom = stateRef.current.get(pathKey);
|
|
297
|
+
if (!atom) {
|
|
298
|
+
atom = Atom.make({ open: true, current: false }).pipe(Atom.keepAlive);
|
|
299
|
+
stateRef.current.set(pathKey, atom);
|
|
300
|
+
}
|
|
301
|
+
return atom;
|
|
294
302
|
},
|
|
303
|
+
[],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const childIdsFamily = useMemo(
|
|
307
|
+
() =>
|
|
308
|
+
Atom.family((id: string) =>
|
|
309
|
+
Atom.make((get) => {
|
|
310
|
+
const connections = get(graph.connections(id, 'child'));
|
|
311
|
+
return connections.map((connection) => connection.id);
|
|
312
|
+
}),
|
|
313
|
+
),
|
|
295
314
|
[graph],
|
|
296
315
|
);
|
|
297
316
|
|
|
298
|
-
const
|
|
299
|
-
(
|
|
300
|
-
|
|
301
|
-
.
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})
|
|
307
|
-
.filter(isNonNullable) as Node[];
|
|
308
|
-
const parentOf =
|
|
309
|
-
children.length > 0 ? children.map(({ id }) => id) : node.properties.role === 'branch' ? [] : undefined;
|
|
310
|
-
return {
|
|
311
|
-
id: node.id,
|
|
312
|
-
label: node.id,
|
|
313
|
-
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
314
|
-
parentOf,
|
|
315
|
-
};
|
|
316
|
-
},
|
|
317
|
+
const itemFamily = useMemo(
|
|
318
|
+
() =>
|
|
319
|
+
Atom.family((id: string) =>
|
|
320
|
+
Atom.make((get) => {
|
|
321
|
+
const node = get(graph.node(id));
|
|
322
|
+
return Option.isSome(node) ? node.value : undefined;
|
|
323
|
+
}),
|
|
324
|
+
),
|
|
317
325
|
[graph],
|
|
318
326
|
);
|
|
319
327
|
|
|
320
|
-
const
|
|
321
|
-
(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
328
|
+
const itemPropsFamily = useMemo(
|
|
329
|
+
() =>
|
|
330
|
+
Atom.family((pathKey: string) => {
|
|
331
|
+
const path = pathKey.split('~');
|
|
332
|
+
const id = path[path.length - 1];
|
|
333
|
+
return Atom.make((get) => {
|
|
334
|
+
const nodeOpt = get(graph.node(id));
|
|
335
|
+
const node = Option.isSome(nodeOpt) ? nodeOpt.value : undefined;
|
|
336
|
+
if (!node) {
|
|
337
|
+
return { id, label: id };
|
|
338
|
+
}
|
|
339
|
+
const connections = get(graph.connections(node.id, 'child'));
|
|
340
|
+
const safeChildren = connections.filter((n) => !path.includes(n.id));
|
|
341
|
+
const parentOf =
|
|
342
|
+
safeChildren.length > 0
|
|
343
|
+
? safeChildren.map(({ id }) => id)
|
|
344
|
+
: node.properties.role === 'branch'
|
|
345
|
+
? []
|
|
346
|
+
: undefined;
|
|
347
|
+
return {
|
|
348
|
+
id: node.id,
|
|
349
|
+
label: node.id,
|
|
350
|
+
icon: node.type === 'dxos.org/type/Space' ? 'ph--planet--regular' : 'ph--placeholder--regular',
|
|
351
|
+
parentOf,
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
}),
|
|
355
|
+
[graph],
|
|
356
|
+
);
|
|
327
357
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
358
|
+
const itemOpenFamily = useMemo(
|
|
359
|
+
() =>
|
|
360
|
+
Atom.family((pathKey: string) => {
|
|
361
|
+
const stateAtom = getOrCreateState(pathKey);
|
|
362
|
+
return Atom.make((get) => get(stateAtom).open);
|
|
363
|
+
}),
|
|
364
|
+
[getOrCreateState],
|
|
331
365
|
);
|
|
332
366
|
|
|
333
|
-
const
|
|
334
|
-
(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
367
|
+
const itemCurrentFamily = useMemo(
|
|
368
|
+
() =>
|
|
369
|
+
Atom.family((pathKey: string) => {
|
|
370
|
+
const stateAtom = getOrCreateState(pathKey);
|
|
371
|
+
return Atom.make((get) => get(stateAtom).current);
|
|
372
|
+
}),
|
|
373
|
+
[getOrCreateState],
|
|
374
|
+
);
|
|
340
375
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
376
|
+
const model: TreeModel<Node.Node> = useMemo(
|
|
377
|
+
() => ({
|
|
378
|
+
childIds: (parentId?: string) => childIdsFamily(parentId ?? Node.RootId),
|
|
379
|
+
item: (id: string) => itemFamily(id),
|
|
380
|
+
itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
|
|
381
|
+
itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
|
|
382
|
+
itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
|
|
383
|
+
}),
|
|
384
|
+
[childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
|
|
344
385
|
);
|
|
345
386
|
|
|
346
387
|
const onOpenChange = useCallback(
|
|
347
388
|
({ path: _path, open }: { path: string[]; open: boolean }) => {
|
|
348
389
|
const path = Path.create(..._path);
|
|
349
|
-
const
|
|
350
|
-
|
|
390
|
+
const atom = stateRef.current.get(path);
|
|
391
|
+
if (atom) {
|
|
392
|
+
const prev = registry.get(atom);
|
|
393
|
+
registry.set(atom, { ...prev, open });
|
|
394
|
+
}
|
|
351
395
|
},
|
|
352
|
-
[
|
|
396
|
+
[registry],
|
|
353
397
|
);
|
|
354
398
|
|
|
355
399
|
const onSelect = useCallback(
|
|
356
400
|
({ path: _path, current }: { path: string[]; current: boolean }) => {
|
|
357
401
|
const path = Path.create(..._path);
|
|
358
|
-
const
|
|
359
|
-
|
|
402
|
+
const atom = stateRef.current.get(path);
|
|
403
|
+
if (atom) {
|
|
404
|
+
const prev = registry.get(atom);
|
|
405
|
+
registry.set(atom, { ...prev, current });
|
|
406
|
+
}
|
|
360
407
|
},
|
|
361
|
-
[
|
|
408
|
+
[registry],
|
|
362
409
|
);
|
|
363
410
|
|
|
364
411
|
return (
|
|
365
412
|
<>
|
|
366
413
|
<Controls />
|
|
367
|
-
<Tree
|
|
368
|
-
id={ROOT_ID}
|
|
369
|
-
useItems={useItems}
|
|
370
|
-
getProps={getProps}
|
|
371
|
-
isOpen={isOpen}
|
|
372
|
-
isCurrent={isCurrent}
|
|
373
|
-
onOpenChange={onOpenChange}
|
|
374
|
-
onSelect={onSelect}
|
|
375
|
-
/>
|
|
414
|
+
<Tree model={model} id={Node.RootId} onOpenChange={onOpenChange} onSelect={onSelect} />
|
|
376
415
|
</>
|
|
377
416
|
);
|
|
378
417
|
},
|
package/src/stories/Tree.tsx
CHANGED
package/src/util.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Node from './node';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Key separators for compound string keys used across the app-graph package.
|
|
9
|
+
* `primary` separates top-level components (e.g., node ID from relation).
|
|
10
|
+
* `secondary` separates sub-components within an encoded value (e.g., relation kind from direction).
|
|
11
|
+
* Two distinct characters are needed because secondary separators appear inside primary-separated fields.
|
|
12
|
+
*/
|
|
13
|
+
export const Separators = {
|
|
14
|
+
primary: '\u0001',
|
|
15
|
+
secondary: '\u0002',
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalize a relation input to a full Relation object.
|
|
20
|
+
*/
|
|
21
|
+
export const normalizeRelation = (relation?: Node.RelationInput): Node.Relation =>
|
|
22
|
+
relation == null ? Node.childRelation() : typeof relation === 'string' ? Node.relation(relation) : relation;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Shallow-compare two values: same reference, or same own-keys with === values.
|
|
26
|
+
*/
|
|
27
|
+
export const shallowEqual = (a: unknown, b: unknown): boolean => {
|
|
28
|
+
if (a === b) return true;
|
|
29
|
+
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
30
|
+
const keysA = Object.keys(a as Record<string, unknown>);
|
|
31
|
+
const keysB = Object.keys(b as Record<string, unknown>);
|
|
32
|
+
if (keysA.length !== keysB.length) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return keysA.every((k) => (a as Record<string, unknown>)[k] === (b as Record<string, unknown>)[k]);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns true if two NodeArg arrays are semantically identical (same id, type, data, properties per index).
|
|
40
|
+
*/
|
|
41
|
+
export const nodeArgsUnchanged = (prev: Node.NodeArg<any>[], next: Node.NodeArg<any>[]): boolean => {
|
|
42
|
+
if (prev.length !== next.length) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return prev.every((prevNode, idx) => {
|
|
47
|
+
const nextNode = next[idx];
|
|
48
|
+
return (
|
|
49
|
+
prevNode.id === nextNode.id &&
|
|
50
|
+
prevNode.type === nextNode.type &&
|
|
51
|
+
shallowEqual(prevNode.data, nextNode.data) &&
|
|
52
|
+
shallowEqual(prevNode.properties, nextNode.properties)
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
type Cb = () => void;
|
|
2
|
-
interface Query {
|
|
3
|
-
id?: string[];
|
|
4
|
-
typename?: string[];
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Lazy query result that can be executed.
|
|
8
|
-
* Does not run without the run function being called.
|
|
9
|
-
*/
|
|
10
|
-
interface QueryResult<T> {
|
|
11
|
-
/**
|
|
12
|
-
* Execute the query and subscribe to the result.
|
|
13
|
-
* @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
|
|
14
|
-
* @param error Called on error. `next` is never called after that.
|
|
15
|
-
* @returns Function to dispose the query and unsubscribe.
|
|
16
|
-
*/
|
|
17
|
-
run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
|
|
18
|
-
}
|
|
19
|
-
declare const QueryResult: Readonly<{
|
|
20
|
-
fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>) => QueryResult<T>;
|
|
21
|
-
}>;
|
|
22
|
-
interface _Resolver<T> {
|
|
23
|
-
query(query: Query): QueryResult<T>;
|
|
24
|
-
}
|
|
25
|
-
//# sourceMappingURL=graph-projections.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"graph-projections.test.d.ts","sourceRoot":"","sources":["../../../../src/experimental/graph-projections.test.ts"],"names":[],"mappings":"AAIA,KAAK,EAAE,GAAG,MAAM,IAAI,CAAC;AAErB,UAAU,KAAK;IACb,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;GAGG;AACH,UAAU,WAAW,CAAC,CAAC;IACrB;;;;;OAKG;IACH,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,KAAK,IAAI,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,EAAE,CAAC;CACvE;AAED,QAAA,MAAM,WAAW;kBACD,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,EAAE,CAAC,KAAG,WAAW,CAAC,CAAC,CAAC;EAyBpF,CAAC;AAEH,UAAU,SAAS,CAAC,CAAC;IACnB,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC;CACrC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"signals-integration.test.d.ts","sourceRoot":"","sources":["../../../src/signals-integration.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { Rx } from '@effect-rx/rx-react';
|
|
2
|
-
import { type AnyEchoObject } from '@dxos/echo/internal';
|
|
3
|
-
import { type QueryResult } from '@dxos/echo-db';
|
|
4
|
-
export declare const rxFromQuery: <T extends AnyEchoObject>(query: QueryResult<T>) => Rx.Rx<T[]>;
|
|
5
|
-
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../src/testing.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,EAAE,EAAE,MAAM,qBAAqB,CAAC;AAEzC,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,aAAa,EAAE,OAAO,WAAW,CAAC,CAAC,CAAC,KAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAUrF,CAAC"}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
type Cb = () => void;
|
|
6
|
-
|
|
7
|
-
interface Query {
|
|
8
|
-
id?: string[];
|
|
9
|
-
typename?: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Lazy query result that can be executed.
|
|
14
|
-
* Does not run without the run function being called.
|
|
15
|
-
*/
|
|
16
|
-
interface QueryResult<T> {
|
|
17
|
-
/**
|
|
18
|
-
* Execute the query and subscribe to the result.
|
|
19
|
-
* @param next Called at least once with the first value (maybe synchronously) and then for every subsequent update.
|
|
20
|
-
* @param error Called on error. `next` is never called after that.
|
|
21
|
-
* @returns Function to dispose the query and unsubscribe.
|
|
22
|
-
*/
|
|
23
|
-
run(onData: (value?: T[]) => void, onError: (err: Error) => void): Cb;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const QueryResult = Object.freeze({
|
|
27
|
-
fromPromise: <T>(run: (onDispose: (cb: Cb) => void) => Promise<T[]>): QueryResult<T> => {
|
|
28
|
-
return {
|
|
29
|
-
run: (onData, onError) => {
|
|
30
|
-
const cbs: Cb[] = [];
|
|
31
|
-
let disposed = false;
|
|
32
|
-
const dispose = () => {
|
|
33
|
-
cbs.forEach((cb) => cb());
|
|
34
|
-
disposed = true;
|
|
35
|
-
};
|
|
36
|
-
run((cb) => (disposed ? cb() : cbs.push(cb))).then(
|
|
37
|
-
(data) => {
|
|
38
|
-
if (disposed) {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
onData(data);
|
|
42
|
-
},
|
|
43
|
-
(err) => {
|
|
44
|
-
dispose();
|
|
45
|
-
onError(err);
|
|
46
|
-
},
|
|
47
|
-
);
|
|
48
|
-
return dispose;
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
interface _Resolver<T> {
|
|
55
|
-
query(query: Query): QueryResult<T>;
|
|
56
|
-
}
|