@dxos/app-graph 0.4.7-main.d28a540 → 0.4.7-main.e015b9e
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 +229 -289
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing.mjs +98 -0
- package/dist/lib/browser/testing.mjs.map +7 -0
- package/dist/lib/node/index.cjs +239 -282
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/testing.cjs +122 -0
- package/dist/lib/node/testing.cjs.map +7 -0
- package/dist/types/src/action.d.ts +68 -0
- package/dist/types/src/action.d.ts.map +1 -0
- package/dist/types/src/graph-builder.d.ts +16 -8
- package/dist/types/src/graph-builder.d.ts.map +1 -1
- package/dist/types/src/graph.d.ts +14 -71
- package/dist/types/src/graph.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/node.d.ts +47 -57
- 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.d.ts +52 -0
- package/dist/types/src/testing.d.ts.map +1 -0
- package/package.json +29 -10
- package/src/action.ts +92 -0
- package/src/graph-builder.ts +206 -18
- package/src/graph.test.ts +146 -242
- package/src/graph.ts +48 -241
- package/src/index.ts +1 -1
- package/src/node.ts +66 -78
- package/src/stories/EchoGraph.stories.tsx +27 -59
- package/src/testing.ts +152 -0
- package/dist/types/src/helpers.d.ts +0 -14
- package/dist/types/src/helpers.d.ts.map +0 -1
- package/src/helpers.ts +0 -27
package/dist/types/src/node.d.ts
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { IconProps } from '@phosphor-icons/react';
|
|
2
|
+
import { type FC } from 'react';
|
|
3
|
+
import type { UnsubscribeCallback } from '@dxos/async';
|
|
4
|
+
import { type ActionArg, type Action, type Label } from './action';
|
|
5
|
+
/**
|
|
6
|
+
* Called when a node is added to the graph, allowing other node builders to add children, actions or properties.
|
|
7
|
+
*/
|
|
8
|
+
export type NodeBuilder = (parent: Node) => UnsubscribeCallback | void;
|
|
2
9
|
/**
|
|
3
10
|
* Represents a node in the graph.
|
|
4
11
|
*/
|
|
5
|
-
export type
|
|
12
|
+
export type Node<TData = any, TProperties extends Record<string, any> = Record<string, any>> = {
|
|
6
13
|
/**
|
|
7
14
|
* Globally unique ID.
|
|
8
15
|
*/
|
|
9
16
|
id: string;
|
|
17
|
+
/**
|
|
18
|
+
* Parent node in the graph.
|
|
19
|
+
*/
|
|
20
|
+
parent: Node | null;
|
|
21
|
+
/**
|
|
22
|
+
* Label to be used when displaying the node.
|
|
23
|
+
* For default labels, use a translated string.
|
|
24
|
+
*
|
|
25
|
+
* @example 'My Node'
|
|
26
|
+
* @example ['unknown node label, { ns: 'example-plugin' }]
|
|
27
|
+
*/
|
|
28
|
+
label: Label;
|
|
29
|
+
/**
|
|
30
|
+
* Description to be used when displaying a detailed view of the node.
|
|
31
|
+
* For default descriptions, use a translated string.
|
|
32
|
+
*/
|
|
33
|
+
description?: Label;
|
|
34
|
+
/**
|
|
35
|
+
* Icon to be used when displaying the node.
|
|
36
|
+
*/
|
|
37
|
+
icon?: FC<IconProps>;
|
|
10
38
|
/**
|
|
11
39
|
* Properties of the node relevant to displaying the node.
|
|
12
40
|
*/
|
|
@@ -15,69 +43,31 @@ export type NodeBase<TData = any, TProperties extends Record<string, any> = Reco
|
|
|
15
43
|
* Data the node represents.
|
|
16
44
|
*/
|
|
17
45
|
data: TData;
|
|
18
|
-
};
|
|
19
|
-
export type NodeFilter<T = any, U extends Record<string, any> = Record<string, any>> = (node: Node<unknown, Record<string, any>>, connectedNode: Node) => node is Node<T, U>;
|
|
20
|
-
export type EdgeDirection = 'outbound' | 'inbound';
|
|
21
|
-
export type ConnectedNodes = {
|
|
22
46
|
/**
|
|
23
|
-
*
|
|
47
|
+
* Children of the node stored by their id.
|
|
24
48
|
*/
|
|
25
|
-
|
|
26
|
-
direction?: EdgeDirection;
|
|
27
|
-
}): Readonly<string[]>;
|
|
49
|
+
childrenMap: Record<string, Node>;
|
|
28
50
|
/**
|
|
29
|
-
*
|
|
51
|
+
* Actions of the node stored by their id.
|
|
30
52
|
*/
|
|
31
|
-
|
|
32
|
-
direction?: EdgeDirection;
|
|
33
|
-
filter?: NodeFilter<T, U>;
|
|
34
|
-
}): Node<T>[];
|
|
53
|
+
actionsMap: Record<string, Action>;
|
|
35
54
|
/**
|
|
36
|
-
*
|
|
55
|
+
* Children of the node in default order.
|
|
37
56
|
*/
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
export type ConnectedActions = {
|
|
57
|
+
get children(): Node[];
|
|
41
58
|
/**
|
|
42
|
-
* Actions
|
|
59
|
+
* Actions of the node in default order.
|
|
43
60
|
*/
|
|
44
|
-
actions():
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
} & ConnectedNodes & ConnectedActions>;
|
|
52
|
-
export type NodeArg<TData, TProperties extends Record<string, any> = Record<string, any>> = MakeOptional<NodeBase<TData, TProperties>, 'data' | 'properties'> & {
|
|
53
|
-
/** Will automatically add nodes with an edge from this node to each. */
|
|
54
|
-
nodes?: NodeArg<unknown>[];
|
|
55
|
-
/** Will automatically add specified edges. */
|
|
56
|
-
edges?: [string, EdgeDirection][];
|
|
61
|
+
get actions(): Action[];
|
|
62
|
+
addProperty(key: string, value: any): void;
|
|
63
|
+
removeProperty(key: string): void;
|
|
64
|
+
addNode<TChildData = null, TChildProperties extends Record<string, any> = Record<string, any>>(id: string, ...node: NodeArg<TChildData, TChildProperties>[]): Node<TChildData, TChildProperties>[];
|
|
65
|
+
removeNode(id: string): Node;
|
|
66
|
+
addAction<TActionProperties extends Record<string, any> = Record<string, any>>(...action: ActionArg<TActionProperties>[]): Action<TActionProperties>[];
|
|
67
|
+
removeAction(id: string): Action;
|
|
57
68
|
};
|
|
58
|
-
export type
|
|
59
|
-
|
|
60
|
-
node: Node;
|
|
61
|
-
caller?: string;
|
|
69
|
+
export type NodeArg<TData = null, TProperties extends Record<string, any> = Record<string, any>> = Pick<Node, 'id' | 'label'> & Partial<Omit<Node<TData, TProperties>, 'id' | 'label' | 'actions'>> & {
|
|
70
|
+
actions?: ActionArg[];
|
|
62
71
|
};
|
|
63
|
-
export
|
|
64
|
-
export type Action<TProperties extends Record<string, any> = Record<string, any>> = Readonly<Omit<NodeBase<ActionData, TProperties>, 'properties'> & {
|
|
65
|
-
properties: Readonly<TProperties>;
|
|
66
|
-
} & ConnectedNodes>;
|
|
67
|
-
export declare const isAction: (data: unknown) => data is Readonly<Omit<NodeBase<ActionData, Record<string, any>>, "properties"> & {
|
|
68
|
-
properties: Readonly<Record<string, any>>;
|
|
69
|
-
} & ConnectedNodes>;
|
|
70
|
-
export declare const actionGroupSymbol: unique symbol;
|
|
71
|
-
export type ActionGroup = Readonly<Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, 'properties'> & {
|
|
72
|
-
properties: Readonly<Record<string, any>>;
|
|
73
|
-
} & ConnectedActions>;
|
|
74
|
-
export declare const isActionGroup: (data: unknown) => data is Readonly<Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, "properties"> & {
|
|
75
|
-
properties: Readonly<Record<string, any>>;
|
|
76
|
-
} & ConnectedActions>;
|
|
77
|
-
export type ActionLike = Action | ActionGroup;
|
|
78
|
-
export declare const isActionLike: (data: unknown) => data is Readonly<Omit<NodeBase<ActionData, Record<string, any>>, "properties"> & {
|
|
79
|
-
properties: Readonly<Record<string, any>>;
|
|
80
|
-
} & ConnectedNodes> | Readonly<Omit<NodeBase<typeof actionGroupSymbol, Record<string, any>>, "properties"> & {
|
|
81
|
-
properties: Readonly<Record<string, any>>;
|
|
82
|
-
} & ConnectedActions>;
|
|
72
|
+
export declare const isGraphNode: (data: unknown) => data is Node<any, Record<string, any>>;
|
|
83
73
|
//# sourceMappingURL=node.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/node.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../../src/node.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEhC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAEnE;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,IAAI,KAAK,mBAAmB,GAAG,IAAI,CAAC;AAEvE;;GAEG;AACH,MAAM,MAAM,IAAI,CAAC,KAAK,GAAG,GAAG,EAAE,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI;IAC7F;;OAEG;IACH,EAAE,EAAE,MAAM,CAAC;IAEX;;OAEG;IACH,MAAM,EAAE,IAAI,GAAG,IAAI,CAAC;IAEpB;;;;;;OAMG;IACH,KAAK,EAAE,KAAK,CAAC;IAEb;;;OAGG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC;IAEpB;;OAEG;IACH,IAAI,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC;IAErB;;OAEG;IAEH,UAAU,EAAE,WAAW,CAAC;IAExB;;OAEG;IAIH,IAAI,EAAE,KAAK,CAAC;IAEZ;;OAEG;IAEH,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAElC;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEnC;;OAEG;IACH,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;IAEvB;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;IAExB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI,CAAC;IAC3C,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAElC,OAAO,CAAC,UAAU,GAAG,IAAI,EAAE,gBAAgB,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3F,EAAE,EAAE,MAAM,EACV,GAAG,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,gBAAgB,CAAC,EAAE,GAC/C,IAAI,CAAC,UAAU,EAAE,gBAAgB,CAAC,EAAE,CAAC;IACxC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAE7B,SAAS,CAAC,iBAAiB,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3E,GAAG,MAAM,EAAE,SAAS,CAAC,iBAAiB,CAAC,EAAE,GACxC,MAAM,CAAC,iBAAiB,CAAC,EAAE,CAAC;IAC/B,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,OAAO,CAAC,KAAK,GAAG,IAAI,EAAE,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,IAAI,CACrG,IAAI,EACJ,IAAI,GAAG,OAAO,CACf,GACC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,CAAC,EAAE,IAAI,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,GAAG;IAAE,OAAO,CAAC,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC;AAElG,eAAO,MAAM,WAAW,SAAU,OAAO,2CACmC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EchoGraph.stories.d.ts","sourceRoot":"","sources":["../../../../src/stories/EchoGraph.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;;;;;AAoBpB,wBAGE;
|
|
1
|
+
{"version":3,"file":"EchoGraph.stories.d.ts","sourceRoot":"","sources":["../../../../src/stories/EchoGraph.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,YAAY,CAAC;;;;;AAoBpB,wBAGE;AA6MF,eAAO,MAAM,OAAO;;CAEnB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type Action } from './action';
|
|
2
|
+
import { type Graph } from './graph';
|
|
3
|
+
import { type NodeBuilder, type Node } from './node';
|
|
4
|
+
/**
|
|
5
|
+
* Create a test node builder that always adds nodes and actions to the specified depth.
|
|
6
|
+
*
|
|
7
|
+
* @param id The id of the node builder, used to identify nodes created by this builder.
|
|
8
|
+
* @param depth The depth at which to add nodes and actions.
|
|
9
|
+
* @default 1
|
|
10
|
+
*
|
|
11
|
+
* @returns A test node builder
|
|
12
|
+
*/
|
|
13
|
+
export declare const createTestNodeBuilder: (id: string, depth?: number) => {
|
|
14
|
+
nodeBuilder: NodeBuilder;
|
|
15
|
+
addNode: (parentId: string, node: Pick<Node, 'id' | 'label'> & Partial<Node>) => Node<any, Record<string, any>> | undefined;
|
|
16
|
+
removeNode: (parentId: string, id: string) => Node<any, Record<string, any>> | undefined;
|
|
17
|
+
addAction: (parentId: string, action: Pick<Action, 'id' | 'label' | 'invoke'> & Partial<Action>) => Action<Record<string, any>>[] | undefined;
|
|
18
|
+
removeAction: (parentId: string, id: string) => Action | undefined;
|
|
19
|
+
addProperty: (parentId: string, key: string, value: any) => void;
|
|
20
|
+
removeProperty: (parentId: string, key: string) => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Build a graph from a nested list of nodes.
|
|
24
|
+
*
|
|
25
|
+
* @param graph Graph to add nodes to.
|
|
26
|
+
* @param nodes Nodes to add to the
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const graph = new GraphStore();
|
|
30
|
+
* buildGraph(graph, [
|
|
31
|
+
* {
|
|
32
|
+
* id: 'test1',
|
|
33
|
+
* label: 'test1',
|
|
34
|
+
* children: [
|
|
35
|
+
* {
|
|
36
|
+
* id: 'test1.1',
|
|
37
|
+
* label: 'test1.1',
|
|
38
|
+
* },
|
|
39
|
+
* {
|
|
40
|
+
* id: 'test1.2',
|
|
41
|
+
* label: 'test1.2',
|
|
42
|
+
* },
|
|
43
|
+
* ],
|
|
44
|
+
* },
|
|
45
|
+
* {
|
|
46
|
+
* id: 'test2',
|
|
47
|
+
* label: 'test2',
|
|
48
|
+
* },
|
|
49
|
+
* ]);
|
|
50
|
+
*/
|
|
51
|
+
export declare const buildGraph: (graph: Graph, id: string, nodes: any[]) => Graph;
|
|
52
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../../src/testing.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,IAAI,EAAE,MAAM,QAAQ,CAAC;AAErD;;;;;;;;GAQG;AAEH,eAAO,MAAM,qBAAqB,OAAQ,MAAM;;wBAwBnB,MAAM,QAAQ,KAAK,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,GAAG,QAAQ,IAAI,CAAC;2BAWrD,MAAM,MAAM,MAAM;0BASnB,MAAM,UAAU,KAAK,MAAM,EAAE,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC,GAAG,QAAQ,MAAM,CAAC;6BAStE,MAAM,MAAM,MAAM;4BASnB,MAAM,OAAO,MAAM,SAAS,GAAG;+BAS5B,MAAM,OAAO,MAAM;CAUtD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,eAAO,MAAM,UAAU,UAAW,KAAK,MAAM,MAAM,SAAS,GAAG,EAAE,UAGhE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/app-graph",
|
|
3
|
-
"version": "0.4.7-main.
|
|
3
|
+
"version": "0.4.7-main.e015b9e",
|
|
4
4
|
"description": "Constructs knowledge graphs for the purpose of building applications on top of",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -12,9 +12,22 @@
|
|
|
12
12
|
"import": "./dist/lib/browser/index.mjs",
|
|
13
13
|
"require": "./dist/lib/node/index.cjs",
|
|
14
14
|
"node": "./dist/lib/node/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./testing": {
|
|
17
|
+
"browser": "./dist/lib/browser/testing.mjs",
|
|
18
|
+
"import": "./dist/lib/browser/testing.mjs",
|
|
19
|
+
"require": "./dist/lib/node/testing.cjs",
|
|
20
|
+
"node": "./dist/lib/node/testing.cjs"
|
|
15
21
|
}
|
|
16
22
|
},
|
|
17
23
|
"types": "dist/types/src/index.d.ts",
|
|
24
|
+
"typesVersions": {
|
|
25
|
+
"*": {
|
|
26
|
+
"testing": [
|
|
27
|
+
"dist/types/src/testing.d.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
18
31
|
"files": [
|
|
19
32
|
"dist",
|
|
20
33
|
"src"
|
|
@@ -22,25 +35,31 @@
|
|
|
22
35
|
"dependencies": {
|
|
23
36
|
"@preact/signals-core": "^1.5.1",
|
|
24
37
|
"deepsignal": "^1.5.0",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"@dxos/
|
|
28
|
-
"@dxos/
|
|
38
|
+
"lodash.get": "^4.4.2",
|
|
39
|
+
"lodash.set": "^4.3.2",
|
|
40
|
+
"@dxos/async": "0.4.7-main.e015b9e",
|
|
41
|
+
"@dxos/debug": "0.4.7-main.e015b9e",
|
|
42
|
+
"@dxos/invariant": "0.4.7-main.e015b9e",
|
|
43
|
+
"@dxos/keyboard": "0.4.7-main.e015b9e",
|
|
44
|
+
"@dxos/util": "0.4.7-main.e015b9e"
|
|
29
45
|
},
|
|
30
46
|
"devDependencies": {
|
|
31
47
|
"@phosphor-icons/react": "^2.0.5",
|
|
48
|
+
"@types/lodash.get": "^4.4.7",
|
|
49
|
+
"@types/lodash.set": "^4.3.7",
|
|
32
50
|
"@types/react": "^18.0.21",
|
|
33
51
|
"@types/react-dom": "^18.0.6",
|
|
34
52
|
"react": "^18.2.0",
|
|
35
53
|
"react-dom": "^18.2.0",
|
|
36
54
|
"vite": "^5.1.3",
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/react-ui": "0.4.7-main.
|
|
40
|
-
"@dxos/
|
|
41
|
-
"@dxos/
|
|
55
|
+
"@dxos/react-client": "0.4.7-main.e015b9e",
|
|
56
|
+
"@dxos/random": "0.4.7-main.e015b9e",
|
|
57
|
+
"@dxos/react-ui-theme": "0.4.7-main.e015b9e",
|
|
58
|
+
"@dxos/react-ui": "0.4.7-main.e015b9e",
|
|
59
|
+
"@dxos/storybook-utils": "0.4.7-main.e015b9e"
|
|
42
60
|
},
|
|
43
61
|
"peerDependencies": {
|
|
62
|
+
"@phosphor-icons/react": "^2.0.5",
|
|
44
63
|
"react": "^18.2.0",
|
|
45
64
|
"react-dom": "^18.2.0"
|
|
46
65
|
},
|
package/src/action.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type { IconProps } from '@phosphor-icons/react';
|
|
6
|
+
import type { FC } from 'react';
|
|
7
|
+
|
|
8
|
+
import type { MaybePromise } from '@dxos/util';
|
|
9
|
+
|
|
10
|
+
// TODO(thure): `Parameters<TFunction>` causes typechecking issues because `TFunction` has so many signatures.
|
|
11
|
+
export type Label = string | [string, { ns: string; count?: number }];
|
|
12
|
+
|
|
13
|
+
export type InvokeParams = Partial<{
|
|
14
|
+
caller: string;
|
|
15
|
+
}>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Platform-specific key binding.
|
|
19
|
+
*/
|
|
20
|
+
// NOTE: Keys come from `getHostPlatform` in `@dxos/util`.
|
|
21
|
+
export type KeyBinding = {
|
|
22
|
+
windows?: string;
|
|
23
|
+
macos?: string;
|
|
24
|
+
ios?: string;
|
|
25
|
+
linux?: string;
|
|
26
|
+
unknown?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* An action on a node in the graph which may be invoked by sending the associated intent.
|
|
31
|
+
*/
|
|
32
|
+
export type Action<TProperties extends Record<string, any> = Record<string, any>> = {
|
|
33
|
+
/**
|
|
34
|
+
* Locally unique ID.
|
|
35
|
+
*/
|
|
36
|
+
id: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Label to be used when displaying the node.
|
|
40
|
+
* For default labels, use a translated string.
|
|
41
|
+
*
|
|
42
|
+
* @example 'Test Action'
|
|
43
|
+
* @example ['test action label, { ns: 'example-plugin' }]
|
|
44
|
+
*/
|
|
45
|
+
label: Label;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Icon to be used when displaying the node.
|
|
49
|
+
*/
|
|
50
|
+
icon?: FC<IconProps>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Key binding.
|
|
54
|
+
*/
|
|
55
|
+
// TODO(wittjosiah): Factor out.
|
|
56
|
+
// TODO(burdon): Rename shortcut.
|
|
57
|
+
keyBinding?: string | KeyBinding;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Properties of the node relevant to displaying the action.
|
|
61
|
+
*
|
|
62
|
+
* @example { index: 'a1' }
|
|
63
|
+
*/
|
|
64
|
+
properties: TProperties;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sub-actions of the node stored by their id.
|
|
68
|
+
*/
|
|
69
|
+
actionsMap: Record<string, Action>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Actions of the node in default order.
|
|
73
|
+
*/
|
|
74
|
+
// TODO(burdon): Why get vs. prop?
|
|
75
|
+
get actions(): Action[];
|
|
76
|
+
|
|
77
|
+
invoke: (params?: InvokeParams) => MaybePromise<any>;
|
|
78
|
+
|
|
79
|
+
addProperty(key: string, value: any): void;
|
|
80
|
+
removeProperty(key: string): void;
|
|
81
|
+
|
|
82
|
+
addAction<TActionProperties extends Record<string, any> = Record<string, any>>(
|
|
83
|
+
...action: ActionArg<TActionProperties>[]
|
|
84
|
+
): Action<TActionProperties>[];
|
|
85
|
+
removeAction(id: string): Action;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type ActionArg<TProperties extends Record<string, any> = Record<string, any>> = Pick<
|
|
89
|
+
Action,
|
|
90
|
+
'id' | 'label' | 'invoke'
|
|
91
|
+
> &
|
|
92
|
+
Partial<Omit<Action<TProperties>, 'id' | 'label' | 'invoke' | 'actions'>> & { actions?: ActionArg[] };
|
package/src/graph-builder.ts
CHANGED
|
@@ -2,50 +2,238 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { untracked } from '@preact/signals-core';
|
|
6
|
+
import { type RevertDeepSignal, deepSignal } from 'deepsignal/react';
|
|
6
7
|
|
|
8
|
+
import { EventSubscriptions } from '@dxos/async';
|
|
9
|
+
import { Keyboard } from '@dxos/keyboard';
|
|
10
|
+
import { getHostPlatform } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import type { ActionArg, Action } from './action';
|
|
7
13
|
import { Graph } from './graph';
|
|
14
|
+
import type { NodeArg, Node, NodeBuilder } from './node';
|
|
8
15
|
|
|
9
|
-
export
|
|
16
|
+
export const KEY_BINDING = 'KeyBinding';
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
|
-
* The builder
|
|
19
|
+
* The builder...
|
|
13
20
|
*/
|
|
14
21
|
export class GraphBuilder {
|
|
15
|
-
private readonly
|
|
16
|
-
private readonly _unsubscribe = new EventSubscriptions();
|
|
22
|
+
private readonly _nodeBuilders = new Map<string, NodeBuilder>();
|
|
23
|
+
private readonly _unsubscribe = new Map<string, EventSubscriptions>();
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* Register a node builder which will be called in order to construct the graph.
|
|
20
27
|
*/
|
|
21
|
-
|
|
22
|
-
this.
|
|
28
|
+
addNodeBuilder(id: string, builder: NodeBuilder): GraphBuilder {
|
|
29
|
+
this._nodeBuilders.set(id, builder);
|
|
23
30
|
return this;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
27
34
|
* Remove a node builder from the graph builder.
|
|
28
35
|
*/
|
|
29
|
-
|
|
30
|
-
this.
|
|
36
|
+
removeNodeBuilder(id: string): GraphBuilder {
|
|
37
|
+
this._nodeBuilders.delete(id);
|
|
31
38
|
return this;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
/**
|
|
35
|
-
* Construct the graph, starting by calling all registered
|
|
42
|
+
* Construct the graph, starting by calling all registered node builders on the root node.
|
|
43
|
+
* Node builders will be filtered out as they are used such that they are only used once on any given path.
|
|
36
44
|
* @param previousGraph If provided, the graph will be updated in place.
|
|
45
|
+
* @param startingPath If provided, the graph will be updated starting at the given path.
|
|
46
|
+
*/
|
|
47
|
+
build(previousGraph?: Graph, startingPath: string[] = []): Graph {
|
|
48
|
+
const graph: Graph = previousGraph ?? new Graph(this._createNode(() => graph, { id: 'root', label: 'Root' }));
|
|
49
|
+
return this._build(graph, graph.root, startingPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Called recursively.
|
|
37
54
|
*/
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
55
|
+
private _build(graph: Graph, node: Node, path: string[] = [], ignoreBuilders: string[] = []): Graph {
|
|
56
|
+
// TODO(wittjosiah): Should this support multiple paths to the same node?
|
|
57
|
+
graph._setPath(node.id, path);
|
|
58
|
+
|
|
59
|
+
// TODO(burdon): Document.
|
|
60
|
+
const subscriptions = this._unsubscribe.get(node.id) ?? new EventSubscriptions();
|
|
61
|
+
subscriptions.clear();
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
Array.from(this._nodeBuilders.entries())
|
|
64
|
+
.filter(([id]) => ignoreBuilders.findIndex((ignore) => ignore === id) === -1)
|
|
65
|
+
.forEach(([_, builder]) => {
|
|
66
|
+
const unsubscribe = builder(node);
|
|
67
|
+
unsubscribe && subscriptions.add(unsubscribe);
|
|
68
|
+
});
|
|
43
69
|
|
|
44
|
-
|
|
45
|
-
const unsubscribe = builder(graph);
|
|
46
|
-
unsubscribe && this._unsubscribe.add(unsubscribe);
|
|
47
|
-
});
|
|
70
|
+
this._unsubscribe.set(node.id, subscriptions);
|
|
48
71
|
|
|
49
72
|
return graph;
|
|
50
73
|
}
|
|
74
|
+
|
|
75
|
+
private _createNode<TData = null, TProperties extends Record<string, any> = Record<string, any>>(
|
|
76
|
+
getGraph: () => Graph,
|
|
77
|
+
partial: NodeArg<TData, TProperties>,
|
|
78
|
+
path: string[] = [],
|
|
79
|
+
ignoreBuilders: string[] = [],
|
|
80
|
+
): Node<TData, TProperties> {
|
|
81
|
+
// TODO(burdon): Document implications and rationale of deepSignal.
|
|
82
|
+
const node: Node<TData, TProperties> = deepSignal({
|
|
83
|
+
parent: null,
|
|
84
|
+
data: null as TData, // TODO(burdon): Allow null property?
|
|
85
|
+
properties: {} as TProperties,
|
|
86
|
+
childrenMap: {},
|
|
87
|
+
actionsMap: {},
|
|
88
|
+
// TODO(burdon): Document.
|
|
89
|
+
...partial,
|
|
90
|
+
|
|
91
|
+
get children() {
|
|
92
|
+
return Object.values(node.childrenMap);
|
|
93
|
+
},
|
|
94
|
+
get actions() {
|
|
95
|
+
return Object.values(node.actionsMap);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
//
|
|
99
|
+
// Properties
|
|
100
|
+
//
|
|
101
|
+
|
|
102
|
+
addProperty: (key, value) => {
|
|
103
|
+
untracked(() => {
|
|
104
|
+
(node.properties as Record<string, any>)[key] = value;
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
removeProperty: (key) => {
|
|
108
|
+
untracked(() => {
|
|
109
|
+
delete (node.properties as Record<string, any>)[key];
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
//
|
|
114
|
+
// Nodes
|
|
115
|
+
//
|
|
116
|
+
|
|
117
|
+
addNode: (builder, ...partials) => {
|
|
118
|
+
return untracked(() => {
|
|
119
|
+
return partials.map((partial) => {
|
|
120
|
+
const builders = [...ignoreBuilders, builder];
|
|
121
|
+
const childPath = [...path, 'childrenMap', partial.id];
|
|
122
|
+
const child = this._createNode(getGraph, { ...partial, parent: node }, childPath, builders);
|
|
123
|
+
node.childrenMap[child.id] = child;
|
|
124
|
+
// TODO(burdon): Defer triggering recursive updates until task has completed.
|
|
125
|
+
this._build(getGraph(), child, childPath, builders);
|
|
126
|
+
return child;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
removeNode: (id) => {
|
|
131
|
+
return untracked(() => {
|
|
132
|
+
const child = node.childrenMap[id];
|
|
133
|
+
delete node.childrenMap[id];
|
|
134
|
+
return child;
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
//
|
|
139
|
+
// Actions
|
|
140
|
+
//
|
|
141
|
+
|
|
142
|
+
addAction: (...partials) => {
|
|
143
|
+
return untracked(() => {
|
|
144
|
+
return partials.map((partial) => {
|
|
145
|
+
const action = this._createAction(partial);
|
|
146
|
+
let shortcut: string | undefined;
|
|
147
|
+
if (typeof action.keyBinding === 'object') {
|
|
148
|
+
const availablePlatforms = Object.keys(action.keyBinding);
|
|
149
|
+
const platform = getHostPlatform();
|
|
150
|
+
shortcut = availablePlatforms.includes(platform)
|
|
151
|
+
? action.keyBinding[platform]
|
|
152
|
+
: platform === 'ios'
|
|
153
|
+
? action.keyBinding.macos // Fallback to macos if ios-specific bindings not provided.
|
|
154
|
+
: platform === 'linux' || platform === 'unknown'
|
|
155
|
+
? action.keyBinding.windows // Fallback to windows if platform-specific bindings not provided.
|
|
156
|
+
: undefined;
|
|
157
|
+
} else {
|
|
158
|
+
shortcut = action.keyBinding;
|
|
159
|
+
}
|
|
160
|
+
if (shortcut) {
|
|
161
|
+
Keyboard.singleton.getContext(path.join('/')).bind({
|
|
162
|
+
shortcut,
|
|
163
|
+
handler: () => {
|
|
164
|
+
action.invoke({ caller: KEY_BINDING });
|
|
165
|
+
},
|
|
166
|
+
data: action.label,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
node.actionsMap[action.id] = action;
|
|
171
|
+
return action;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
removeAction: (id) => {
|
|
176
|
+
return untracked(() => {
|
|
177
|
+
const action = node.actionsMap[id];
|
|
178
|
+
if (action.keyBinding) {
|
|
179
|
+
// keyboardjs.unbind(action.keyBinding);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
delete node.actionsMap[id];
|
|
183
|
+
return action;
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
}) as RevertDeepSignal<Node<TData, TProperties>>;
|
|
187
|
+
|
|
188
|
+
// Only actions added at this stage are available to subsequent builders.
|
|
189
|
+
// `addNode` immediately passes the new node to other builders.
|
|
190
|
+
// As such, actions added later with `addAction` are not available to those builders.
|
|
191
|
+
// Having actions available to subsequent builders is useful for building groups.
|
|
192
|
+
partial.actions && partial.actions.forEach((action) => node.addAction(action));
|
|
193
|
+
|
|
194
|
+
return node;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private _createAction<TProperties extends Record<string, any> = Record<string, any>>(
|
|
198
|
+
partial: ActionArg<TProperties>,
|
|
199
|
+
): Action<TProperties> {
|
|
200
|
+
const action: Action<TProperties> = deepSignal({
|
|
201
|
+
properties: {} as TProperties,
|
|
202
|
+
...partial,
|
|
203
|
+
actionsMap: {},
|
|
204
|
+
get actions() {
|
|
205
|
+
return Object.values(action.actionsMap);
|
|
206
|
+
},
|
|
207
|
+
addAction: (...partials) => {
|
|
208
|
+
return untracked(() => {
|
|
209
|
+
return partials.map((partial) => {
|
|
210
|
+
const subAction = this._createAction(partial);
|
|
211
|
+
action.actionsMap[subAction.id] = subAction;
|
|
212
|
+
return subAction;
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
removeAction: (id) => {
|
|
217
|
+
return untracked(() => {
|
|
218
|
+
const subAction = action.actionsMap[id];
|
|
219
|
+
delete action.actionsMap[id];
|
|
220
|
+
return subAction;
|
|
221
|
+
});
|
|
222
|
+
},
|
|
223
|
+
addProperty: (key, value) => {
|
|
224
|
+
return untracked(() => {
|
|
225
|
+
(action.properties as Record<string, any>)[key] = value;
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
removeProperty: (key) => {
|
|
229
|
+
return untracked(() => {
|
|
230
|
+
delete (action.properties as Record<string, any>)[key];
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
}) as RevertDeepSignal<Action<TProperties>>;
|
|
234
|
+
|
|
235
|
+
partial.actions && partial.actions.forEach((subAction) => action.addAction(subAction));
|
|
236
|
+
|
|
237
|
+
return action;
|
|
238
|
+
}
|
|
51
239
|
}
|