[aidan] ux/workflows: add workflow step removal (#91)

This commit is contained in:
Aidan
2026-06-16 19:02:54 -07:00
committed by GitHub
parent 5a5046d5a9
commit 11bbabd0ed
3 changed files with 103 additions and 3 deletions
@@ -1,9 +1,12 @@
import React from 'react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import InputBase from '@mui/material/InputBase';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import Tooltip from '@mui/material/Tooltip';
import DeleteOutlineRounded from '@mui/icons-material/DeleteOutlineRounded';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { useAppDispatch } from '@/shared/hooks';
@@ -124,6 +127,33 @@ export default function GeneralFacet({ draft, setDraft }: { draft: Workflow; set
}}
sx={{ flex: 1, fontSize: INPUT_FS, color: c.text.primary, border: `1px solid ${c.border.subtle}`, borderRadius: `${c.radius.md}px`, px: 1.25, py: 0.6, lineHeight: 1.4 }}
/>
<Tooltip title={draft.steps.length > 1 ? 'Remove step' : 'Workflow needs at least one step'}>
<span>
<IconButton
size="small"
aria-label={`Remove step ${idx + 1}`}
disabled={draft.steps.length <= 1}
onClick={() => {
if (draft.steps.length <= 1) return;
setDraft({ ...draft, steps: draft.steps.filter((_, i) => i !== idx) });
}}
sx={{
width: 26,
height: 26,
p: 0,
mt: 0.3,
color: c.text.muted,
flexShrink: 0,
'&:hover': {
color: c.status.error,
bgcolor: c.status.errorBg,
},
}}
>
<DeleteOutlineRounded sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
</Box>
))}
</Box>
+42 -2
View File
@@ -11,8 +11,11 @@
import React from 'react';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import DeleteOutlineRounded from '@mui/icons-material/DeleteOutlineRounded';
import KeyboardArrowDownRounded from '@mui/icons-material/KeyboardArrowDownRounded';
import CheckRounded from '@mui/icons-material/CheckRounded';
import CloseRounded from '@mui/icons-material/CloseRounded';
@@ -29,6 +32,7 @@ interface Props {
framed?: boolean;
// Edit mode
onChangeStep?: (idx: number, text: string) => void;
onDeleteStep?: (idx: number, id: string) => void;
// Expand mode
expandable?: boolean;
expandedIds?: string[];
@@ -46,7 +50,7 @@ const CONNECTOR_X = CIRCLE_SIZE / 2;
export default function StepList(props: Props) {
const {
steps, framed, onChangeStep,
steps, framed, onChangeStep, onDeleteStep,
expandable, expandedIds, onToggleExpand,
stepStatuses, activeStepSubtitle, activeStepDuration,
maxVisible = 4,
@@ -86,6 +90,42 @@ export default function StepList(props: Props) {
const label = (s.label || '').trim() || firstWords(s.text, 6);
const rawBody = (s.text || '').trim();
const hasExpandableBody = Boolean(expandable && rawBody);
const canDelete = Boolean(onDeleteStep && steps.length > 1);
const deleteButton = onDeleteStep ? (
<Tooltip title={canDelete ? 'Remove step' : 'Workflow needs at least one step'}>
<span
onClick={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
style={{ display: 'inline-flex' }}
>
<IconButton
size="small"
aria-label={`Remove step ${idx + 1}`}
disabled={!canDelete}
onClick={(e) => {
e.stopPropagation();
if (canDelete) onDeleteStep(idx, s.id);
}}
onPointerDown={(e) => e.stopPropagation()}
sx={{
width: 24,
height: 24,
p: 0,
color: c.text.muted,
flexShrink: 0,
opacity: canDelete ? 0.72 : 0.35,
'&:hover': {
color: c.status.error,
bgcolor: c.status.errorBg,
opacity: 1,
},
}}
>
<DeleteOutlineRounded sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
) : null;
return (
<Box key={s.id} sx={{ display: 'flex', flexDirection: 'column' }}>
@@ -151,6 +191,7 @@ export default function StepList(props: Props) {
{activeStepDuration}
</Typography>
)}
{deleteButton}
{hasExpandableBody && !isActive && (
<KeyboardArrowDownRounded sx={{
fontSize: 18,
@@ -185,7 +226,6 @@ export default function StepList(props: Props) {
p: 1,
borderRadius: `${c.radius.md}px`,
bgcolor: c.bg.elevated,
border: `1px solid ${c.border.subtle}`,
}}>
<Typography sx={{ fontSize: '0.82rem', color: c.text.secondary, lineHeight: 1.5, whiteSpace: 'pre-wrap' }}>
{rawBody}
@@ -126,6 +126,21 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft,
dispatch(toggleExpandedStep({ workflowId, stepId }));
}, [dispatch, workflowId]);
const onDeleteStep = useCallback((idx: number, stepId: string) => {
if (steps.length <= 1) return;
const nextSteps = steps.filter((_, i) => i !== idx);
dispatch(updateWorkflowCard({
workflowId,
patch: {
draft: {
...liveDraft,
steps: nextSteps,
},
expandedStepIds: (card?.expandedStepIds || []).filter((id) => id !== stepId),
},
}));
}, [card?.expandedStepIds, dispatch, liveDraft, steps, workflowId]);
const onChangeDescription = useCallback((value: string) => {
dispatch(updateWorkflowCard({ workflowId, patch: { draft: { ...liveDraft, description: value } } }));
}, [dispatch, workflowId, liveDraft]);
@@ -171,7 +186,7 @@ export function PreviewView({ workflowId, steps, sourceSessionId, initialDraft,
void onChangeDescription;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.25, minHeight: '100%' }}>
<StepList steps={steps} expandable expandedIds={expandedIds} onToggleExpand={onToggleStep} />
<StepList steps={steps} expandable expandedIds={expandedIds} onToggleExpand={onToggleStep} onDeleteStep={onDeleteStep} />
<Box sx={{ flex: 1 }} />
{/* Schedule prompt card. Soft accent tint + calendar icon. Accent is the
same color the human-intervention (AskUserQuestion) popup uses. */}
@@ -267,6 +282,7 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
void runs; void activeRunId;
const card = useAppSelector((s) => s.workflows.openCards[workflow.id]);
const expandedIds = card?.expandedStepIds || [];
const [deletingStepId, setDeletingStepId] = useState<string | null>(null);
const openEditAgent = useCallback(() => {
dispatch(updateWorkflowCard({ workflowId: workflow.id, patch: { view: 'edit_agent' } }));
}, [dispatch, workflow.id]);
@@ -276,6 +292,19 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
const onToggleStep = useCallback((stepId: string) => {
dispatch(toggleExpandedStep({ workflowId: workflow.id, stepId }));
}, [dispatch, workflow.id]);
const onDeleteStep = useCallback(async (idx: number, stepId: string) => {
if (workflow.steps.length <= 1 || deletingStepId) return;
setDeletingStepId(stepId);
try {
await dispatch(updateWorkflow({
id: workflow.id,
patch: { steps: workflow.steps.filter((_, i) => i !== idx) },
ifMatch: workflow.updated_at || null,
}));
} finally {
setDeletingStepId(null);
}
}, [deletingStepId, dispatch, workflow.id, workflow.steps, workflow.updated_at]);
const scheduleLine = workflow.schedule.enabled ? describeSchedule(workflow) : 'Schedule this workflow';
const scheduleClickable = !workflow.schedule.enabled;
@@ -288,6 +317,7 @@ export function SavedView({ workflow, steps, runs, activeRunId }: { workflow: Wo
expandable
expandedIds={expandedIds}
onToggleExpand={onToggleStep}
onDeleteStep={onDeleteStep}
/>
<Box sx={{ flex: 1 }} />
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>