From 8e31077bcbce7920d74d1834928b84eca6f4e7a4 Mon Sep 17 00:00:00 2001 From: 1AhmedYasser <26207361+1AhmedYasser@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:02:28 +0300 Subject: [PATCH 1/2] chore(1058): Added node dependency highlighting --- .../components/Flow/EdgeTypes/CustomEdge.tsx | 6 +- GUI/src/components/Flow/NodeTypes/Node.scss | 34 ++++++++++ .../components/FlowBuilder/FlowBuilder.tsx | 8 ++- GUI/src/hooks/flow/useNodeHighlight.ts | 64 +++++++++++++++++++ 4 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 GUI/src/hooks/flow/useNodeHighlight.ts diff --git a/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx b/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx index 38166ab56..2e1f7213d 100644 --- a/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx +++ b/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx @@ -33,6 +33,8 @@ function reorderElements(elements: T[], activeId: string | number, overId: st return arrayMove(elements, oldIndex, newIndex); } +type HighlightData = { isHighlighted?: boolean; isDimmed?: boolean }; + function CustomEdge({ id, label, @@ -44,7 +46,9 @@ function CustomEdge({ targetPosition, style, markerEnd, + data, }: EdgeProps) { + const highlightData = data as HighlightData | undefined; const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({ sourceX, sourceY, @@ -138,7 +142,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 ?? '+'} diff --git a/GUI/src/components/Flow/NodeTypes/Node.scss b/GUI/src/components/Flow/NodeTypes/Node.scss index 7f8424849..b5a97d60c 100644 --- a/GUI/src/components/Flow/NodeTypes/Node.scss +++ b/GUI/src/components/Flow/NodeTypes/Node.scss @@ -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); } @@ -66,6 +70,17 @@ 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 { @@ -73,3 +88,22 @@ 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; +} diff --git a/GUI/src/components/FlowBuilder/FlowBuilder.tsx b/GUI/src/components/FlowBuilder/FlowBuilder.tsx index 7ff089974..c068ceabe 100644 --- a/GUI/src/components/FlowBuilder/FlowBuilder.tsx +++ b/GUI/src/components/FlowBuilder/FlowBuilder.tsx @@ -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'; @@ -63,6 +64,7 @@ const FlowBuilder: FC = ({ 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 }) => { @@ -141,8 +143,8 @@ const FlowBuilder: FC = ({ nodes, edges }) => { return ( <> = ({ nodes, edges }) => { await fitView({ duration: 200, padding: 5 }); }} nodesDraggable={false} + onNodeClick={handleNodeClick} + onPaneClick={handlePaneClick} onSelectionChange={onSelectionChange} onConnect={onConnect} onEdgesDelete={(edges) => { diff --git a/GUI/src/hooks/flow/useNodeHighlight.ts b/GUI/src/hooks/flow/useNodeHighlight.ts new file mode 100644 index 000000000..472186f6f --- /dev/null +++ b/GUI/src/hooks/flow/useNodeHighlight.ts @@ -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(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 }; +} From 9850e404ff1c7f29af688998c53eb0793e085f95 Mon Sep 17 00:00:00 2001 From: 1AhmedYasser <26207361+1AhmedYasser@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:28:22 +0300 Subject: [PATCH 2/2] chore(1085): Addressed PR comments --- GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx b/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx index 2e1f7213d..f25cf559e 100644 --- a/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx +++ b/GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx @@ -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'; @@ -33,7 +33,7 @@ function reorderElements(elements: T[], activeId: string | number, overId: st return arrayMove(elements, oldIndex, newIndex); } -type HighlightData = { isHighlighted?: boolean; isDimmed?: boolean }; +type HighlightData = { readonly isHighlighted?: boolean; readonly isDimmed?: boolean }; function CustomEdge({ id, @@ -46,9 +46,8 @@ function CustomEdge({ targetPosition, style, markerEnd, - data, -}: EdgeProps) { - const highlightData = data as HighlightData | undefined; + data: highlightData, +}: EdgeProps>) { const [edgePath, edgeCenterX, edgeCenterY] = getBezierPath({ sourceX, sourceY,