@macrostrat/feedback-components 1.0.0

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -0
  3. package/dist/edit-state.e8edb13a.js +251 -0
  4. package/dist/edit-state.e8edb13a.js.map +1 -0
  5. package/dist/extractions.54be85f8.js +177 -0
  6. package/dist/extractions.54be85f8.js.map +1 -0
  7. package/dist/feedback.46c2b5c4.js +252 -0
  8. package/dist/feedback.46c2b5c4.js.map +1 -0
  9. package/dist/feedback.module.7e16830e.css +44 -0
  10. package/dist/feedback.module.7e16830e.css.map +1 -0
  11. package/dist/feedback.module.c28cbac7.js +28 -0
  12. package/dist/feedback.module.c28cbac7.js.map +1 -0
  13. package/dist/graph.cb42b871.js +83 -0
  14. package/dist/graph.cb42b871.js.map +1 -0
  15. package/dist/index.d.ts +145 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +9 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/main.module.21bbfaf4.js +19 -0
  20. package/dist/main.module.21bbfaf4.js.map +1 -0
  21. package/dist/main.module.ca3db294.js +16 -0
  22. package/dist/main.module.ca3db294.js.map +1 -0
  23. package/dist/main.module.d6508c0e.css +14 -0
  24. package/dist/main.module.d6508c0e.css.map +1 -0
  25. package/dist/main.module.f9f92ece.css +17 -0
  26. package/dist/main.module.f9f92ece.css.map +1 -0
  27. package/dist/node.30d0b8c3.js +59 -0
  28. package/dist/node.30d0b8c3.js.map +1 -0
  29. package/dist/text-visualizer.77af0d24.js +101 -0
  30. package/dist/text-visualizer.77af0d24.js.map +1 -0
  31. package/dist/type-selector.e75dd247.js +62 -0
  32. package/dist/type-selector.e75dd247.js.map +1 -0
  33. package/package.json +48 -0
  34. package/src/extractions/index.ts +219 -0
  35. package/src/extractions/main.module.sass +10 -0
  36. package/src/extractions/types.ts +30 -0
  37. package/src/feedback/edit-state.ts +311 -0
  38. package/src/feedback/feedback.module.sass +37 -0
  39. package/src/feedback/graph.ts +98 -0
  40. package/src/feedback/index.ts +271 -0
  41. package/src/feedback/node.ts +65 -0
  42. package/src/feedback/text-visualizer.ts +116 -0
  43. package/src/feedback/type-selector/index.ts +75 -0
  44. package/src/feedback/type-selector/main.module.sass +13 -0
  45. package/src/feedback/types.ts +76 -0
  46. package/src/index.ts +2 -0
  47. package/stories/feedback.stories.ts +40 -0
  48. package/stories/test-data.ts +330 -0
@@ -0,0 +1,311 @@
1
+ import { TreeData } from "./types";
2
+ import { createContext, Dispatch, useContext, useReducer } from "react";
3
+ import update, { Spec } from "immutability-helper";
4
+ import { EntityType } from "../extractions/types";
5
+
6
+ export enum ViewMode {
7
+ Tree = "tree",
8
+ Graph = "graph",
9
+ }
10
+
11
+ interface TreeState {
12
+ initialTree: TreeData[];
13
+ tree: TreeData[];
14
+ selectedNodes: number[];
15
+ entityTypesMap: Map<number, EntityType>;
16
+ selectedEntityType: EntityType;
17
+ lastInternalId: number;
18
+ isSelectingEntityType: boolean;
19
+ viewMode: ViewMode;
20
+ }
21
+
22
+ type TextRange = {
23
+ start: number;
24
+ end: number;
25
+ text: string;
26
+ };
27
+
28
+ type TreeAction =
29
+ | {
30
+ type: "move-node";
31
+ payload: { dragIds: number[]; parentId: number; index: number };
32
+ }
33
+ | { type: "delete-node"; payload: { ids: number[] } }
34
+ | { type: "select-node"; payload: { ids: number[] } }
35
+ | { type: "toggle-node-selected"; payload: { ids: number[] } }
36
+ | { type: "set-view-mode"; payload: ViewMode }
37
+ | { type: "create-node"; payload: TextRange }
38
+ | { type: "select-entity-type"; payload: EntityType }
39
+ | { type: "toggle-entity-type-selector"; payload?: boolean | null }
40
+ | { type: "deselect" }
41
+ | { type: "reset" };
42
+
43
+ export type TreeDispatch = Dispatch<TreeAction>;
44
+
45
+ export function useUpdatableTree(
46
+ initialTree: TreeData[],
47
+ entityTypes: Map<number, EntityType>
48
+ ): [TreeState, TreeDispatch] {
49
+ // Get the first entity type
50
+ const type = entityTypes.values().next().value;
51
+
52
+ return useReducer(treeReducer, {
53
+ initialTree,
54
+ tree: initialTree,
55
+ selectedNodes: [],
56
+ entityTypesMap: entityTypes,
57
+ selectedEntityType: type,
58
+ lastInternalId: 0,
59
+ isSelectingEntityType: false,
60
+ viewMode: ViewMode.Tree,
61
+ });
62
+ }
63
+
64
+ export const TreeDispatchContext = createContext<TreeDispatch | null>(null);
65
+
66
+ export function useTreeDispatch() {
67
+ const dispatch = useContext(TreeDispatchContext);
68
+ if (dispatch == null) {
69
+ throw new Error("No dispatch context available");
70
+ }
71
+ return dispatch;
72
+ }
73
+
74
+ function treeReducer(state: TreeState, action: TreeAction) {
75
+ console.log(action);
76
+ switch (action.type) {
77
+ case "move-node":
78
+ // For each node in the tree, if the node is in the dragIds, remove it from the tree and collect it
79
+ const [newTree, removedNodes] = removeNodes(
80
+ state.tree,
81
+ action.payload.dragIds
82
+ );
83
+
84
+ let keyPath: (number | "children")[] = [];
85
+ if (action.payload.parentId) {
86
+ keyPath = findNode(newTree, action.payload.parentId);
87
+ keyPath.push("children");
88
+ }
89
+
90
+ // Add removed nodes to the new tree at the correct location
91
+ let updateSpec = buildNestedSpec(keyPath, {
92
+ $splice: [[action.payload.index, 0, ...removedNodes]],
93
+ });
94
+
95
+ return { ...state, tree: update(newTree, updateSpec) };
96
+ case "delete-node":
97
+ // For each node in the tree, if the node is in the ids, remove it from the tree
98
+ const [newTree2, _removedNodes] = removeNodes(
99
+ state.tree,
100
+ action.payload.ids
101
+ );
102
+ // Get children of the removed nodes
103
+ // If children are not present elsewhere in the tree, insert them
104
+
105
+ const children = _removedNodes
106
+ .flatMap((node) => node.children ?? [])
107
+ .filter((child) => !nodeIsInTree(newTree2, child.id));
108
+
109
+ // Reset the selection
110
+
111
+ return {
112
+ ...state,
113
+ tree: [...newTree2, ...children],
114
+ selectedNodes: state.selectedNodes.filter(
115
+ (id) => !action.payload.ids.includes(id)
116
+ ),
117
+ };
118
+ case "select-node":
119
+ const { ids } = action.payload;
120
+ return { ...state, selectedNodes: ids };
121
+ // otherwise fall through to toggle-node-selected for a single ID
122
+ case "toggle-node-selected":
123
+ const nodesToAdd = action.payload.ids.filter(
124
+ (id) => !state.selectedNodes.includes(id)
125
+ );
126
+ const nodesToKeep = state.selectedNodes.filter(
127
+ (id) => !action.payload.ids.includes(id)
128
+ );
129
+ return { ...state, selectedNodes: [...nodesToKeep, ...nodesToAdd] };
130
+
131
+ case "create-node":
132
+ const newId = state.lastInternalId - 1;
133
+ const { text, start, end } = action.payload;
134
+ const node: TreeData = {
135
+ id: newId,
136
+ name: text,
137
+ children: [],
138
+ indices: [start, end],
139
+ type: state.selectedEntityType,
140
+ };
141
+
142
+ return {
143
+ ...state,
144
+ tree: [...state.tree, node],
145
+ selectedNodes: [newId],
146
+ lastInternalId: newId,
147
+ };
148
+
149
+ /** Entity type selection */
150
+ case "toggle-entity-type-selector":
151
+ return {
152
+ ...state,
153
+ isSelectingEntityType: action.payload ?? !state.isSelectingEntityType,
154
+ };
155
+ case "select-entity-type": {
156
+ // For each selected node, update the type
157
+ let newTree2 = state.tree;
158
+ for (let id of state.selectedNodes) {
159
+ const keyPath = findNode(state.tree, id);
160
+ const nestedSpec = buildNestedSpec(keyPath, {
161
+ type: { $set: action.payload },
162
+ });
163
+ newTree2 = update(newTree2, nestedSpec);
164
+ }
165
+
166
+ return {
167
+ ...state,
168
+ tree: newTree2,
169
+ selectedEntityType: action.payload,
170
+ };
171
+ }
172
+ case "deselect":
173
+ return { ...state, selectedNodes: [] };
174
+ case "reset":
175
+ return {
176
+ ...state,
177
+ tree: state.initialTree,
178
+ selectedNodes: [],
179
+ };
180
+ case "set-view-mode":
181
+ return { ...state, viewMode: action.payload };
182
+ }
183
+ }
184
+
185
+ function nodeIsInTree(tree: TreeData[], id: number): boolean {
186
+ for (let node of tree) {
187
+ if (node.id == id) {
188
+ return true;
189
+ } else if (node.children) {
190
+ if (nodeIsInTree(node.children, id)) {
191
+ return true;
192
+ }
193
+ }
194
+ }
195
+ return false;
196
+ }
197
+
198
+ function buildNestedSpec(
199
+ keyPath: (number | "children")[],
200
+ innerSpec: Spec<any>
201
+ ): Spec<TreeData[]> {
202
+ // Build a nested object from a key path
203
+
204
+ let spec = innerSpec;
205
+ for (let i = keyPath.length - 1; i >= 0; i--) {
206
+ spec = { [keyPath[i]]: spec };
207
+ }
208
+ return spec as any;
209
+ // Since we don't have a "children" key at the root, we make the top-level spec an array
210
+ }
211
+
212
+ function findNode(
213
+ tree: TreeData[],
214
+ id: number
215
+ ): (number | "children")[] | null {
216
+ // Find the index of the node with the given id in the tree, returning the key path
217
+ for (let i = 0; i < tree.length; i++) {
218
+ if (tree[i].id == id) {
219
+ return [i];
220
+ } else if (tree[i].children) {
221
+ let path = findNode(tree[i].children, id);
222
+ if (path != null) {
223
+ return [i, "children", ...path];
224
+ }
225
+ }
226
+ }
227
+ return null;
228
+ }
229
+
230
+ function removeNodes(
231
+ tree: TreeData[],
232
+ ids: number[]
233
+ ): [TreeData[], TreeData[]] {
234
+ /** Remove nodes with the given ids from the tree and return the new tree and the removed nodes */
235
+ let newTree: TreeData[] = [];
236
+ let removedNodes: TreeData[] = [];
237
+
238
+ for (let node of tree) {
239
+ if (ids.includes(node.id)) {
240
+ removedNodes.push(node);
241
+ } else {
242
+ // Recurse into children
243
+ if (node.children) {
244
+ let [newChildren, removedChildren] = removeNodes(node.children, ids);
245
+ node = { ...node, children: newChildren };
246
+ removedNodes.push(...removedChildren);
247
+ }
248
+ newTree.push(node);
249
+ }
250
+ }
251
+
252
+ return [newTree, removedNodes];
253
+ }
254
+
255
+ export interface EntityOutput {
256
+ id: number;
257
+ type: number | null;
258
+ txt_range: number[][];
259
+ name: string;
260
+ match: any | null;
261
+ reasoning: string | null;
262
+ }
263
+
264
+ export interface GraphData {
265
+ nodes: EntityOutput[];
266
+ edges: { source: number; dest: number }[];
267
+ }
268
+
269
+ export function treeToGraph(tree: TreeData[]): GraphData {
270
+ // Convert the tree to a graph
271
+ let nodes: EntityOutput[] = [];
272
+ let edges: { source: number; dest: number }[] = [];
273
+ const nodeMap = new Map<number, TreeData>();
274
+
275
+ for (let node of tree) {
276
+ // If we've already found an instance of this node, we don't need to record
277
+ // it again
278
+ if (nodeMap.has(node.id)) {
279
+ continue;
280
+ }
281
+
282
+ const { indices, id, name } = node;
283
+
284
+ const nodeData: EntityOutput = {
285
+ id,
286
+ type: node.type.id,
287
+ name,
288
+ txt_range: [indices],
289
+ reasoning: null,
290
+ match: node.match,
291
+ };
292
+
293
+ nodeMap.set(node.id, node);
294
+ nodes.push(nodeData);
295
+
296
+ if (node.children) {
297
+ for (let child of node.children) {
298
+ edges.push({ source: node.id, dest: child.id });
299
+ }
300
+
301
+ // Now process the children
302
+ const { nodes: childNodes, edges: childEdges } = treeToGraph(
303
+ node.children
304
+ );
305
+ nodes.push(...childNodes);
306
+ edges.push(...childEdges);
307
+ }
308
+ }
309
+
310
+ return { nodes, edges };
311
+ }
@@ -0,0 +1,37 @@
1
+ .feedback-component
2
+ position: relative
3
+ width: 800px
4
+
5
+ & > svg
6
+ width: 800px
7
+
8
+ .node
9
+ cursor: pointer
10
+
11
+
12
+ .feedback-text
13
+ margin-bottom: 2em
14
+
15
+ .entity-panel
16
+ position: relative
17
+ max-height: 600px
18
+
19
+ .control-panel
20
+ max-width: 15em
21
+ position: absolute
22
+ top: 1em
23
+ right: 1em
24
+ padding: 0.2em 0.5em
25
+
26
+ .entity-panel
27
+ flex: 1
28
+ min-height: 100px
29
+ padding: 1em
30
+ background: var(--panel-secondary-background-color)
31
+ border-radius: 4px
32
+ // Inset box shadow
33
+ box-shadow: 0 0 0 1px var(--panel-border-color) inset
34
+
35
+ .selection-tree
36
+ margin: -1em 0
37
+ padding: 1em 0
@@ -0,0 +1,98 @@
1
+ import { TreeData } from "./types";
2
+ import { treeToGraph } from "./edit-state";
3
+ import h from "@macrostrat/hyper";
4
+
5
+ import {
6
+ forceSimulation,
7
+ SimulationNodeDatum,
8
+ SimulationLinkDatum,
9
+ forceCenter,
10
+ forceLink,
11
+ forceManyBody,
12
+ forceCollide,
13
+ } from "d3-force";
14
+ import { useEffect, useState } from "react";
15
+ import { Spinner } from "@blueprintjs/core";
16
+
17
+ export function GraphView(props: {
18
+ tree: TreeData[];
19
+ width: number;
20
+ height: number;
21
+ }) {
22
+ // A graph view with react-flow
23
+ // Get positions of nodes using force simulation
24
+ const { tree, width, height } = props;
25
+
26
+ const [nodes, setNodes] = useState<SimulationNodeDatum[]>(null);
27
+ const [links, setLinks] = useState<SimulationLinkDatum[]>(null);
28
+
29
+ useEffect(() => {
30
+ const { nodes, edges } = treeToGraph(tree);
31
+
32
+ const nodesMap = new Map<number, SimulationNodeDatum>(
33
+ nodes.map((d) => [d.id, d])
34
+ );
35
+
36
+ const links = edges.map((d) => {
37
+ return {
38
+ source: nodesMap.get(d.source),
39
+ target: nodesMap.get(d.dest),
40
+ strength: 1,
41
+ };
42
+ });
43
+
44
+ const simulation = forceSimulation(nodes)
45
+ .force("link", forceLink(links))
46
+ .force("charge", forceManyBody().strength(-50))
47
+ .force("center", forceCenter(width / 2, height / 2))
48
+ .force("collide", forceCollide().radius(20))
49
+ .on("tick", () => {
50
+ // Update the positions of the nodes
51
+ // setNodes(nodes);
52
+ console.log("Simulation tick");
53
+ })
54
+ .on("end", () => {
55
+ // Update the positions of the nodes
56
+ setNodes(nodes);
57
+ setLinks(links);
58
+ });
59
+
60
+ return () => {
61
+ simulation.stop();
62
+ };
63
+ }, [tree, width, height]);
64
+
65
+ if (nodes == null || links == null) {
66
+ return h(Spinner);
67
+ }
68
+
69
+ console.log("Graph", nodes, links);
70
+
71
+ return h("div.graph-view", { style: { width, height } }, [
72
+ h("svg", { width, height }, [
73
+ h(
74
+ "g.nodes",
75
+ nodes.map((d) => {
76
+ return h("circle", {
77
+ cx: d.x,
78
+ cy: d.y,
79
+ r: 5,
80
+ fill: "blue",
81
+ });
82
+ })
83
+ ),
84
+ h(
85
+ "g.links",
86
+ links.map((d) => {
87
+ return h("line", {
88
+ x1: d.source.x,
89
+ y1: d.source.y,
90
+ x2: d.target.x,
91
+ y2: d.target.y,
92
+ stroke: "black",
93
+ });
94
+ })
95
+ ),
96
+ ]),
97
+ ]);
98
+ }
@@ -0,0 +1,271 @@
1
+ import styles from "./feedback.module.sass";
2
+ import hyper from "@macrostrat/hyper";
3
+
4
+ import { Tree, TreeApi } from "react-arborist";
5
+ import Node from "./node";
6
+ import { FeedbackText } from "./text-visualizer";
7
+ import type { InternalEntity, TreeData } from "./types";
8
+ import type { Entity } from "../extractions";
9
+ import { ModelInfo } from "../extractions";
10
+ import {
11
+ TreeDispatchContext,
12
+ treeToGraph,
13
+ useUpdatableTree,
14
+ ViewMode,
15
+ } from "./edit-state";
16
+ import { useCallback, useEffect, useRef } from "react";
17
+ import { ButtonGroup, Card, SegmentedControl } from "@blueprintjs/core";
18
+ import { OmniboxSelector } from "./type-selector";
19
+ import {
20
+ CancelButton,
21
+ DataField,
22
+ FlexBox,
23
+ FlexRow,
24
+ SaveButton,
25
+ } from "@macrostrat/ui-components";
26
+ import useElementDimensions from "use-element-dimensions";
27
+ import { GraphView } from "./graph";
28
+
29
+ export type { GraphData } from "./edit-state";
30
+ export { treeToGraph } from "./edit-state";
31
+ export type { TreeData } from "./types";
32
+
33
+ const h = hyper.styled(styles);
34
+
35
+ function setsAreTheSame<T>(a: Set<T>, b: Set<T>) {
36
+ if (a.size !== b.size) return false;
37
+ for (const item of a) {
38
+ if (!b.has(item)) return false;
39
+ }
40
+ return true;
41
+ }
42
+
43
+ export function FeedbackComponent({
44
+ entities = [],
45
+ text,
46
+ model,
47
+ entityTypes,
48
+ matchComponent,
49
+ onSave,
50
+ }) {
51
+ // Get the input arguments
52
+
53
+ const [state, dispatch] = useUpdatableTree(
54
+ entities.map(processEntity) as any,
55
+ entityTypes
56
+ );
57
+
58
+ const { selectedNodes, tree, selectedEntityType, isSelectingEntityType } =
59
+ state;
60
+
61
+ const [{ width, height }, ref] = useElementDimensions();
62
+
63
+ return h(TreeDispatchContext.Provider, { value: dispatch }, [
64
+ h(FeedbackText, {
65
+ text,
66
+ dispatch,
67
+ // @ts-ignore
68
+ nodes: tree,
69
+ selectedNodes,
70
+ }),
71
+ h(FlexRow, { alignItems: "baseline", justifyContent: "space-between" }, [
72
+ h(ModelInfo, { data: model }),
73
+ h(SegmentedControl, {
74
+ options: [
75
+ { label: "Tree", value: "tree" },
76
+ { label: "Graph", value: "graph" },
77
+ ],
78
+ value: state.viewMode,
79
+ small: true,
80
+ onValueChange(value: ViewMode) {
81
+ console.log("Setting view mode", value);
82
+ dispatch({ type: "set-view-mode", payload: value });
83
+ },
84
+ }),
85
+ ]),
86
+ h(
87
+ "div.entity-panel",
88
+ {
89
+ ref,
90
+ },
91
+ [
92
+ h(Card, { className: "control-panel" }, [
93
+ h(
94
+ ButtonGroup,
95
+ {
96
+ vertical: true,
97
+ fill: true,
98
+ minimal: true,
99
+ alignText: "left",
100
+ },
101
+ [
102
+ h(
103
+ CancelButton,
104
+ {
105
+ icon: "trash",
106
+ disabled: state.initialTree == state.tree,
107
+ onClick() {
108
+ dispatch({ type: "reset" });
109
+ },
110
+ },
111
+ "Reset"
112
+ ),
113
+ h(
114
+ SaveButton,
115
+ {
116
+ onClick() {
117
+ onSave(state.tree);
118
+ },
119
+ disabled: state.initialTree == state.tree,
120
+ },
121
+ "Save"
122
+ ),
123
+ ]
124
+ ),
125
+ h(EntityTypeSelector, {
126
+ entityTypes,
127
+ selected: selectedEntityType,
128
+ onChange(payload) {
129
+ dispatch({ type: "select-entity-type", payload });
130
+ },
131
+ isOpen: isSelectingEntityType,
132
+ setOpen: (isOpen: boolean) =>
133
+ dispatch({
134
+ type: "toggle-entity-type-selector",
135
+ payload: isOpen,
136
+ }),
137
+ }),
138
+ ]),
139
+ h.if(state.viewMode == "tree")(ManagedSelectionTree, {
140
+ selectedNodes,
141
+ dispatch,
142
+ tree,
143
+ width,
144
+ height,
145
+ matchComponent,
146
+ }),
147
+ h.if(state.viewMode == "graph")(GraphView, {
148
+ tree,
149
+ width,
150
+ height,
151
+ }),
152
+ ]
153
+ ),
154
+ ]);
155
+ }
156
+
157
+ function processEntity(entity: Entity): InternalEntity {
158
+ // @ts-ignore
159
+ return {
160
+ ...entity,
161
+ // @ts-ignore
162
+ term_type: entity.type.name,
163
+ txt_range: [entity.indices],
164
+ children: entity.children?.map(processEntity) ?? [],
165
+ };
166
+ }
167
+
168
+ function EntityTypeSelector({
169
+ entityTypes,
170
+ selected,
171
+ isOpen,
172
+ setOpen,
173
+ onChange,
174
+ }) {
175
+ // Show all entity types when selected is null
176
+ const _selected = selected != null ? selected : undefined;
177
+ return h(DataField, { label: "Entity type", inline: true }, [
178
+ h(
179
+ "code.bp5-code",
180
+ {
181
+ onClick() {
182
+ setOpen((d) => !d);
183
+ },
184
+ },
185
+ selected.name
186
+ ),
187
+ h(OmniboxSelector, {
188
+ isOpen,
189
+ items: Array.from(entityTypes.values()),
190
+ selectedItem: _selected,
191
+ onSelectItem(item) {
192
+ setOpen(false);
193
+ onChange(item);
194
+ },
195
+ onClose() {
196
+ setOpen(false);
197
+ },
198
+ }),
199
+ ]);
200
+ }
201
+
202
+ function ManagedSelectionTree(props) {
203
+ const {
204
+ selectedNodes,
205
+ dispatch,
206
+ tree,
207
+ height,
208
+ width,
209
+ matchComponent,
210
+ ...rest
211
+ } = props;
212
+
213
+ const ref = useRef<TreeApi<TreeData>>();
214
+
215
+ const _Node = useCallback(
216
+ (props) => h(Node, { ...props, matchComponent }),
217
+ [matchComponent]
218
+ );
219
+
220
+ useEffect(() => {
221
+ if (ref.current == null) return;
222
+ // Check if selection matches current
223
+ const selection = new Set(selectedNodes.map((d) => d.toString()));
224
+ const currentSelection = ref.current.selectedIds;
225
+ if (setsAreTheSame(selection, currentSelection)) return;
226
+ // If the selection is the same, do nothing
227
+
228
+ // Set selection
229
+ ref.current.setSelection({
230
+ ids: selectedNodes.map((d) => d.toString()),
231
+ anchor: null,
232
+ mostRecent: null,
233
+ });
234
+ }, [selectedNodes]);
235
+
236
+ return h(Tree, {
237
+ className: "selection-tree",
238
+ height,
239
+ width,
240
+ ref,
241
+ data: tree,
242
+ onMove({ dragIds, parentId, index }) {
243
+ dispatch({
244
+ type: "move-node",
245
+ payload: {
246
+ dragIds: dragIds.map((d) => parseInt(d)),
247
+ parentId: parentId ? parseInt(parentId) : null,
248
+ index,
249
+ },
250
+ });
251
+ },
252
+ onDelete({ ids }) {
253
+ dispatch({
254
+ type: "delete-node",
255
+ payload: { ids: ids.map((d) => parseInt(d)) },
256
+ });
257
+ },
258
+ onSelect(nodes) {
259
+ let ids = nodes.map((d) => parseInt(d.id));
260
+ if (ids.length == 1 && ids[0] == selectedNodes[0]) {
261
+ // Deselect
262
+ ids = [];
263
+ }
264
+ dispatch({ type: "select-node", payload: { ids } });
265
+ },
266
+ children: _Node,
267
+ idAccessor(d: TreeData) {
268
+ return d.id.toString();
269
+ },
270
+ });
271
+ }