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>
|
||||
<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