Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react';
import { BaseEdge, Edge, EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react';
import { Collapsible, Dropdown, StepElement, Track } from 'components';
import useEdgeAdd from 'hooks/flow/useEdgeAdd';
import { CSSProperties, memo, useEffect, useState } from 'react';
Expand All @@ -33,6 +33,8 @@ function reorderElements<T>(elements: T[], activeId: string | number, overId: st
return arrayMove(elements, oldIndex, newIndex);
}

type HighlightData = { readonly isHighlighted?: boolean; readonly isDimmed?: boolean };

function CustomEdge({
id,
label,
Expand All @@ -44,7 +46,8 @@ function CustomEdge({
targetPosition,
style,
markerEnd,
}: EdgeProps) {
data: highlightData,
}: EdgeProps<Edge<HighlightData>>) {
const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({
sourceX,
sourceY,
Expand Down Expand Up @@ -138,7 +141,7 @@ function CustomEdge({
transform: `translate(${edgeCenterX}px, ${edgeCenterY}px) translate(-50%, -50%)`,
}}
onClick={() => {}}
className="edge-button nodrag nopan"
className={`edge-button nodrag nopan${highlightData?.isDimmed ? ' edge-button--dimmed' : ''}${highlightData?.isHighlighted ? ' edge-button--highlighted' : ''}`}
>
{label ?? '+'}
</button>
Expand Down
34 changes: 34 additions & 0 deletions GUI/src/components/Flow/NodeTypes/Node.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
@import 'src/styles/settings/variables/_colors';

.react-flow__node {
transition:
opacity 0.2s ease,
filter 0.2s ease;

.icon {
color: rgba(0, 0, 0, 0.54);
}
Expand Down Expand Up @@ -66,10 +70,40 @@
padding-top: 5px;
}
}

&.node-highlighted {
opacity: 1;
}

&.node-highlighted--source {
filter: drop-shadow(0 0 6px rgba(69, 135, 188, 0.55));
outline: 2px solid get-color(sapphire-blue-7);
border-radius: 6px;
outline-offset: 3px;
}
}

[data-theme='dark'] .react-flow__node {
.icon {
color: var(--dark-text-label);
}
}

.react-flow__edge {
transition: opacity 0.2s ease;

&.edge-highlighted {
opacity: 1;

.react-flow__edge-path {
stroke: get-color(sapphire-blue-7);
stroke-width: 2;
}
}
}

.edge-button--highlighted {
background-color: get-color(sapphire-blue-7) !important;
color: white !important;
border-color: get-color(sapphire-blue-7) !important;
}
8 changes: 6 additions & 2 deletions GUI/src/components/FlowBuilder/FlowBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import McqBranchSelectModal from 'components/Flow/McqBranchSelectModal';
import nodeTypes from 'components/Flow/NodeTypes';
import useLayout from 'hooks/flow/useLayout';
import useMcqConnect from 'hooks/flow/useMcqConnect';
import { useNodeHighlight } from 'hooks/flow/useNodeHighlight';
import { useOnNodesDelete } from 'hooks/flow/useOnNodeDelete';
import { ChangeEventHandler, FC, useCallback, useEffect, useState } from 'react';
import '@xyflow/react/dist/style.css';
Expand Down Expand Up @@ -63,6 +64,7 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {

const { runLayout } = useLayout();
const { pendingConnection, handleConnect, isValidConnection, confirmBranch, cancelBranchSelection } = useMcqConnect();
const { displayNodes, displayEdges, handleNodeClick, handlePaneClick } = useNodeHighlight(nodes, edges);

const onConnect = useCallback(
(connection: { source?: string | null; target?: string | null; sourceHandle?: string | null }) => {
Expand Down Expand Up @@ -141,8 +143,8 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
return (
<>
<ReactFlow
nodes={nodes}
edges={edges}
nodes={displayNodes}
edges={displayEdges}
onNodesChange={useNewServiceStore.getState().onNodesChange}
onEdgesChange={useNewServiceStore.getState().onEdgesChange}
snapToGrid
Expand All @@ -156,6 +158,8 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
await fitView({ duration: 200, padding: 5 });
}}
nodesDraggable={false}
onNodeClick={handleNodeClick}
onPaneClick={handlePaneClick}
onSelectionChange={onSelectionChange}
onConnect={onConnect}
onEdgesDelete={(edges) => {
Expand Down
64 changes: 64 additions & 0 deletions GUI/src/hooks/flow/useNodeHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { getConnectedEdges, getIncomers, getOutgoers } from '@xyflow/react';
import type { Edge, Node } from '@xyflow/react';
import { MouseEvent, useCallback, useMemo, useState } from 'react';

const stripClasses = (cls: string, ...toRemove: string[]) =>
cls
.replace(new RegExp(String.raw`\b(${toRemove.join('|')})\b`, 'g'), '')
.replace(/\s+/g, ' ')
.trim();

export function useNodeHighlight(nodes: Node[], edges: Edge[]) {
const [highlightedNodeId, setHighlightedNodeId] = useState<string | null>(null);

const handleNodeClick = useCallback((_event: MouseEvent, node: Node) => {
if (node.type === 'ghost' || node.type === 'start') return;
setHighlightedNodeId(node.id);
}, []);

const handlePaneClick = useCallback(() => {
setHighlightedNodeId(null);
}, []);

const { displayNodes, displayEdges } = useMemo(() => {
if (!highlightedNodeId) {
return { displayNodes: nodes, displayEdges: edges };
}

const node = nodes.find((n) => n.id === highlightedNodeId);
if (!node) {
return { displayNodes: nodes, displayEdges: edges };
}

const connectedEdges = getConnectedEdges([node], edges);
const connectedEdgeIds = new Set(connectedEdges.map((e) => e.id));

const incomers = getIncomers(node, nodes, edges);
const outgoers = getOutgoers(node, nodes, edges);
const connectedNodeIds = new Set([highlightedNodeId, ...incomers.map((n) => n.id), ...outgoers.map((n) => n.id)]);

const displayNodes = nodes.map((n) => {
const base = stripClasses(n.className ?? '', 'node-highlighted', 'node-highlighted--source', 'node-dimmed');
const isSource = n.id === highlightedNodeId;
const isConnected = connectedNodeIds.has(n.id);
let extra = 'node-dimmed';
if (isSource) extra = 'node-highlighted node-highlighted--source';
else if (isConnected) extra = 'node-highlighted';
return { ...n, className: `${base} ${extra}`.trim() };
});

const displayEdges = edges.map((e) => {
const isHighlighted = connectedEdgeIds.has(e.id);
const base = stripClasses(e.className ?? '', 'edge-highlighted', 'edge-dimmed');
return {
...e,
className: `${base} ${isHighlighted ? 'edge-highlighted' : 'edge-dimmed'}`.trim(),
data: { ...(e.data as object | undefined), isHighlighted, isDimmed: !isHighlighted },
};
});

return { displayNodes, displayEdges };
}, [highlightedNodeId, nodes, edges]);

return { displayNodes, displayEdges, handleNodeClick, handlePaneClick, highlightedNodeId };
}
Loading