1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: add delete functionality for milestone progressions (#10770)

This commit is contained in:
Fredrik Strand Oseberg 2025-10-10 09:10:10 +02:00 committed by GitHub
parent fce4c5bbab
commit ce2ef4fe6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 151 additions and 7 deletions

View File

@ -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) => (
<Dialogue
title='Remove automation?'
open={open}
primaryButtonText={isDeleting ? 'Removing...' : 'Remove automation'}
secondaryButtonText='Cancel'
onClick={onConfirm}
onClose={onClose}
disabledPrimaryButton={isDeleting}
>
<p>
You are about to remove the automation that progresses from{' '}
<strong>{milestoneName}</strong> to the next milestone.
</p>
<br />
<p>This action cannot be undone.</p>
</Dialogue>
);

View File

@ -24,6 +24,8 @@ import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { Truncator } from 'component/common/Truncator/Truncator'; import { Truncator } from 'component/common/Truncator/Truncator';
import { useUiFlag } from 'hooks/useUiFlag'; import { useUiFlag } from 'hooks/useUiFlag';
import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx'; import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx';
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(2), padding: theme.spacing(2),
@ -106,6 +108,7 @@ export const ReleasePlan = ({
); );
const { removeReleasePlanFromFeature, startReleasePlanMilestone } = const { removeReleasePlanFromFeature, startReleasePlanMilestone } =
useReleasePlansApi(); useReleasePlansApi();
const { deleteMilestoneProgression } = useMilestoneProgressionsApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
@ -128,6 +131,9 @@ export const ReleasePlan = ({
const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState< const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState<
number | null number | null
>(null); >(null);
const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] =
useState<IReleasePlanMilestone | null>(null);
const [isDeletingProgression, setIsDeletingProgression] = useState(false);
const onAddRemovePlanChangesConfirm = async () => { const onAddRemovePlanChangesConfirm = async () => {
await addChange(projectId, environment, { await addChange(projectId, environment, {
@ -244,6 +250,40 @@ export const ReleasePlan = ({
setProgressionFormOpenIndex(null); 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( const activeIndex = milestones.findIndex(
(milestone) => milestone.id === activeMilestoneId, (milestone) => milestone.id === activeMilestoneId,
); );
@ -302,9 +342,16 @@ export const ReleasePlan = ({
onStartMilestone={onStartMilestone} onStartMilestone={onStartMilestone}
showAutomation={ showAutomation={
milestoneProgressionsEnabled && milestoneProgressionsEnabled &&
isNotLastMilestone isNotLastMilestone &&
!readonly
} }
onAddAutomation={handleOpenProgressionForm} onAddAutomation={handleOpenProgressionForm}
onDeleteAutomation={
milestone.transitionCondition
? () =>
handleDeleteProgression(milestone)
: undefined
}
automationForm={ automationForm={
isProgressionFormOpen ? ( isProgressionFormOpen ? (
<MilestoneProgressionForm <MilestoneProgressionForm
@ -354,6 +401,15 @@ export const ReleasePlan = ({
releasePlan={plan} releasePlan={plan}
milestone={milestoneForChangeRequestDialog} milestone={milestoneForChangeRequestDialog}
/> />
{milestoneToDeleteProgression && (
<DeleteProgressionDialog
open={milestoneToDeleteProgression !== null}
onClose={handleCloseDeleteDialog}
onConfirm={onDeleteProgressionConfirm}
milestoneName={milestoneToDeleteProgression.name}
isDeleting={isDeletingProgression}
/>
)}
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -51,18 +51,22 @@ interface IMilestoneAutomationSectionProps {
showAutomation?: boolean; showAutomation?: boolean;
status?: MilestoneStatus; status?: MilestoneStatus;
onAddAutomation?: () => void; onAddAutomation?: () => void;
onDeleteAutomation?: () => void;
automationForm?: React.ReactNode; automationForm?: React.ReactNode;
transitionCondition?: { transitionCondition?: {
intervalMinutes: number; intervalMinutes: number;
} | null; } | null;
milestoneName: string;
} }
export const MilestoneAutomationSection = ({ export const MilestoneAutomationSection = ({
showAutomation, showAutomation,
status, status,
onAddAutomation, onAddAutomation,
onDeleteAutomation,
automationForm, automationForm,
transitionCondition, transitionCondition,
milestoneName,
}: IMilestoneAutomationSectionProps) => { }: IMilestoneAutomationSectionProps) => {
if (!showAutomation) return null; if (!showAutomation) return null;
@ -73,6 +77,8 @@ export const MilestoneAutomationSection = ({
) : transitionCondition ? ( ) : transitionCondition ? (
<MilestoneTransitionDisplay <MilestoneTransitionDisplay
intervalMinutes={transitionCondition.intervalMinutes} intervalMinutes={transitionCondition.intervalMinutes}
onDelete={onDeleteAutomation!}
milestoneName={milestoneName}
/> />
) : ( ) : (
<StyledAddAutomationButton <StyledAddAutomationButton

View File

@ -1,11 +1,20 @@
import BoltIcon from '@mui/icons-material/Bolt'; import BoltIcon from '@mui/icons-material/Bolt';
import { styled } from '@mui/material'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import { IconButton, styled } from '@mui/material';
import { formatDuration, intervalToDuration } from 'date-fns'; import { formatDuration, intervalToDuration } from 'date-fns';
const StyledDisplayContainer = styled('div')(({ theme }) => ({ const StyledDisplayContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: theme.spacing(1), 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 }) => ({ const StyledIcon = styled(BoltIcon)(({ theme }) => ({
@ -24,6 +33,8 @@ const StyledText = styled('span')(({ theme }) => ({
interface IMilestoneTransitionDisplayProps { interface IMilestoneTransitionDisplayProps {
intervalMinutes: number; intervalMinutes: number;
onDelete: () => void;
milestoneName: string;
} }
const formatInterval = (minutes: number): string => { const formatInterval = (minutes: number): string => {
@ -42,14 +53,26 @@ const formatInterval = (minutes: number): string => {
export const MilestoneTransitionDisplay = ({ export const MilestoneTransitionDisplay = ({
intervalMinutes, intervalMinutes,
onDelete,
milestoneName,
}: IMilestoneTransitionDisplayProps) => { }: IMilestoneTransitionDisplayProps) => {
return ( return (
<StyledDisplayContainer> <StyledDisplayContainer>
<StyledIcon /> <StyledContentGroup>
<StyledText> <StyledIcon />
Proceed to the next milestone after{' '} <StyledText>
{formatInterval(intervalMinutes)} Proceed to the next milestone after{' '}
</StyledText> {formatInterval(intervalMinutes)}
</StyledText>
</StyledContentGroup>
<IconButton
onClick={onDelete}
size='small'
aria-label={`Delete automation for ${milestoneName}`}
sx={{ padding: 0.5 }}
>
<DeleteOutlineIcon fontSize='small' />
</IconButton>
</StyledDisplayContainer> </StyledDisplayContainer>
); );
}; };

View File

@ -78,6 +78,7 @@ interface IReleasePlanMilestoneProps {
readonly?: boolean; readonly?: boolean;
showAutomation?: boolean; showAutomation?: boolean;
onAddAutomation?: () => void; onAddAutomation?: () => void;
onDeleteAutomation?: () => void;
automationForm?: React.ReactNode; automationForm?: React.ReactNode;
} }
@ -88,6 +89,7 @@ export const ReleasePlanMilestone = ({
readonly, readonly,
showAutomation, showAutomation,
onAddAutomation, onAddAutomation,
onDeleteAutomation,
automationForm, automationForm,
}: IReleasePlanMilestoneProps) => { }: IReleasePlanMilestoneProps) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
@ -117,8 +119,10 @@ export const ReleasePlanMilestone = ({
showAutomation={showAutomation} showAutomation={showAutomation}
status={status} status={status}
onAddAutomation={onAddAutomation} onAddAutomation={onAddAutomation}
onDeleteAutomation={onDeleteAutomation}
automationForm={automationForm} automationForm={automationForm}
transitionCondition={milestone.transitionCondition} transitionCondition={milestone.transitionCondition}
milestoneName={milestone.name}
/> />
</StyledMilestoneContainer> </StyledMilestoneContainer>
); );
@ -174,8 +178,10 @@ export const ReleasePlanMilestone = ({
showAutomation={showAutomation} showAutomation={showAutomation}
status={status} status={status}
onAddAutomation={onAddAutomation} onAddAutomation={onAddAutomation}
onDeleteAutomation={onDeleteAutomation}
automationForm={automationForm} automationForm={automationForm}
transitionCondition={milestone.transitionCondition} transitionCondition={milestone.transitionCondition}
milestoneName={milestone.name}
/> />
</StyledMilestoneContainer> </StyledMilestoneContainer>
); );

View File

@ -25,8 +25,27 @@ export const useMilestoneProgressionsApi = () => {
await makeRequest(req.caller, req.id); await makeRequest(req.caller, req.id);
}; };
const deleteMilestoneProgression = async (
projectId: string,
environment: string,
sourceMilestoneId: string,
): Promise<void> => {
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 { return {
createMilestoneProgression, createMilestoneProgression,
deleteMilestoneProgression,
errors, errors,
loading, loading,
}; };