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 { 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<IReleasePlanMilestone | null>(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 ? (
<MilestoneProgressionForm
@ -354,6 +401,15 @@ export const ReleasePlan = ({
releasePlan={plan}
milestone={milestoneForChangeRequestDialog}
/>
{milestoneToDeleteProgression && (
<DeleteProgressionDialog
open={milestoneToDeleteProgression !== null}
onClose={handleCloseDeleteDialog}
onConfirm={onDeleteProgressionConfirm}
milestoneName={milestoneToDeleteProgression.name}
isDeleting={isDeletingProgression}
/>
)}
</StyledContainer>
);
};

View File

@ -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 ? (
<MilestoneTransitionDisplay
intervalMinutes={transitionCondition.intervalMinutes}
onDelete={onDeleteAutomation!}
milestoneName={milestoneName}
/>
) : (
<StyledAddAutomationButton

View File

@ -1,11 +1,20 @@
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';
const StyledDisplayContainer = styled('div')(({ theme }) => ({
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 (
<StyledDisplayContainer>
<StyledContentGroup>
<StyledIcon />
<StyledText>
Proceed to the next milestone after{' '}
{formatInterval(intervalMinutes)}
</StyledText>
</StyledContentGroup>
<IconButton
onClick={onDelete}
size='small'
aria-label={`Delete automation for ${milestoneName}`}
sx={{ padding: 0.5 }}
>
<DeleteOutlineIcon fontSize='small' />
</IconButton>
</StyledDisplayContainer>
);
};

View File

@ -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}
/>
</StyledMilestoneContainer>
);
@ -174,8 +178,10 @@ export const ReleasePlanMilestone = ({
showAutomation={showAutomation}
status={status}
onAddAutomation={onAddAutomation}
onDeleteAutomation={onDeleteAutomation}
automationForm={automationForm}
transitionCondition={milestone.transitionCondition}
milestoneName={milestone.name}
/>
</StyledMilestoneContainer>
);

View File

@ -25,8 +25,27 @@ export const useMilestoneProgressionsApi = () => {
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 {
createMilestoneProgression,
deleteMilestoneProgression,
errors,
loading,
};