mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[aidan] ux/workflows: add workflow step removal (#91)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
Reference in New Issue
Block a user