Skip to content

Commit 694530b

Browse files
refactor(apollo-react): task replace in case mgmt [MST-7038]
1 parent e8b1e10 commit 694530b

4 files changed

Lines changed: 98 additions & 64 deletions

File tree

packages/apollo-react/src/canvas/components/StageNode/StageNode.stories.tsx

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -966,9 +966,7 @@ const AddAndReplaceTasksStory = () => {
966966
const nodeTypes = useMemo(() => ({ stage: StageNodeWrapper }), [StageNodeWrapper]);
967967
const edgeTypes = useMemo(() => ({ stage: StageEdge }), []);
968968

969-
const [pendingReplaceTask, setPendingReplaceTask] = useState<
970-
{ groupIndex: number; taskIndex: number } | undefined
971-
>();
969+
const [pendingReplaceTask, setPendingReplaceTask] = useState(false);
972970
const [selectedTaskId, setSelectedTaskId] = useState<string | undefined>();
973971
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
974972

@@ -1071,7 +1069,8 @@ const AddAndReplaceTasksStory = () => {
10711069
})
10721070
);
10731071

1074-
setPendingReplaceTask(undefined);
1072+
setPendingReplaceTask(false);
1073+
setSelectedTaskId(undefined);
10751074
},
10761075
[setNodes]
10771076
);
@@ -1118,20 +1117,41 @@ const AddAndReplaceTasksStory = () => {
11181117
setSelectedTaskId(taskId);
11191118
}, []);
11201119

1120+
// Clear task selection when clicking on the canvas background
1121+
const handlePaneClick = useCallback(() => {
1122+
setSelectedTaskId(undefined);
1123+
setPendingReplaceTask(false);
1124+
}, []);
1125+
1126+
// Clear replace state when the node is deselected (e.g., clicking on canvas)
1127+
const handleNodesChange = useCallback(
1128+
(...args: Parameters<typeof onNodesChange>) => {
1129+
onNodesChange(...args);
1130+
const deselected = args[0].some(
1131+
(c) => c.type === 'select' && c.id === 'add-replace-stage' && !c.selected
1132+
);
1133+
if (deselected) {
1134+
setPendingReplaceTask(false);
1135+
setSelectedTaskId(undefined);
1136+
}
1137+
},
1138+
[onNodesChange]
1139+
);
1140+
11211141
// Get current tasks from node state for menu items
11221142
const currentTasks =
11231143
nodesState.find((n) => n.id === 'add-replace-stage')?.data.stageDetails.tasks || [];
11241144

11251145
// Create menu items for task selection
11261146
const taskMenuItems = useMemo<IMenuItem[]>(() => {
1127-
return currentTasks.flatMap((group, groupIndex) =>
1128-
group.map((task, taskIndex) => ({
1147+
return currentTasks.flatMap((group) =>
1148+
group.map((task) => ({
11291149
title: task.label,
11301150
variant: 'item' as const,
11311151
startIcon: task.icon,
11321152
onClick: () => {
11331153
setSelectedTaskId(task.id);
1134-
setPendingReplaceTask({ groupIndex, taskIndex });
1154+
setPendingReplaceTask(true);
11351155
setMenuAnchorEl(null);
11361156
},
11371157
}))
@@ -1147,10 +1167,10 @@ const AddAndReplaceTasksStory = () => {
11471167
...node,
11481168
data: {
11491169
...node.data,
1150-
pendingReplaceTask: pendingReplaceTask,
1170+
...(selectedTaskId && { pendingReplaceTask }),
11511171
stageDetails: {
11521172
...node.data.stageDetails,
1153-
selectedTasks: selectedTaskId ? [selectedTaskId] : undefined,
1173+
selectedTaskId,
11541174
},
11551175
onAddTaskFromToolbox: handleAddTask,
11561176
onReplaceTaskFromToolbox: handleReplaceTask,
@@ -1178,25 +1198,25 @@ const AddAndReplaceTasksStory = () => {
11781198

11791199
// Compute button label based on whether a task is being replaced
11801200
const replaceButtonLabel = useMemo(() => {
1181-
if (pendingReplaceTask) {
1182-
const taskBeingReplaced =
1183-
currentTasks[pendingReplaceTask.groupIndex]?.[pendingReplaceTask.taskIndex];
1201+
if (pendingReplaceTask && selectedTaskId) {
1202+
const taskBeingReplaced = currentTasks.flat().find((t) => t.id === selectedTaskId);
11841203
if (taskBeingReplaced) {
11851204
return `Replacing Task: ${taskBeingReplaced.label}`;
11861205
}
11871206
}
11881207
return 'Replace Task';
1189-
}, [pendingReplaceTask, currentTasks]);
1208+
}, [pendingReplaceTask, selectedTaskId, currentTasks]);
11901209

11911210
return (
11921211
<div style={{ width: '100vw', height: '100vh' }}>
11931212
<ReactFlowProvider>
11941213
<BaseCanvas
11951214
nodes={nodesWithMetadata}
11961215
edges={edges}
1197-
onNodesChange={onNodesChange}
1216+
onNodesChange={handleNodesChange}
11981217
onEdgesChange={onEdgesChange}
11991218
onConnect={onConnect}
1219+
onPaneClick={handlePaneClick}
12001220
nodeTypes={nodeTypes}
12011221
edgeTypes={edgeTypes}
12021222
mode="design"

packages/apollo-react/src/canvas/components/StageNode/StageNode.tsx

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const StageNodeComponent = (props: StageNodeProps) => {
9696
const isException = stageDetails?.isException;
9797
const isReadOnly = !!stageDetails?.isReadOnly;
9898
const icon = stageDetails?.icon;
99-
const selectedTasks = stageDetails?.selectedTasks;
99+
const selectedTaskId = stageDetails?.selectedTaskId;
100100
const defaultContent = stageDetails?.defaultContent || 'Add first task';
101101

102102
const status = execution?.stageStatus?.status;
@@ -130,16 +130,21 @@ const StageNodeComponent = (props: StageNodeProps) => {
130130
const [isReplacingTask, setIsReplacingTask] = useState(false);
131131

132132
useEffect(() => {
133-
if (pendingReplaceTask?.groupIndex != null && pendingReplaceTask?.taskIndex != null) {
134-
const taskGroup = tasks[pendingReplaceTask.groupIndex];
135-
taskStateReference.current = {
136-
isParallel: (taskGroup?.length ?? 0) > 1,
137-
groupIndex: pendingReplaceTask.groupIndex,
138-
taskIndex: pendingReplaceTask.taskIndex,
139-
};
140-
setIsReplacingTask(true);
141-
} else setIsReplacingTask(false);
142-
}, [pendingReplaceTask, tasks]);
133+
if (pendingReplaceTask) {
134+
const match = tasks
135+
.flatMap((group, gi) => group.map((task, ti) => ({ task, groupIndex: gi, taskIndex: ti })))
136+
.find(({ task }) => task.id === selectedTaskId);
137+
138+
if (match) {
139+
taskStateReference.current = {
140+
isParallel: (tasks[match.groupIndex]?.length ?? 0) > 1,
141+
groupIndex: match.groupIndex,
142+
taskIndex: match.taskIndex,
143+
};
144+
setIsReplacingTask(true);
145+
}
146+
}
147+
}, [pendingReplaceTask, selectedTaskId, tasks]);
143148

144149
const [activeDragId, setActiveDragId] = useState<string | null>(null);
145150
const [offsetLeft, setOffsetLeft] = useState(0);
@@ -245,7 +250,13 @@ const StageNodeComponent = (props: StageNodeProps) => {
245250
const items: NodeMenuItem[] = [];
246251

247252
if (onReplaceTaskFromToolbox) {
248-
items.push(getMenuItem('replace-task', 'Replace task', () => setIsReplacingTask(true)));
253+
items.push(
254+
getMenuItem('replace-task', 'Replace task', () => {
255+
const taskId = tasks[groupIndex]?.[taskIndex]?.id;
256+
if (taskId) onTaskClick?.(taskId);
257+
setIsReplacingTask(true);
258+
})
259+
);
249260
items.push(getDivider());
250261
}
251262

@@ -265,7 +276,7 @@ const StageNodeComponent = (props: StageNodeProps) => {
265276

266277
return items;
267278
},
268-
[onReplaceTaskFromToolbox, onTaskGroupModification, reGroupTaskFunction]
279+
[onReplaceTaskFromToolbox, onTaskClick, onTaskGroupModification, reGroupTaskFunction, tasks]
269280
);
270281

271282
const { setSelectedNodeId } = useNodeSelection();
@@ -306,21 +317,14 @@ const StageNodeComponent = (props: StageNodeProps) => {
306317

307318
const handleReplaceTaskToolboxItemSelected = useCallback(
308319
(item: ListItem) => {
309-
setIsReplacingTask(true);
310-
const groupIndex = taskStateReference.current.groupIndex;
311-
const taskIndex = taskStateReference.current.taskIndex;
312-
const taskId = tasks[groupIndex]?.[taskIndex]?.id;
313-
if (taskId) {
314-
onTaskClick?.(taskId);
315-
}
316320
onReplaceTaskFromToolbox?.(
317321
item,
318322
taskStateReference.current.groupIndex,
319323
taskStateReference.current.taskIndex
320324
);
321325
setIsReplacingTask(false);
322326
},
323-
[onReplaceTaskFromToolbox, onTaskClick, tasks]
327+
[onReplaceTaskFromToolbox]
324328
);
325329

326330
const handleConfigurations: HandleGroupManifest[] = useMemo(
@@ -619,7 +623,7 @@ const StageNodeComponent = (props: StageNodeProps) => {
619623
key={task.id}
620624
task={task}
621625
taskExecution={taskExecution}
622-
isSelected={!!selectedTasks?.includes(task.id)}
626+
isSelected={selectedTaskId === task.id}
623627
isParallel={isParallel}
624628
contextMenuItems={contextMenuItems(
625629
isParallel,

packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ export interface StageNodeProps extends NodeProps {
2727
dragging: boolean;
2828
selected: boolean;
2929
id: string;
30-
pendingReplaceTask?: {
31-
groupIndex: number;
32-
taskIndex: number;
33-
};
30+
pendingReplaceTask?: boolean;
3431
stageDetails: {
3532
label: string;
3633
defaultContent?: string;
@@ -42,7 +39,7 @@ export interface StageNodeProps extends NodeProps {
4239
isException?: boolean;
4340
isReadOnly?: boolean;
4441
tasks: StageTaskItem[][];
45-
selectedTasks?: string[];
42+
selectedTaskId?: string;
4643
};
4744
addTaskLabel?: string;
4845
addTaskLoading?: boolean;

packages/apollo-react/src/canvas/components/Toolbox/Toolbox.tsx

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export function Toolbox<T>({
101101
parentItem: ListItem<T> | null;
102102
}>();
103103

104+
const containerRef = useRef<HTMLDivElement>(null);
104105
const transitionTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
105106
const searchIdRef = useRef(0);
106107
const initialItemsRef = useRef(initialItems);
@@ -297,8 +298,18 @@ export function Toolbox<T>({
297298
}
298299
};
299300

301+
const handleClickOutside = (e: MouseEvent) => {
302+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
303+
onClose();
304+
}
305+
};
306+
300307
document.addEventListener('keydown', handleKeyDown);
301-
return () => document.removeEventListener('keydown', handleKeyDown);
308+
document.addEventListener('mousedown', handleClickOutside);
309+
return () => {
310+
document.removeEventListener('keydown', handleKeyDown);
311+
document.removeEventListener('mousedown', handleClickOutside);
312+
};
302313
}, [
303314
isSearching,
304315
navigationStack.canGoBack,
@@ -309,27 +320,29 @@ export function Toolbox<T>({
309320
]);
310321

311322
return (
312-
<Column px={20} py={12} gap={12} w={320} h={440}>
313-
<Header
314-
title={currentParentItem?.name || title}
315-
onBack={handleBackTransition}
316-
showBackButton={navigationStack.canGoBack}
317-
/>
318-
319-
<SearchBox value={search} onChange={handleSearch} clear={clearSearch} placeholder="Search" />
320-
321-
<AnimatedContainer>
322-
<AnimatedContent entering={isTransitioning} direction={animationDirection}>
323-
<ListView
324-
isLoading={childrenLoading || searchLoading || loading}
325-
items={isSearching && !isSearchingInitialItems ? searchedItems : items}
326-
emptyStateMessage={isSearching ? 'No matching nodes found' : 'No nodes found'}
327-
enableSections={!isSearching}
328-
onItemClick={handleItemSelect}
329-
onItemHover={onItemHover}
330-
/>
331-
</AnimatedContent>
332-
</AnimatedContainer>
333-
</Column>
323+
<div ref={containerRef}>
324+
<Column px={20} py={12} gap={12} w={320} h={440}>
325+
<Header
326+
title={currentParentItem?.name || title}
327+
onBack={handleBackTransition}
328+
showBackButton={navigationStack.canGoBack}
329+
/>
330+
331+
<SearchBox value={search} onChange={handleSearch} clear={clearSearch} placeholder="Search" />
332+
333+
<AnimatedContainer>
334+
<AnimatedContent entering={isTransitioning} direction={animationDirection}>
335+
<ListView
336+
isLoading={childrenLoading || searchLoading || loading}
337+
items={isSearching && !isSearchingInitialItems ? searchedItems : items}
338+
emptyStateMessage={isSearching ? 'No matching nodes found' : 'No nodes found'}
339+
enableSections={!isSearching}
340+
onItemClick={handleItemSelect}
341+
onItemHover={onItemHover}
342+
/>
343+
</AnimatedContent>
344+
</AnimatedContainer>
345+
</Column>
346+
</div>
334347
);
335348
}

0 commit comments

Comments
 (0)