@dxos/app-graph 0.8.3-main.7f5a14c → 0.8.3

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.
@@ -4,10 +4,10 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { Rx, useRxValue } from '@effect-rx/rx-react';
7
+ import { type Registry, RegistryContext, Rx, useRxValue } from '@effect-rx/rx-react';
8
8
  import { Pause, Play, Plus, Timer } from '@phosphor-icons/react';
9
9
  import { Option, pipe } from 'effect';
10
- import React, { useEffect, useMemo, useState } from 'react';
10
+ import React, { type PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react';
11
11
 
12
12
  import {
13
13
  live,
@@ -20,17 +20,21 @@ import {
20
20
  type Live,
21
21
  Filter,
22
22
  } from '@dxos/client/echo';
23
+ import { Obj, Type } from '@dxos/echo';
23
24
  import { faker } from '@dxos/random';
24
25
  import { type Client, useClient } from '@dxos/react-client';
25
26
  import { withClientProvider } from '@dxos/react-client/testing';
26
27
  import { Button, Input, Select } from '@dxos/react-ui';
28
+ import { Path, Tree } from '@dxos/react-ui-list';
29
+ import { Tabs } from '@dxos/react-ui-tabs';
27
30
  import { getSize, mx } from '@dxos/react-ui-theme';
28
31
  import { withTheme } from '@dxos/storybook-utils';
29
- import { safeParseInt } from '@dxos/util';
32
+ import { byPosition, isNonNullable, safeParseInt } from '@dxos/util';
30
33
 
31
- import { Tree } from './Tree';
34
+ import { JsonTree } from './Tree';
32
35
  import { type ExpandableGraph, ROOT_ID } from '../graph';
33
36
  import { GraphBuilder, createExtension, rxFromObservable, rxFromSignal } from '../graph-builder';
37
+ import { type Node } from '../node';
34
38
  import { rxFromQuery } from '../testing';
35
39
 
36
40
  const DEFAULT_PERIOD = 500;
@@ -53,7 +57,7 @@ const actionWeights = {
53
57
  [Action.RENAME_OBJECT]: 4,
54
58
  };
55
59
 
56
- const createGraph = (client: Client): ExpandableGraph => {
60
+ const createGraph = (client: Client, registry: Registry.Registry): ExpandableGraph => {
57
61
  const spaceBuilderExtension = createExtension({
58
62
  id: 'space',
59
63
  connector: (node) =>
@@ -89,7 +93,8 @@ const createGraph = (client: Client): ExpandableGraph => {
89
93
  if (!query) {
90
94
  query = space.db.query(Query.type(Expando, { type: 'test' }));
91
95
  }
92
- return get(rxFromQuery(query)).map((object) => ({
96
+ const objects = get(rxFromQuery(query));
97
+ return objects.map((object) => ({
93
98
  id: object.id,
94
99
  type: 'dxos.org/type/test',
95
100
  properties: { label: object.name },
@@ -102,12 +107,14 @@ const createGraph = (client: Client): ExpandableGraph => {
102
107
  },
103
108
  });
104
109
 
105
- const graph = new GraphBuilder().addExtension(spaceBuilderExtension).addExtension(objectBuilderExtension).graph;
110
+ const graph = new GraphBuilder({ registry })
111
+ .addExtension(spaceBuilderExtension)
112
+ .addExtension(objectBuilderExtension).graph;
106
113
  graph.onNodeChanged.on(({ id }) => {
107
- console.log('onNodeChanged', { id });
108
114
  graph.expand(id);
109
115
  });
110
116
  graph.expand(ROOT_ID);
117
+ (window as any).graph = graph;
111
118
 
112
119
  return graph;
113
120
  };
@@ -153,7 +160,7 @@ const runAction = async (client: Client, action: Action) => {
153
160
  }
154
161
 
155
162
  case Action.ADD_OBJECT:
156
- getRandomSpace(client)?.db.add(live({ type: 'test', name: faker.commerce.productName() }));
163
+ getRandomSpace(client)?.db.add(Obj.make(Type.Expando, { type: 'test', name: faker.commerce.productName() }));
157
164
  break;
158
165
 
159
166
  case Action.REMOVE_OBJECT: {
@@ -176,14 +183,12 @@ const runAction = async (client: Client, action: Action) => {
176
183
  }
177
184
  };
178
185
 
179
- const DefaultStory = () => {
186
+ const Controls = ({ children }: PropsWithChildren) => {
180
187
  const [generating, setGenerating] = useState(false);
181
188
  const [actionInterval, setActionInterval] = useState(String(DEFAULT_PERIOD));
182
189
  const [action, setAction] = useState<Action>();
183
190
 
184
191
  const client = useClient();
185
- const graph = useMemo(() => createGraph(client), [client]);
186
- const data = useRxValue(graph.json());
187
192
 
188
193
  useEffect(() => {
189
194
  if (!generating) {
@@ -235,14 +240,13 @@ const DefaultStory = () => {
235
240
  </Select.Portal>
236
241
  </Select.Root>
237
242
  </div>
238
- {data && <Tree data={data} />}
243
+ {children}
239
244
  </>
240
245
  );
241
246
  };
242
247
 
243
248
  export default {
244
249
  title: 'sdk/app-graph/EchoGraph',
245
- render: DefaultStory,
246
250
  decorators: [
247
251
  withTheme,
248
252
  withClientProvider({
@@ -255,4 +259,235 @@ export default {
255
259
  ],
256
260
  };
257
261
 
258
- export const Default = {};
262
+ export const JsonView = {
263
+ render: () => {
264
+ const client = useClient();
265
+ const registry = useContext(RegistryContext);
266
+ const graph = useMemo(() => createGraph(client, registry), [client, registry]);
267
+ const data = useRxValue(graph.json());
268
+
269
+ return (
270
+ <>
271
+ <Controls />
272
+ {data && <JsonTree data={data} />}
273
+ </>
274
+ );
275
+ },
276
+ };
277
+
278
+ export const TreeView = {
279
+ render: () => {
280
+ const client = useClient();
281
+ const registry = useContext(RegistryContext);
282
+ const graph = useMemo(() => createGraph(client, registry), [client, registry]);
283
+ const state = useMemo(() => new Map<string, Live<{ open: boolean; current: boolean }>>(), []);
284
+
285
+ const useItems = useCallback(
286
+ (node?: Node, options?: { disposition?: string; sort?: boolean }) => {
287
+ const connections = useRxValue(graph.connections(node?.id ?? ROOT_ID));
288
+ return options?.sort ? connections.toSorted((a, b) => byPosition(a.properties, b.properties)) : connections;
289
+ },
290
+ [graph],
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);
321
+ }
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;
355
+ },
356
+ [state],
357
+ );
358
+
359
+ return (
360
+ <>
361
+ <Controls />
362
+ <Tree
363
+ id={ROOT_ID}
364
+ useItems={useItems}
365
+ getProps={getProps}
366
+ isOpen={isOpen}
367
+ isCurrent={isCurrent}
368
+ onOpenChange={onOpenChange}
369
+ onSelect={onSelect}
370
+ />
371
+ </>
372
+ );
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
+
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
+ };
@@ -8,7 +8,7 @@ import { mx } from '@dxos/react-ui-theme';
8
8
 
9
9
  // TODO(burdon): Copied form devtools.
10
10
 
11
- export const Tree: FC<{ data?: object }> = ({ data }) => {
11
+ export const JsonTree: FC<{ data?: object }> = ({ data }) => {
12
12
  return (
13
13
  <div className='flex overflow-auto ml-2 border-l-4 border-blue-500'>
14
14
  <Node data={data} root />