diff --git a/packages/apollo-react/src/canvas/components/StageNode/DraggableTask.tsx b/packages/apollo-react/src/canvas/components/StageNode/DraggableTask.tsx index 496acb699..ca53bf951 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/DraggableTask.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/DraggableTask.tsx @@ -256,7 +256,7 @@ const DraggableTaskComponent = ({ )} - {task.isAdhoc && !onTaskPlay && ( + {task.hasEntryCondition && ( , isAdhoc: true, + hasEntryCondition: true, }, ], [ diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts b/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts index 4f1832628..653f29663 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.styles.ts @@ -305,6 +305,19 @@ export const StageChip = styled.button` } `; +export const StageAdhocSection = styled.div` + display: flex; + flex-direction: column; + gap: ${Spacing.SpacingS}; + margin-top: ${Spacing.SpacingS}; +`; + +export const StageAdhocHeaderSection = styled.div` + height: 36px; + display: flex; + align-items: center; +`; + export const StageTaskDragPlaceholder = styled.div<{ isTargetParallel?: boolean }>` display: flex; align-items: center; diff --git a/packages/apollo-react/src/canvas/components/StageNode/StageNode.tsx b/packages/apollo-react/src/canvas/components/StageNode/StageNode.tsx index 774b6f54b..90e1bd188 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.tsx +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.tsx @@ -20,6 +20,7 @@ import { FontVariantToken, Icon, Padding, Spacing } from '@uipath/apollo-core'; import { Column, Row } from '@uipath/apollo-react/canvas/layouts'; import { Position, useStore, useViewport } from '@uipath/apollo-react/canvas/xyflow/react'; import { + ApCircularProgress, ApIcon, ApIconButton, ApLink, @@ -28,6 +29,7 @@ import { } from '@uipath/apollo-react/material'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; +import { EntryConditionIcon, ExitConditionIcon, PlayIcon, ReturnToOriginIcon } from '../../icons'; import type { HandleGroupManifest } from '../../schema/node-definition'; import { useConnectedHandles } from '../BaseCanvas/ConnectedHandlesContext'; import { useButtonHandles } from '../ButtonHandle/useButtonHandles'; @@ -36,11 +38,12 @@ import { FloatingCanvasPanel } from '../FloatingCanvasPanel'; import { NodeContextMenu, type NodeMenuItem } from '../NodeContextMenu'; import { useNodeSelection } from '../NodePropertiesPanel/hooks'; import { type ListItem, Toolbox } from '../Toolbox'; -import { EntryConditionIcon, ExitConditionIcon, ReturnToOriginIcon } from '../../icons'; import { DraggableTask, TaskContent } from './DraggableTask'; import { INDENTATION_WIDTH, STAGE_CONTENT_INSET, + StageAdhocHeaderSection, + StageAdhocSection, StageChip, StageContainer, StageContent, @@ -54,8 +57,8 @@ import { StageTitleContainer, StageTitleInput, } from './StageNode.styles'; -import { StageHeaderChipType } from './StageNode.types'; import type { StageNodeProps } from './StageNode.types'; +import { StageHeaderChipType } from './StageNode.types'; import { flattenTasks, getProjection, reorderTasks } from './StageNode.utils'; import { getContextMenuItems, getDivider, getMenuItem } from './StageNodeTaskUtilities'; @@ -73,6 +76,48 @@ const CHIP_ICONS: Record = { [StageHeaderChipType.CaseCompletion]: , }; +const AdhocTaskPlayButton = memo( + ({ taskId, onTaskPlay }: { taskId: string; onTaskPlay: (taskId: string) => Promise }) => { + const [playLoading, setPlayLoading] = useState(false); + + const handlePlayClick = useCallback( + async (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setPlayLoading(true); + try { + await onTaskPlay(taskId); + } catch { + // Do nothing + } finally { + setPlayLoading(false); + } + }, + [onTaskPlay, taskId] + ); + + return ( + + e.stopPropagation()} + className="task-menu-icon-button" + sx={{ + color: 'var(--uix-canvas-primary) !important', + minWidth: 'unset !important', + width: `${Spacing.SpacingL} !important`, + height: `${Spacing.SpacingL} !important`, + padding: '0 !important', + }} + > + {playLoading ? : } + + + ); + } +); + const StageNodeComponent = (props: StageNodeProps) => { const { dragging, @@ -102,7 +147,15 @@ const StageNodeComponent = (props: StageNodeProps) => { const taskWidth = width ? width - STAGE_CONTENT_INSET : undefined; - const tasks = useMemo(() => stageDetails?.tasks || [], [stageDetails?.tasks]); + const allTasks = useMemo(() => stageDetails?.tasks || [], [stageDetails?.tasks]); + + // Split tasks into regular (draggable) and ad hoc (separate section) + const tasks = useMemo( + () => allTasks.filter((group) => group.some((t) => !t.isAdhoc)), + [allTasks] + ); + const adhocTasks = useMemo(() => allTasks.flat().filter((t) => t.isAdhoc), [allTasks]); + const flatTasks = useMemo(() => tasks.flat(), [tasks]); const taskIds = useMemo(() => flatTasks.map((task) => task.id), [flatTasks]); @@ -145,20 +198,20 @@ const StageNodeComponent = (props: StageNodeProps) => { useEffect(() => { if (pendingReplaceTask) { - const match = tasks + const match = allTasks .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, + isParallel: (allTasks[match.groupIndex]?.length ?? 0) > 1, groupIndex: match.groupIndex, taskIndex: match.taskIndex, }; setIsReplacingTask(true); } } - }, [pendingReplaceTask, selectedTaskId, tasks]); + }, [pendingReplaceTask, selectedTaskId, allTasks]); const [activeDragId, setActiveDragId] = useState(null); const [offsetLeft, setOffsetLeft] = useState(0); @@ -623,7 +676,7 @@ const StageNodeComponent = (props: StageNodeProps) => { - {!tasks || tasks.length === 0 ? ( + {tasks.length === 0 && adhocTasks.length === 0 ? ( {(onTaskAdd || onAddTaskFromToolbox) && !isReadOnly ? ( { )} ) : ( - - - {/* Disable dragging and panning the canvas when dragging a task */} - - {tasks.map((taskGroup, groupIndex) => { - const isParallel = taskGroup.length > 1; - return ( - - {isParallel && } - - {isParallel && ( - - - Parallel - - - )} - {taskGroup.map((task, taskIndex) => { - const taskExecution = execution?.taskStatus?.[task.id]; - return ( - 1, - (tasks[groupIndex + 1]?.length ?? 0) > 1 - )} - onTaskClick={handleTaskClick} - projectedDepth={ - task.id === activeDragId && projected - ? projected.depth - : undefined - } - onTaskPlay={onTaskPlay} - isDragDisabled={!onTaskReorder} - zoom={zoom} - {...((onTaskGroupModification || onReplaceTaskFromToolbox) && { - onMenuOpen: () => { - taskStateReference.current = { + <> + {tasks.length > 0 && ( + + + {/* Disable dragging and panning the canvas when dragging a task */} + + {tasks.map((taskGroup, groupIndex) => { + const isParallel = taskGroup.length > 1; + return ( + + {isParallel && } + + {isParallel && ( + + + Parallel + + + )} + {taskGroup.map((task, taskIndex) => { + const taskExecution = execution?.taskStatus?.[task.id]; + return ( + - ); - })} - - - ); - })} - - - {createPortal( - - {activeTask ? ( -
- - - -
- ) : null} -
, - document.body + tasks.length, + taskGroup.length, + (tasks[groupIndex - 1]?.length ?? 0) > 1, + (tasks[groupIndex + 1]?.length ?? 0) > 1 + )} + onTaskClick={handleTaskClick} + projectedDepth={ + task.id === activeDragId && projected + ? projected.depth + : undefined + } + onTaskPlay={onTaskPlay} + isDragDisabled={!onTaskReorder} + zoom={zoom} + {...((onTaskGroupModification || onReplaceTaskFromToolbox) && { + onMenuOpen: () => { + taskStateReference.current = { + isParallel, + groupIndex, + taskIndex, + }; + }, + })} + /> + ); + })} +
+
+ ); + })} +
+
+ {createPortal( + + {activeTask ? ( +
+ + + +
+ ) : null} +
, + document.body + )} +
+ )} + {adhocTasks.length > 0 && ( + + + + Adhoc tasks + + + + {adhocTasks.map((task) => { + const taskExecution = execution?.taskStatus?.[task.id]; + return ( + handleTaskClick(e, task.id)} + > + + {onTaskPlay && ( + + )} + {task.hasEntryCondition && ( + + + + + + )} + + ); + })} + + )} - + )}
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 f9f33bf6e..4c9c79c66 100644 --- a/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts +++ b/packages/apollo-react/src/canvas/components/StageNode/StageNode.types.ts @@ -23,6 +23,7 @@ export interface StageTaskItem { label: string; icon?: React.ReactElement; isAdhoc?: boolean; + hasEntryCondition?: boolean; } export enum StageHeaderChipType {