Skip to content

Commit

Permalink
Highlight path edges (#73)
Browse files Browse the repository at this point in the history
* move Nodes directory to subcomponent of Tree component

* initial custom edge DecisionEdge

* set new DecisionEdge as teh deafult edge

* optional styling for edge when decision made prop passed

* add edge to chosen path implementation

* remove old edges from path on selection
  • Loading branch information
dpgraham4401 committed Mar 15, 2024
1 parent 503f551 commit 78607ee
Show file tree
Hide file tree
Showing 20 changed files with 148 additions and 19 deletions.
39 changes: 39 additions & 0 deletions src/components/Tree/Edges/DecisionEdge/DecisionEdge.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import '@testing-library/jest-dom';
import { cleanup, render } from '@testing-library/react';
import { Position, ReactFlowProvider } from 'reactflow';
import { afterEach, describe, test } from 'vitest';
import { DecisionEdge } from './DecisionEdge';

afterEach(() => cleanup());

interface TestComponentProps {
source?: string;
target?: string;
}

const TestComponent = (props: TestComponentProps) => (
<ReactFlowProvider>
<svg>
<DecisionEdge
id={'1'}
source={props.source || 'foo'}
target={props.target || 'bar'}
sourceX={0}
sourceY={0}
targetX={0}
targetY={0}
sourcePosition={Position.Left}
targetPosition={Position.Right}
/>
</svg>
</ReactFlowProvider>
);

// ToDo: We need to find a better way to test this component.
// The challenge is our component is only a slim wrapper for the BaseEdge component from reactflow.
// There's not much to test here, or even grab as a reference to test the component.
describe('Decision Edge', () => {
test('renders', () => {
render(<TestComponent />);
});
});
32 changes: 32 additions & 0 deletions src/components/Tree/Edges/DecisionEdge/DecisionEdge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BaseEdge, EdgeProps, getSmoothStepPath } from 'reactflow';

export interface DecisionEdgeData {
decisionMade?: boolean;
}

export interface DecisionEdgeProps extends EdgeProps<DecisionEdgeData> {}

export const DecisionEdge = (props: DecisionEdgeProps) => {
const { id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition } = props;
const [edgePath] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
});

return (
<>
<BaseEdge
id={id}
path={edgePath}
style={{
stroke: props.data?.decisionMade ? '#05b485' : '',
strokeWidth: '3px',
}}
/>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { act, cleanup, render, screen } from '@testing-library/react';
import { BaseNode } from 'components/Nodes/BaseNode/BaseNode';
import { BaseNode } from 'components/Tree/Nodes/BaseNode/BaseNode';
import { ReactFlowProvider } from 'reactflow';
import useTreeStore from 'store';
import { afterEach, describe, expect, test } from 'vitest';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import styles from 'components/Tree/Nodes/BaseNode/baseNode.module.css';
import { DragHandle } from 'components/Tree/Nodes/BaseNode/DragHandle/DragHandle';
import { useTreeDirection } from 'hooks';
import { ReactNode, useEffect } from 'react';
import { Handle, NodeProps, Position, useUpdateNodeInternals } from 'reactflow';
import { DecisionStatus } from 'store/DecisionSlice/decisionSlice';

import styles from './baseNode.module.css';
import { DragHandle } from './DragHandle/DragHandle';

interface BaseNodeProps extends Omit<NodeProps, 'data'> {
children: ReactNode;
status?: DecisionStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styles from 'components/Nodes/BaseNode/DragHandle/dragHandle.module.css';
import styles from 'components/Tree/Nodes/BaseNode/DragHandle/dragHandle.module.css';

export const DragHandle = () => {
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '@testing-library/jest-dom';
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BoolNode, BoolNodeData } from 'components/Nodes/BoolNode/BoolNode';
import { BoolNode, BoolNodeData } from 'components/Tree/Nodes/BoolNode/BoolNode';
import { NodeProps, ReactFlowProvider } from 'reactflow';
import useTreeStore from 'store';
import { afterEach, describe, expect, test } from 'vitest';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { BaseNode } from 'components/Nodes/BaseNode/BaseNode';
import { BaseNode } from 'components/Tree/Nodes/BaseNode/BaseNode';

import styles from 'components/Tree/Nodes/BoolNode/bool.module.css';
import { useDecisionTree } from 'hooks';
import { useState } from 'react';
import { NodeProps } from 'reactflow';
import { NodeData } from 'store/DecisionSlice/decisionSlice';

import styles from './bool.module.css';

export interface BoolNodeData extends NodeData {
label: string;
yesId: string;
Expand All @@ -25,6 +25,7 @@ export const BoolNode = ({
hideDescendants,
markDecisionMade,
markDecisionFocused,
addToPath,
} = useDecisionTree();
const [selected, setSelected] = useState<'yes' | 'no' | undefined>(undefined);

Expand All @@ -35,6 +36,7 @@ export const BoolNode = ({
hideDescendants(noId);
markDecisionMade(id);
markDecisionFocused(yesId);
addToPath(id, yesId);
setSelected('yes');
};

Expand All @@ -45,6 +47,7 @@ export const BoolNode = ({
hideDescendants(yesId);
markDecisionMade(id);
markDecisionFocused(noId);
addToPath(id, noId);
setSelected('no');
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import '@testing-library/jest-dom';
import { cleanup, render, screen } from '@testing-library/react';
import { DefaultNode } from 'components/Tree/Nodes/DefaultNode/DefaultNode';
import { ReactFlowProvider } from 'reactflow';
import { afterEach, describe, expect, test } from 'vitest';
import { DefaultNode } from './DefaultNode';

afterEach(() => cleanup());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { BaseNode } from 'components/Nodes/BaseNode/BaseNode';
import { BaseNode } from 'components/Tree/Nodes/BaseNode/BaseNode';

import styles from 'components/Tree/Nodes/DefaultNode/default.module.css';
import { NodeProps } from 'reactflow';
import { NodeData } from 'store/DecisionSlice/decisionSlice';

import styles from './default.module.css';

export const DefaultNode = ({ data, ...props }: NodeProps<NodeData>) => {
return (
<BaseNode {...props} status={data.status}>
Expand Down
10 changes: 8 additions & 2 deletions src/components/Tree/Tree.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BoolNode } from 'components/Nodes/BoolNode/BoolNode';
import { DefaultNode } from 'components/Nodes/DefaultNode/DefaultNode';
import { ControlCenter } from 'components/Tree/ControlCenter';
import { DecisionEdge } from 'components/Tree/Edges/DecisionEdge/DecisionEdge';
import { BoolNode } from 'components/Tree/Nodes/BoolNode/BoolNode';
import { DefaultNode } from 'components/Tree/Nodes/DefaultNode/DefaultNode';
import { useDecisionTree, useTreeDirection } from 'hooks';
import React, { useMemo, useState } from 'react';
import ReactFlow, { Edge, MiniMap, Node, useReactFlow, useViewport, XYPosition } from 'reactflow';
Expand All @@ -11,6 +12,10 @@ export interface TreeProps {
mapVisible?: boolean;
}

const edgeTypes = {
decision: DecisionEdge,
};

/**
* Tree - responsible for rendering the decision tree
*/
Expand All @@ -27,6 +32,7 @@ export const Tree = ({ nodes, edges }: TreeProps) => {
<div style={{ width: '100vw', height: '100vh' }} data-testid="decision-tree">
<ReactFlow
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
nodes={nodes}
edges={edges}
onEdgesChange={onEdgesChange}
Expand Down
4 changes: 4 additions & 0 deletions src/components/Tree/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { Tree } from './Tree';
export { BoolNode } from './Nodes/BoolNode/BoolNode';
export type { BoolNodeData } from './Nodes/BoolNode/BoolNode';
export { DefaultNode } from './Nodes/DefaultNode/DefaultNode';
6 changes: 6 additions & 0 deletions src/hooks/useDecisionTree/useDecisionTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const useDecisionTree = (initialTree?: PositionUnawareDecisionTree) => {
onEdgesChange,
markDecisionMade,
markDecisionFocused,
updatePath,
} = useDecTreeStore((state) => state);

/** show a node's direct children and the edges leading to them */
Expand Down Expand Up @@ -64,6 +65,10 @@ export const useDecisionTree = (initialTree?: PositionUnawareDecisionTree) => {
}
}, [initialTree, setStoreTree, showStoreNode]);

const addToPath = (source: string, target: string) => {
updatePath(source, target);
};

return {
tree,
showNode,
Expand All @@ -77,5 +82,6 @@ export const useDecisionTree = (initialTree?: PositionUnawareDecisionTree) => {
onNodesChange,
markDecisionMade,
markDecisionFocused,
addToPath,
} as const;
};
2 changes: 1 addition & 1 deletion src/hooks/useFetchConfig/useFetchConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BoolNodeData } from 'components/Nodes/BoolNode/BoolNode';
import { BoolNodeData } from 'components/Tree';
import { useEffect, useState } from 'react';
import { PositionUnawareDecisionTree, TreeNode } from 'store';
import { BooleanNodeData, NodeData } from 'store/DecisionSlice/decisionSlice';
Expand Down
38 changes: 37 additions & 1 deletion src/store/DagEdgeSlice/dagEdgeSlice.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { DecisionEdgeData } from 'components/Tree/Edges/DecisionEdge/DecisionEdge';
import { applyEdgeChanges, Edge, EdgeChange, OnEdgesChange } from 'reactflow';
import { addDagEdge } from 'store/DagEdgeSlice/dagEdgeUtils';
import { StateCreator } from 'zustand';

interface DagEdgeSliceState {
dagEdges: Edge[];
dagEdges: Edge<DecisionEdgeData>[];
}

interface DagEdgeSliceActions {
Expand All @@ -13,6 +14,10 @@ interface DagEdgeSliceActions {
removeEdgesByTarget: (nodeIds: string[]) => void;
/** Create an edge */
createEdge: (sourceId?: string, targetId?: string) => void;
/** Mark an edge as decision made by source */
addEdgeToPath: (source: string, target: string) => void;
/** Remove an edge from the path by source */
removeEdgeFromPathBySource: (source: string) => void;
}

export interface DagEdgeSlice extends DagEdgeSliceState, DagEdgeSliceActions {}
Expand Down Expand Up @@ -54,4 +59,35 @@ export const createDagEdgeSlice: StateCreator<
'createEdge'
);
},
removeEdgeFromPathBySource: (source: string) => {
const newEdges = get().dagEdges.map((edge) => {
if (edge.source === source) {
edge.data = { decisionMade: false };
}
return edge;
});
set(
{
dagEdges: newEdges,
},
false,
'removeEdgeFromPathBySource'
);
},
addEdgeToPath: (source: string, target: string) => {
const newEdges = get().dagEdges.map((edge) => {
if (edge.source === source && edge.target === target) {
edge.style = { stroke: '#05b485', strokeWidth: '3px' };
edge.data = { decisionMade: true };
}
return edge;
});
set(
{
dagEdges: newEdges,
},
false,
'markEdgeAsDecisionMade'
);
},
});
2 changes: 1 addition & 1 deletion src/store/DagEdgeSlice/dagEdgeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const createDagEdge = (source: string, target: string): Edge => {
hidden: false,
source,
target,
type: 'smoothstep',
type: 'decision',
markerEnd: { type: MarkerType.ArrowClosed },
};
};
6 changes: 5 additions & 1 deletion src/store/TreeSlice/treeSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface TreeSlice {
removeNiblings: (nodeId: string) => void;
markDecisionMade: (nodeId: string) => void;
markDecisionFocused: (nodeId: string) => void;
updatePath: (source: string, target: string) => void;
}

/** The state of the tree, implemented as a shared slice that builds on concrete slices
Expand Down Expand Up @@ -75,7 +76,6 @@ export const createTreeSlice: StateCreator<
const siblingDescendantIds = siblings.flatMap((id) => getDescendantIds(get().tree, id));
get().setStatus([nodeId], 'chosen');
get().setStatus([...siblingDescendantIds, ...siblings], undefined);
get().updateDagNodes(get().tree);
},
markDecisionFocused: (nodeId: string) => {
const siblings = getSiblingIds(get().tree, nodeId);
Expand All @@ -84,4 +84,8 @@ export const createTreeSlice: StateCreator<
get().setStatus([...siblingDescendantIds, ...siblings], undefined);
get().updateDagNodes(get().tree);
},
updatePath: (source: string, target: string) => {
get().removeEdgeFromPathBySource(source);
get().addEdgeToPath(source, target);
},
});

0 comments on commit 78607ee

Please sign in to comment.