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 { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { ReleasePlanChangeRequestDialog } from './ChangeRequest/ReleasePlanChangeRequestDialog.tsx'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { Truncator } from 'component/common/Truncator/Truncator'; import { useUiFlag } from 'hooks/useUiFlag'; import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi'; import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx'; import type { ChangeMilestoneProgressionSchema } from 'openapi'; import { ReleasePlanMilestoneItem } from './ReleasePlanMilestoneItem/ReleasePlanMilestoneItem.tsx'; 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', })); 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 [changeRequestAction, setChangeRequestAction] = useState< | { type: 'removeReleasePlan'; environmentActive: boolean } | { type: 'startMilestone'; milestone: IReleasePlanMilestone } | { type: 'changeMilestoneProgression'; payload: ChangeMilestoneProgressionSchema; } | { type: 'deleteMilestoneProgression'; sourceMilestoneId: string; } | null >(null); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { addChange } = useChangeRequestApi(); const { data: pendingChangeRequests, refetch: refetchChangeRequests } = usePendingChangeRequests(projectId); // Find progression changes for this feature in pending change requests const getPendingProgressionChange = (sourceMilestoneId: string) => { if (!pendingChangeRequests) return null; for (const changeRequest of pendingChangeRequests) { if (changeRequest.environment !== environment) continue; const featureInChangeRequest = changeRequest.features.find( (featureItem) => featureItem.name === featureName, ); if (!featureInChangeRequest) continue; // Look for update or delete progression changes const progressionChange = featureInChangeRequest.changes.find( (change: any) => (change.action === 'updateMilestoneProgression' && (change.payload.sourceMilestoneId === sourceMilestoneId || change.payload.sourceMilestone === sourceMilestoneId)) || (change.action === 'deleteMilestoneProgression' && (change.payload.sourceMilestoneId === sourceMilestoneId || change.payload.sourceMilestone === sourceMilestoneId)), ); if (progressionChange) { return { action: progressionChange.action, payload: progressionChange.payload, changeRequestId: changeRequest.id, }; } } return null; }; const milestoneProgressionsEnabled = useUiFlag('milestoneProgression'); const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState< number | null >(null); const [milestoneToDeleteProgression, setMilestoneToDeleteProgression] = useState(null); const [isDeletingProgression, setIsDeletingProgression] = useState(false); const onChangeRequestConfirm = async () => { if (!changeRequestAction) return; switch (changeRequestAction.type) { case 'removeReleasePlan': await addChange(projectId, environment, { feature: featureName, action: 'deleteReleasePlan', payload: { planId: plan.id, }, }); break; case 'startMilestone': await addChange(projectId, environment, { feature: featureName, action: 'startMilestone', payload: { planId: plan.id, milestoneId: changeRequestAction.milestone.id, }, }); break; case 'changeMilestoneProgression': await addChange(projectId, environment, { feature: featureName, action: 'changeMilestoneProgression', payload: changeRequestAction.payload, }); break; case 'deleteMilestoneProgression': await addChange(projectId, environment, { feature: featureName, action: 'deleteMilestoneProgression', payload: { sourceMilestone: changeRequestAction.sourceMilestoneId, }, }); break; } await refetchChangeRequests(); setToastData({ type: 'success', text: 'Added to draft', }); setChangeRequestAction(null); setProgressionFormOpenIndex(null); }; const confirmRemoveReleasePlan = () => { if (isChangeRequestConfigured(environment)) { setChangeRequestAction({ type: 'removeReleasePlan', environmentActive: !environmentIsDisabled, }); } 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)) { setChangeRequestAction({ type: 'startMilestone', milestone, }); } 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 handleAddToChangeRequest = (action: { type: 'changeMilestoneProgression'; payload: ChangeMilestoneProgressionSchema; }) => { setChangeRequestAction(action); }; const handleDeleteProgression = (milestone: IReleasePlanMilestone) => { if (isChangeRequestConfigured(environment)) { setChangeRequestAction({ type: 'deleteMilestoneProgression', sourceMilestoneId: milestone.id, }); } else { 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) => ( ))} setChangeRequestAction(null)} /> {milestoneToDeleteProgression && ( )} ); };