From ce2ef4fe6f4c6604aa6d301552723ef50892c6a5 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 10 Oct 2025 09:10:10 +0200 Subject: [PATCH] feat: add delete functionality for milestone progressions (#10770) --- .../ReleasePlan/DeleteProgressionDialog.tsx | 34 +++++++++++ .../ReleasePlan/ReleasePlan.tsx | 58 ++++++++++++++++++- .../MilestoneAutomationSection.tsx | 6 ++ .../MilestoneTransitionDisplay.tsx | 35 +++++++++-- .../ReleasePlanMilestone.tsx | 6 ++ .../useMilestoneProgressionsApi.ts | 19 ++++++ 6 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/DeleteProgressionDialog.tsx diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/DeleteProgressionDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/DeleteProgressionDialog.tsx new file mode 100644 index 0000000000..d338e38def --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/DeleteProgressionDialog.tsx @@ -0,0 +1,34 @@ +import { Dialogue } from 'component/common/Dialogue/Dialogue'; + +interface IDeleteProgressionDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; + milestoneName: string; + isDeleting?: boolean; +} + +export const DeleteProgressionDialog = ({ + open, + onClose, + onConfirm, + milestoneName, + isDeleting = false, +}: IDeleteProgressionDialogProps) => ( + +

+ You are about to remove the automation that progresses from{' '} + {milestoneName} to the next milestone. +

+
+

This action cannot be undone.

+
+); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx index 71337fe88d..f25ee0b639 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx @@ -24,6 +24,8 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { Truncator } from 'component/common/Truncator/Truncator'; import { useUiFlag } from 'hooks/useUiFlag'; import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx'; +import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi'; +import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx'; const StyledContainer = styled('div')(({ theme }) => ({ padding: theme.spacing(2), @@ -106,6 +108,7 @@ export const ReleasePlan = ({ ); const { removeReleasePlanFromFeature, startReleasePlanMilestone } = useReleasePlansApi(); + const { deleteMilestoneProgression } = useMilestoneProgressionsApi(); const { setToastData, setToastApiError } = useToast(); const { trackEvent } = usePlausibleTracker(); @@ -128,6 +131,9 @@ export const ReleasePlan = ({ const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState< number | null >(null); + const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] = + useState(null); + const [isDeletingProgression, setIsDeletingProgression] = useState(false); const onAddRemovePlanChangesConfirm = async () => { await addChange(projectId, environment, { @@ -244,6 +250,40 @@ export const ReleasePlan = ({ setProgressionFormOpenIndex(null); }; + const handleDeleteProgression = (milestone: IReleasePlanMilestone) => { + setMilestoneToDeleteProgression(milestone); + }; + + const handleCloseDeleteDialog = () => { + if (!isDeletingProgression) { + setMilestoneToDeleteProgression(null); + } + }; + + const onDeleteProgressionConfirm = async () => { + if (!milestoneToDeleteProgression || isDeletingProgression) return; + + setIsDeletingProgression(true); + try { + await deleteMilestoneProgression( + projectId, + environment, + milestoneToDeleteProgression.id, + ); + await refetch(); + setMilestoneToDeleteProgression(null); + setToastData({ + type: 'success', + text: 'Automation removed successfully', + }); + } catch (error: unknown) { + setMilestoneToDeleteProgression(null); + setToastApiError(formatUnknownError(error)); + } finally { + setIsDeletingProgression(false); + } + }; + const activeIndex = milestones.findIndex( (milestone) => milestone.id === activeMilestoneId, ); @@ -302,9 +342,16 @@ export const ReleasePlan = ({ onStartMilestone={onStartMilestone} showAutomation={ milestoneProgressionsEnabled && - isNotLastMilestone + isNotLastMilestone && + !readonly } onAddAutomation={handleOpenProgressionForm} + onDeleteAutomation={ + milestone.transitionCondition + ? () => + handleDeleteProgression(milestone) + : undefined + } automationForm={ isProgressionFormOpen ? ( + {milestoneToDeleteProgression && ( + + )} ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx index db241ee876..30b3dd8b38 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx @@ -51,18 +51,22 @@ interface IMilestoneAutomationSectionProps { showAutomation?: boolean; status?: MilestoneStatus; onAddAutomation?: () => void; + onDeleteAutomation?: () => void; automationForm?: React.ReactNode; transitionCondition?: { intervalMinutes: number; } | null; + milestoneName: string; } export const MilestoneAutomationSection = ({ showAutomation, status, onAddAutomation, + onDeleteAutomation, automationForm, transitionCondition, + milestoneName, }: IMilestoneAutomationSectionProps) => { if (!showAutomation) return null; @@ -73,6 +77,8 @@ export const MilestoneAutomationSection = ({ ) : transitionCondition ? ( ) : ( ({ display: 'flex', alignItems: 'center', gap: theme.spacing(1), + justifyContent: 'space-between', + width: '100%', +})); + +const StyledContentGroup = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), })); const StyledIcon = styled(BoltIcon)(({ theme }) => ({ @@ -24,6 +33,8 @@ const StyledText = styled('span')(({ theme }) => ({ interface IMilestoneTransitionDisplayProps { intervalMinutes: number; + onDelete: () => void; + milestoneName: string; } const formatInterval = (minutes: number): string => { @@ -42,14 +53,26 @@ const formatInterval = (minutes: number): string => { export const MilestoneTransitionDisplay = ({ intervalMinutes, + onDelete, + milestoneName, }: IMilestoneTransitionDisplayProps) => { return ( - - - Proceed to the next milestone after{' '} - {formatInterval(intervalMinutes)} - + + + + Proceed to the next milestone after{' '} + {formatInterval(intervalMinutes)} + + + + + ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx index 69b4e086e6..0802d4ba73 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx @@ -78,6 +78,7 @@ interface IReleasePlanMilestoneProps { readonly?: boolean; showAutomation?: boolean; onAddAutomation?: () => void; + onDeleteAutomation?: () => void; automationForm?: React.ReactNode; } @@ -88,6 +89,7 @@ export const ReleasePlanMilestone = ({ readonly, showAutomation, onAddAutomation, + onDeleteAutomation, automationForm, }: IReleasePlanMilestoneProps) => { const [expanded, setExpanded] = useState(false); @@ -117,8 +119,10 @@ export const ReleasePlanMilestone = ({ showAutomation={showAutomation} status={status} onAddAutomation={onAddAutomation} + onDeleteAutomation={onDeleteAutomation} automationForm={automationForm} transitionCondition={milestone.transitionCondition} + milestoneName={milestone.name} /> ); @@ -174,8 +178,10 @@ export const ReleasePlanMilestone = ({ showAutomation={showAutomation} status={status} onAddAutomation={onAddAutomation} + onDeleteAutomation={onDeleteAutomation} automationForm={automationForm} transitionCondition={milestone.transitionCondition} + milestoneName={milestone.name} /> ); diff --git a/frontend/src/hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi.ts b/frontend/src/hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi.ts index fcfe361715..b7abdcc059 100644 --- a/frontend/src/hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi.ts +++ b/frontend/src/hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi.ts @@ -25,8 +25,27 @@ export const useMilestoneProgressionsApi = () => { await makeRequest(req.caller, req.id); }; + const deleteMilestoneProgression = async ( + projectId: string, + environment: string, + sourceMilestoneId: string, + ): Promise => { + const requestId = 'deleteMilestoneProgression'; + const path = `api/admin/projects/${projectId}/environments/${environment}/progressions/${sourceMilestoneId}`; + const req = createRequest( + path, + { + method: 'DELETE', + }, + requestId, + ); + + await makeRequest(req.caller, req.id); + }; + return { createMilestoneProgression, + deleteMilestoneProgression, errors, loading, };