From 96638db51b99cb5c8bec3ebf5c339a024d1a2232 Mon Sep 17 00:00:00 2001 From: Kodudula Ashish Reddy Date: Fri, 27 Feb 2026 09:33:04 +0530 Subject: [PATCH] refactor(apollo-react): task replace in case mgmt [MST-7038] --- .../StageNode/StageNode.stories.tsx | 48 ++++++++++----- .../canvas/components/StageNode/StageNode.tsx | 48 ++++++++------- .../components/StageNode/StageNode.types.ts | 7 +-- .../src/canvas/components/Toolbox/Toolbox.tsx | 59 +++++++++++-------- 4 files changed, 98 insertions(+), 64 deletions(-) diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx index 22a271203..e1f767ddb 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx @@ -966,9 +966,7 @@ const AddAndReplaceTasksStory = () => { const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), [StageNodeWrapper]); const edgeTypes = useMemo(() => ({ stage: StageEdge }), []); - const [pendingReplaceTask, setPendingReplaceTask] = useState< - { groupIndex: number; taskIndex: number } | undefined - >(); + const [pendingReplaceTask, setPendingReplaceTask] = useState(false); const [selectedTaskId, setSelectedTaskId] = useState(); const [menuAnchorEl, setMenuAnchorEl] = useState(null); @@ -1071,7 +1069,8 @@ const AddAndReplaceTasksStory = () => { }) ); - setPendingReplaceTask(undefined); + setPendingReplaceTask(false); + setSelectedTaskId(undefined); }, [setNodes] ); @@ -1118,20 +1117,41 @@ const AddAndReplaceTasksStory = () => { setSelectedTaskId(taskId); }, []); + // Clear task selection when clicking on the canvas background + const handlePaneClick = useCallback(() => { + setSelectedTaskId(undefined); + setPendingReplaceTask(false); + }, []); + + // Clear replace state when the node is deselected (e.g., clicking on canvas) + const handleNodesChange = useCallback( + (...args: Parameters) => { + onNodesChange(...args); + const deselected = args[0].some( + (c) => c.type === 'select' && c.id === 'add-replace-stage' && !c.selected + ); + if (deselected) { + setPendingReplaceTask(false); + setSelectedTaskId(undefined); + } + }, + [onNodesChange] + ); + // Get current tasks from node state for menu items const currentTasks = nodesState.find((n) => n.id === 'add-replace-stage')?.data.stageDetails.tasks || []; // Create menu items for task selection const taskMenuItems = useMemo(() => { - return currentTasks.flatMap((group, groupIndex) => - group.map((task, taskIndex) => ({ + return currentTasks.flatMap((group) => + group.map((task) => ({ title: task.label, variant: 'item' as const, startIcon: task.icon, onClick: () => { setSelectedTaskId(task.id); - setPendingReplaceTask({ groupIndex, taskIndex }); + setPendingReplaceTask(true); setMenuAnchorEl(null); }, })) @@ -1147,10 +1167,10 @@ const AddAndReplaceTasksStory = () => { ...node, data: { ...node.data, - pendingReplaceTask: pendingReplaceTask, + ...(selectedTaskId && { pendingReplaceTask }), stageDetails: { ...node.data.stageDetails, - selectedTasks: selectedTaskId ? [selectedTaskId] : undefined, + selectedTaskId, }, onAddTaskFromToolbox: handleAddTask, onReplaceTaskFromToolbox: handleReplaceTask, @@ -1178,15 +1198,14 @@ const AddAndReplaceTasksStory = () => { // Compute button label based on whether a task is being replaced const replaceButtonLabel = useMemo(() => { - if (pendingReplaceTask) { - const taskBeingReplaced = - currentTasks[pendingReplaceTask.groupIndex]?.[pendingReplaceTask.taskIndex]; + if (pendingReplaceTask && selectedTaskId) { + const taskBeingReplaced = currentTasks.flat().find((t) => t.id === selectedTaskId); if (taskBeingReplaced) { return `Replacing Task: ${taskBeingReplaced.label}`; } } return 'Replace Task'; - }, [pendingReplaceTask, currentTasks]); + }, [pendingReplaceTask, selectedTaskId, currentTasks]); return (
@@ -1194,9 +1213,10 @@ const AddAndReplaceTasksStory = () => { { const isException = stageDetails?.isException; const isReadOnly = !!stageDetails?.isReadOnly; const icon = stageDetails?.icon; - const selectedTasks = stageDetails?.selectedTasks; + const selectedTaskId = stageDetails?.selectedTaskId; const defaultContent = stageDetails?.defaultContent || 'Add first task'; const status = execution?.stageStatus?.status; @@ -130,16 +130,21 @@ const StageNodeComponent = (props: StageNodeProps) => { const [isReplacingTask, setIsReplacingTask] = useState(false); useEffect(() => { - if (pendingReplaceTask?.groupIndex != null && pendingReplaceTask?.taskIndex != null) { - const taskGroup = tasks[pendingReplaceTask.groupIndex]; - taskStateReference.current = { - isParallel: (taskGroup?.length ?? 0) > 1, - groupIndex: pendingReplaceTask.groupIndex, - taskIndex: pendingReplaceTask.taskIndex, - }; - setIsReplacingTask(true); - } else setIsReplacingTask(false); - }, [pendingReplaceTask, tasks]); + if (pendingReplaceTask) { + const match = tasks + .flatMap((group, gi) => group.map((task, ti) => ({ task, groupIndex: gi, taskIndex: ti }))) + .find(({ task }) => task.id === selectedTaskId); + + if (match) { + taskStateReference.current = { + isParallel: (tasks[match.groupIndex]?.length ?? 0) > 1, + groupIndex: match.groupIndex, + taskIndex: match.taskIndex, + }; + setIsReplacingTask(true); + } + } + }, [pendingReplaceTask, selectedTaskId, tasks]); const [activeDragId, setActiveDragId] = useState(null); const [offsetLeft, setOffsetLeft] = useState(0); @@ -245,7 +250,13 @@ const StageNodeComponent = (props: StageNodeProps) => { const items: NodeMenuItem[] = []; if (onReplaceTaskFromToolbox) { - items.push(getMenuItem('replace-task', 'Replace task', () => setIsReplacingTask(true))); + items.push( + getMenuItem('replace-task', 'Replace task', () => { + const taskId = tasks[groupIndex]?.[taskIndex]?.id; + if (taskId) onTaskClick?.(taskId); + setIsReplacingTask(true); + }) + ); items.push(getDivider()); } @@ -265,7 +276,7 @@ const StageNodeComponent = (props: StageNodeProps) => { return items; }, - [onReplaceTaskFromToolbox, onTaskGroupModification, reGroupTaskFunction] + [onReplaceTaskFromToolbox, onTaskClick, onTaskGroupModification, reGroupTaskFunction, tasks] ); const { setSelectedNodeId } = useNodeSelection(); @@ -306,13 +317,6 @@ const StageNodeComponent = (props: StageNodeProps) => { const handleReplaceTaskToolboxItemSelected = useCallback( (item: ListItem) => { - setIsReplacingTask(true); - const groupIndex = taskStateReference.current.groupIndex; - const taskIndex = taskStateReference.current.taskIndex; - const taskId = tasks[groupIndex]?.[taskIndex]?.id; - if (taskId) { - onTaskClick?.(taskId); - } onReplaceTaskFromToolbox?.( item, taskStateReference.current.groupIndex, @@ -320,7 +324,7 @@ const StageNodeComponent = (props: StageNodeProps) => { ); setIsReplacingTask(false); }, - [onReplaceTaskFromToolbox, onTaskClick, tasks] + [onReplaceTaskFromToolbox] ); const handleConfigurations: HandleGroupManifest[] = useMemo( @@ -619,7 +623,7 @@ const StageNodeComponent = (props: StageNodeProps) => { key={task.id} task={task} taskExecution={taskExecution} - isSelected={!!selectedTasks?.includes(task.id)} + isSelected={selectedTaskId === task.id} isParallel={isParallel} contextMenuItems={contextMenuItems( isParallel, diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts index 1f9ff12f9..5435a6f9c 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts @@ -27,10 +27,7 @@ export interface StageNodeProps extends NodeProps { dragging: boolean; selected: boolean; id: string; - pendingReplaceTask?: { - groupIndex: number; - taskIndex: number; - }; + pendingReplaceTask?: boolean; stageDetails: { label: string; defaultContent?: string; @@ -42,7 +39,7 @@ export interface StageNodeProps extends NodeProps { isException?: boolean; isReadOnly?: boolean; tasks: StageTaskItem[][]; - selectedTasks?: string[]; + selectedTaskId?: string; }; addTaskLabel?: string; addTaskLoading?: boolean; diff --git a/packages/apollo-react/src/canvas/components/Toolbox/Toolbox.tsx b/packages/apollo-react/src/canvas/components/Toolbox/Toolbox.tsx index b1ccecb63..9304c457e 100644 --- a/packages/apollo-react/src/canvas/components/Toolbox/Toolbox.tsx +++ b/packages/apollo-react/src/canvas/components/Toolbox/Toolbox.tsx @@ -101,6 +101,7 @@ export function Toolbox({ parentItem: ListItem | null; }>(); + const containerRef = useRef(null); const transitionTimeoutRef = useRef | undefined>(undefined); const searchIdRef = useRef(0); const initialItemsRef = useRef(initialItems); @@ -297,8 +298,18 @@ export function Toolbox({ } }; + const handleClickOutside = (e: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + onClose(); + } + }; + document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('mousedown', handleClickOutside); + }; }, [ isSearching, navigationStack.canGoBack, @@ -309,27 +320,29 @@ export function Toolbox({ ]); return ( - -
- - - - - - - - - +
+ +
+ + + + + + + + + +
); }