import Delete from '@mui/icons-material/Delete'; import { styled } from '@mui/material'; import { DELETE_FEATURE_STRATEGY } from '@server/types/permissions'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { useReleasePlansApi } from 'hooks/api/actions/useReleasePlansApi/useReleasePlansApi'; import { useFeatureReleasePlans } from 'hooks/api/getters/useFeatureReleasePlans/useFeatureReleasePlans'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import useToast from 'hooks/useToast'; import type { IReleasePlan, IReleasePlanMilestone, } from 'interfaces/releasePlans'; import { useState } from 'react'; import { formatUnknownError } from 'utils/formatUnknownError'; import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog.tsx'; import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone.tsx'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog.tsx'; import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog.tsx'; import { CreateMilestoneProgressionChangeRequestDialog } from './ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx'; 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'; import type { CreateMilestoneProgressionSchema } from 'openapi'; const StyledContainer = styled('div')(({ theme }) => ({ padding: theme.spacing(2), paddingTop: theme.spacing(0), background: 'inherit', display: 'flex', flexFlow: 'column', gap: theme.spacing(1), })); const StyledHeader = styled('div')(({ theme }) => ({ display: 'flex', justifyContent: 'space-between', color: theme.palette.text.primary, })); const StyledHeaderGroup = styled('hgroup')(({ theme }) => ({ paddingTop: theme.spacing(1.5), })); const StyledHeaderTitleLabel = styled('p')(({ theme }) => ({ fontWeight: 'bold', fontSize: theme.typography.body1.fontSize, lineHeight: 0.5, marginBottom: theme.spacing(0.5), display: 'inline', })); const StyledHeaderTitle = styled('h3')(({ theme }) => ({ display: 'inline', margin: 0, fontWeight: 'normal', fontSize: theme.typography.body1.fontSize, })); const StyledHeaderDescription = styled('p')(({ theme }) => ({ marginTop: theme.spacing(1), fontSize: theme.typography.body2.fontSize, color: theme.palette.text.secondary, })); const StyledBody = styled('div')(({ theme }) => ({ display: 'flex', flexDirection: 'column', })); const StyledConnection = styled('div', { shouldForwardProp: (prop) => prop !== 'isCompleted', })<{ isCompleted: boolean }>(({ theme, isCompleted }) => ({ width: 2, height: theme.spacing(2), backgroundColor: isCompleted ? theme.palette.divider : theme.palette.primary.main, marginLeft: theme.spacing(3.25), })); interface IReleasePlanProps { plan: IReleasePlan; environmentIsDisabled?: boolean; readonly?: boolean; } export const ReleasePlan = ({ plan, environmentIsDisabled, readonly, }: IReleasePlanProps) => { const { id, name, description, activeMilestoneId, featureName, environment, milestones, } = plan; const projectId = useRequiredPathParam('projectId'); const { refetch } = useFeatureReleasePlans( projectId, featureName, environment, ); const { removeReleasePlanFromFeature, startReleasePlanMilestone } = useReleasePlansApi(); const { deleteMilestoneProgression } = useMilestoneProgressionsApi(); const { setToastData, setToastApiError } = useToast(); const { trackEvent } = usePlausibleTracker(); const [removeOpen, setRemoveOpen] = useState(false); const [changeRequestDialogRemoveOpen, setChangeRequestDialogRemoveOpen] = useState(false); const [ changeRequestDialogStartMilestoneOpen, setChangeRequestDialogStartMilestoneOpen, ] = useState(false); const [ changeRequestDialogCreateProgressionOpen, setChangeRequestDialogCreateProgressionOpen, ] = useState(false); const [ milestoneForChangeRequestDialog, setMilestoneForChangeRequestDialog, ] = useState(); const [progressionDataForCR, setProgressionDataForCR] = useState(null); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { addChange } = useChangeRequestApi(); const { refetch: refetchChangeRequests } = usePendingChangeRequests(projectId); const milestoneProgressionsEnabled = useUiFlag('milestoneProgression'); const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState< number | null >(null); const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] = useState(null); const [isDeletingProgression, setIsDeletingProgression] = useState(false); const onAddRemovePlanChangesConfirm = async () => { await addChange(projectId, environment, { feature: featureName, action: 'deleteReleasePlan', payload: { planId: plan.id, }, }); await refetchChangeRequests(); setToastData({ type: 'success', text: 'Added to draft', }); setChangeRequestDialogRemoveOpen(false); }; const onAddStartMilestoneChangesConfirm = async () => { await addChange(projectId, environment, { feature: featureName, action: 'startMilestone', payload: { planId: plan.id, milestoneId: milestoneForChangeRequestDialog?.id, }, }); await refetchChangeRequests(); setToastData({ type: 'success', text: 'Added to draft', }); setChangeRequestDialogStartMilestoneOpen(false); }; const onAddCreateProgressionChangesConfirm = async () => { if (!progressionDataForCR) return; await addChange(projectId, environment, { feature: featureName, action: 'createMilestoneProgression', payload: progressionDataForCR, }); await refetchChangeRequests(); setToastData({ type: 'success', text: 'Added to draft', }); setChangeRequestDialogCreateProgressionOpen(false); setProgressionFormOpenIndex(null); setProgressionDataForCR(null); }; const confirmRemoveReleasePlan = () => { if (isChangeRequestConfigured(environment)) { setChangeRequestDialogRemoveOpen(true); } else { setRemoveOpen(true); } trackEvent('release-management', { props: { eventType: 'remove-plan', plan: name, }, }); }; const onRemoveConfirm = async () => { try { await removeReleasePlanFromFeature( projectId, featureName, environment, id, ); setToastData({ text: `Release plan "${name}" has been removed from ${featureName} in ${environment}`, type: 'success', }); refetch(); setRemoveOpen(false); } catch (error: unknown) { setToastApiError(formatUnknownError(error)); } }; const onStartMilestone = async (milestone: IReleasePlanMilestone) => { if (isChangeRequestConfigured(environment)) { setMilestoneForChangeRequestDialog(milestone); setChangeRequestDialogStartMilestoneOpen(true); } else { try { await startReleasePlanMilestone( projectId, featureName, environment, id, milestone.id, ); setToastData({ text: `Milestone "${milestone.name}" has started`, type: 'success', }); refetch(); } catch (error: unknown) { setToastApiError(formatUnknownError(error)); } } trackEvent('release-management', { props: { eventType: 'start-milestone', plan: name, milestone: milestone.name, }, }); }; const handleProgressionSave = async () => { setProgressionFormOpenIndex(null); await refetch(); }; const handleProgressionCancel = () => { setProgressionFormOpenIndex(null); }; const handleProgressionChangeRequestSubmit = ( payload: CreateMilestoneProgressionSchema, ) => { setProgressionDataForCR(payload); setChangeRequestDialogCreateProgressionOpen(true); }; 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, featureName, 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, ); return ( Release plan:{' '} {name} {description} {!readonly && ( )} {milestones.map((milestone, index) => { const isNotLastMilestone = index < milestones.length - 1; const isProgressionFormOpen = progressionFormOpenIndex === index; const nextMilestoneId = milestones[index + 1]?.id || ''; const handleOpenProgressionForm = () => setProgressionFormOpenIndex(index); return (
handleDeleteProgression(milestone) : undefined } automationForm={ isProgressionFormOpen ? ( handleProgressionChangeRequestSubmit( payload, ) } /> ) : undefined } projectId={projectId} environment={environment} featureName={featureName} onUpdate={refetch} allMilestones={milestones} activeMilestoneId={activeMilestoneId} /> } />
); })}
setChangeRequestDialogRemoveOpen(false)} releasePlan={plan} environmentActive={!environmentIsDisabled} /> { setMilestoneForChangeRequestDialog(undefined); setChangeRequestDialogStartMilestoneOpen(false); }} releasePlan={plan} milestone={milestoneForChangeRequestDialog} /> {progressionDataForCR && ( { setChangeRequestDialogCreateProgressionOpen(false); setProgressionDataForCR(null); }} releasePlan={plan} payload={progressionDataForCR} /> )} {milestoneToDeleteProgression && ( )}
); };