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:
		
							parent
							
								
									fce4c5bbab
								
							
						
					
					
						commit
						ce2ef4fe6f
					
				@ -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>
 | 
			
		||||
);
 | 
			
		||||
@ -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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
            <StyledIcon />
 | 
			
		||||
            <StyledText>
 | 
			
		||||
                Proceed to the next milestone after{' '}
 | 
			
		||||
                {formatInterval(intervalMinutes)}
 | 
			
		||||
            </StyledText>
 | 
			
		||||
            <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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user