mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: change request progression view (#10835)
This commit is contained in:
		
							parent
							
								
									b9d81e5f59
								
							
						
					
					
						commit
						866441a1b6
					
				@ -77,6 +77,7 @@ export const ChangeRequest: VFC<IChangeRequestProps> = ({
 | 
				
			|||||||
                            change={change}
 | 
					                            change={change}
 | 
				
			||||||
                            feature={feature}
 | 
					                            feature={feature}
 | 
				
			||||||
                            onNavigate={onNavigate}
 | 
					                            onNavigate={onNavigate}
 | 
				
			||||||
 | 
					                            onRefetch={onRefetch}
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
                    ))}
 | 
					                    ))}
 | 
				
			||||||
                    {feature.defaultChange ? (
 | 
					                    {feature.defaultChange ? (
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,187 @@
 | 
				
			|||||||
 | 
					import type { FC } from 'react';
 | 
				
			||||||
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    ChangeRequestState,
 | 
				
			||||||
 | 
					    IChangeRequestCreateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestUpdateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestDeleteMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestFeature,
 | 
				
			||||||
 | 
					} from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
 | 
					import type { IReleasePlan } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Added,
 | 
				
			||||||
 | 
					    ChangeItemInfo,
 | 
				
			||||||
 | 
					    ChangeItemWrapper,
 | 
				
			||||||
 | 
					    Deleted,
 | 
				
			||||||
 | 
					} from './Change.styles.tsx';
 | 
				
			||||||
 | 
					import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
				
			||||||
 | 
					import { MilestoneListRenderer } from './MilestoneListRenderer.tsx';
 | 
				
			||||||
 | 
					import { applyProgressionChanges } from './applyProgressionChanges.js';
 | 
				
			||||||
 | 
					import { EventDiff } from 'component/events/EventDiff/EventDiff';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledTabs = styled(Tabs)(({ theme }) => ({
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    flexFlow: 'column',
 | 
				
			||||||
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProgressionChange =
 | 
				
			||||||
 | 
					    | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
 | 
					    | IChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					    | IChangeRequestDeleteMilestoneProgression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getFirstChangeWithSnapshot = (
 | 
				
			||||||
 | 
					    progressionChanges: ProgressionChange[],
 | 
				
			||||||
 | 
					) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        progressionChanges.find(
 | 
				
			||||||
 | 
					            (change) =>
 | 
				
			||||||
 | 
					                change.payload?.snapshot &&
 | 
				
			||||||
 | 
					                (change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					                    change.action === 'updateMilestoneProgression'),
 | 
				
			||||||
 | 
					        ) || progressionChanges.find((change) => change.payload?.snapshot)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getMilestonesWithAutomation = (
 | 
				
			||||||
 | 
					    progressionChanges: ProgressionChange[],
 | 
				
			||||||
 | 
					): Set<string> => {
 | 
				
			||||||
 | 
					    return new Set(
 | 
				
			||||||
 | 
					        progressionChanges
 | 
				
			||||||
 | 
					            .filter(
 | 
				
			||||||
 | 
					                (change) =>
 | 
				
			||||||
 | 
					                    change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					                    change.action === 'updateMilestoneProgression',
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .map((change) => change.payload.sourceMilestone)
 | 
				
			||||||
 | 
					            .filter((id): id is string => Boolean(id)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getMilestonesWithDeletedAutomation = (
 | 
				
			||||||
 | 
					    progressionChanges: ProgressionChange[],
 | 
				
			||||||
 | 
					): Set<string> => {
 | 
				
			||||||
 | 
					    return new Set(
 | 
				
			||||||
 | 
					        progressionChanges
 | 
				
			||||||
 | 
					            .filter((change) => change.action === 'deleteMilestoneProgression')
 | 
				
			||||||
 | 
					            .map((change) => change.payload.sourceMilestone)
 | 
				
			||||||
 | 
					            .filter((id): id is string => Boolean(id)),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getChangeDescriptions = (
 | 
				
			||||||
 | 
					    progressionChanges: ProgressionChange[],
 | 
				
			||||||
 | 
					    basePlan: IReleasePlan,
 | 
				
			||||||
 | 
					): string[] => {
 | 
				
			||||||
 | 
					    return progressionChanges.map((change) => {
 | 
				
			||||||
 | 
					        const sourceId = change.payload.sourceMilestone;
 | 
				
			||||||
 | 
					        const sourceName =
 | 
				
			||||||
 | 
					            basePlan.milestones.find((milestone) => milestone.id === sourceId)
 | 
				
			||||||
 | 
					                ?.name || sourceId;
 | 
				
			||||||
 | 
					        const action =
 | 
				
			||||||
 | 
					            change.action === 'createMilestoneProgression'
 | 
				
			||||||
 | 
					                ? 'Adding'
 | 
				
			||||||
 | 
					                : change.action === 'deleteMilestoneProgression'
 | 
				
			||||||
 | 
					                  ? 'Deleting'
 | 
				
			||||||
 | 
					                  : 'Updating';
 | 
				
			||||||
 | 
					        return `${action} automation for ${sourceName}`;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ConsolidatedProgressionChanges: FC<{
 | 
				
			||||||
 | 
					    feature: IChangeRequestFeature;
 | 
				
			||||||
 | 
					    currentReleasePlan?: IReleasePlan;
 | 
				
			||||||
 | 
					    changeRequestState: ChangeRequestState;
 | 
				
			||||||
 | 
					    onUpdateChangeRequestSubmit?: (
 | 
				
			||||||
 | 
					        sourceMilestoneId: string,
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => Promise<void>;
 | 
				
			||||||
 | 
					    onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => Promise<void>;
 | 
				
			||||||
 | 
					}> = ({
 | 
				
			||||||
 | 
					    feature,
 | 
				
			||||||
 | 
					    currentReleasePlan,
 | 
				
			||||||
 | 
					    changeRequestState,
 | 
				
			||||||
 | 
					    onUpdateChangeRequestSubmit,
 | 
				
			||||||
 | 
					    onDeleteChangeRequestSubmit,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    // Get all progression changes for this feature
 | 
				
			||||||
 | 
					    const progressionChanges = feature.changes.filter(
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            change,
 | 
				
			||||||
 | 
					        ): change is
 | 
				
			||||||
 | 
					            | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
 | 
					            | IChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					            | IChangeRequestDeleteMilestoneProgression =>
 | 
				
			||||||
 | 
					            change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					            change.action === 'updateMilestoneProgression' ||
 | 
				
			||||||
 | 
					            change.action === 'deleteMilestoneProgression',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (progressionChanges.length === 0) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const firstChangeWithSnapshot =
 | 
				
			||||||
 | 
					        getFirstChangeWithSnapshot(progressionChanges);
 | 
				
			||||||
 | 
					    const basePlan =
 | 
				
			||||||
 | 
					        firstChangeWithSnapshot?.payload?.snapshot || currentReleasePlan;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!basePlan) {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modifiedPlan = applyProgressionChanges(basePlan, progressionChanges);
 | 
				
			||||||
 | 
					    const milestonesWithAutomation =
 | 
				
			||||||
 | 
					        getMilestonesWithAutomation(progressionChanges);
 | 
				
			||||||
 | 
					    const milestonesWithDeletedAutomation =
 | 
				
			||||||
 | 
					        getMilestonesWithDeletedAutomation(progressionChanges);
 | 
				
			||||||
 | 
					    const changeDescriptions = getChangeDescriptions(
 | 
				
			||||||
 | 
					        progressionChanges,
 | 
				
			||||||
 | 
					        basePlan,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <StyledTabs>
 | 
				
			||||||
 | 
					            <ChangeItemWrapper>
 | 
				
			||||||
 | 
					                <ChangeItemInfo>
 | 
				
			||||||
 | 
					                    {progressionChanges.map((change, index) => {
 | 
				
			||||||
 | 
					                        const Component =
 | 
				
			||||||
 | 
					                            change.action === 'deleteMilestoneProgression'
 | 
				
			||||||
 | 
					                                ? Deleted
 | 
				
			||||||
 | 
					                                : Added;
 | 
				
			||||||
 | 
					                        return (
 | 
				
			||||||
 | 
					                            <Component key={index}>
 | 
				
			||||||
 | 
					                                {changeDescriptions[index]}
 | 
				
			||||||
 | 
					                            </Component>
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    })}
 | 
				
			||||||
 | 
					                </ChangeItemInfo>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <TabList>
 | 
				
			||||||
 | 
					                        <Tab>View change</Tab>
 | 
				
			||||||
 | 
					                        <Tab>View diff</Tab>
 | 
				
			||||||
 | 
					                    </TabList>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </ChangeItemWrapper>
 | 
				
			||||||
 | 
					            <TabPanel>
 | 
				
			||||||
 | 
					                <MilestoneListRenderer
 | 
				
			||||||
 | 
					                    plan={modifiedPlan}
 | 
				
			||||||
 | 
					                    changeRequestState={changeRequestState}
 | 
				
			||||||
 | 
					                    milestonesWithAutomation={milestonesWithAutomation}
 | 
				
			||||||
 | 
					                    milestonesWithDeletedAutomation={
 | 
				
			||||||
 | 
					                        milestonesWithDeletedAutomation
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    onUpdateAutomation={onUpdateChangeRequestSubmit}
 | 
				
			||||||
 | 
					                    onDeleteAutomation={onDeleteChangeRequestSubmit}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </TabPanel>
 | 
				
			||||||
 | 
					            <TabPanel variant='diff'>
 | 
				
			||||||
 | 
					                <EventDiff
 | 
				
			||||||
 | 
					                    entry={{
 | 
				
			||||||
 | 
					                        preData: basePlan,
 | 
				
			||||||
 | 
					                        data: modifiedPlan,
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </TabPanel>
 | 
				
			||||||
 | 
					        </StyledTabs>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -80,6 +80,7 @@ export const FeatureChange: FC<{
 | 
				
			|||||||
    feature: IChangeRequestFeature;
 | 
					    feature: IChangeRequestFeature;
 | 
				
			||||||
    onNavigate?: () => void;
 | 
					    onNavigate?: () => void;
 | 
				
			||||||
    isDefaultChange?: boolean;
 | 
					    isDefaultChange?: boolean;
 | 
				
			||||||
 | 
					    onRefetch?: () => void;
 | 
				
			||||||
}> = ({
 | 
					}> = ({
 | 
				
			||||||
    index,
 | 
					    index,
 | 
				
			||||||
    change,
 | 
					    change,
 | 
				
			||||||
@ -88,6 +89,7 @@ export const FeatureChange: FC<{
 | 
				
			|||||||
    actions,
 | 
					    actions,
 | 
				
			||||||
    onNavigate,
 | 
					    onNavigate,
 | 
				
			||||||
    isDefaultChange,
 | 
					    isDefaultChange,
 | 
				
			||||||
 | 
					    onRefetch,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const lastIndex = feature.defaultChange
 | 
					    const lastIndex = feature.defaultChange
 | 
				
			||||||
        ? feature.changes.length + 1
 | 
					        ? feature.changes.length + 1
 | 
				
			||||||
@ -204,7 +206,10 @@ export const FeatureChange: FC<{
 | 
				
			|||||||
                )}
 | 
					                )}
 | 
				
			||||||
                {(change.action === 'addReleasePlan' ||
 | 
					                {(change.action === 'addReleasePlan' ||
 | 
				
			||||||
                    change.action === 'deleteReleasePlan' ||
 | 
					                    change.action === 'deleteReleasePlan' ||
 | 
				
			||||||
                    change.action === 'startMilestone') && (
 | 
					                    change.action === 'startMilestone' ||
 | 
				
			||||||
 | 
					                    change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					                    change.action === 'updateMilestoneProgression' ||
 | 
				
			||||||
 | 
					                    change.action === 'deleteMilestoneProgression') && (
 | 
				
			||||||
                    <ReleasePlanChange
 | 
					                    <ReleasePlanChange
 | 
				
			||||||
                        actions={actions}
 | 
					                        actions={actions}
 | 
				
			||||||
                        change={change}
 | 
					                        change={change}
 | 
				
			||||||
@ -212,6 +217,8 @@ export const FeatureChange: FC<{
 | 
				
			|||||||
                        environmentName={changeRequest.environment}
 | 
					                        environmentName={changeRequest.environment}
 | 
				
			||||||
                        projectId={changeRequest.project}
 | 
					                        projectId={changeRequest.project}
 | 
				
			||||||
                        changeRequestState={changeRequest.state}
 | 
					                        changeRequestState={changeRequest.state}
 | 
				
			||||||
 | 
					                        feature={feature}
 | 
				
			||||||
 | 
					                        onRefetch={onRefetch}
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
            </ChangeInnerBox>
 | 
					            </ChangeInnerBox>
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
 | 
					import type { IReleasePlan } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
				
			||||||
 | 
					import type { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
 | 
					import { ReleasePlanMilestone } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone';
 | 
				
			||||||
 | 
					import { MilestoneAutomationSection } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx';
 | 
				
			||||||
 | 
					import { MilestoneTransitionDisplay } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneTransitionDisplay.tsx';
 | 
				
			||||||
 | 
					import type { MilestoneStatus } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
 | 
				
			||||||
 | 
					import { Badge } from 'component/common/Badge/Badge';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledConnection = styled('div')(({ theme }) => ({
 | 
				
			||||||
 | 
					    width: 2,
 | 
				
			||||||
 | 
					    height: theme.spacing(2),
 | 
				
			||||||
 | 
					    backgroundColor: theme.palette.divider,
 | 
				
			||||||
 | 
					    marginLeft: theme.spacing(3.25),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MilestoneListRendererProps {
 | 
				
			||||||
 | 
					    plan: IReleasePlan;
 | 
				
			||||||
 | 
					    changeRequestState: ChangeRequestState;
 | 
				
			||||||
 | 
					    milestonesWithAutomation?: Set<string>;
 | 
				
			||||||
 | 
					    milestonesWithDeletedAutomation?: Set<string>;
 | 
				
			||||||
 | 
					    onUpdateAutomation?: (
 | 
				
			||||||
 | 
					        sourceMilestoneId: string,
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => Promise<void>;
 | 
				
			||||||
 | 
					    onDeleteAutomation?: (sourceMilestoneId: string) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MilestoneListRenderer = ({
 | 
				
			||||||
 | 
					    plan,
 | 
				
			||||||
 | 
					    changeRequestState,
 | 
				
			||||||
 | 
					    milestonesWithAutomation = new Set(),
 | 
				
			||||||
 | 
					    milestonesWithDeletedAutomation = new Set(),
 | 
				
			||||||
 | 
					    onUpdateAutomation,
 | 
				
			||||||
 | 
					    onDeleteAutomation,
 | 
				
			||||||
 | 
					}: MilestoneListRendererProps) => {
 | 
				
			||||||
 | 
					    // TODO: Split into read and write model at the type level to avoid having optional handlers
 | 
				
			||||||
 | 
					    const readonly =
 | 
				
			||||||
 | 
					        changeRequestState === 'Applied' || changeRequestState === 'Cancelled';
 | 
				
			||||||
 | 
					    const status: MilestoneStatus = 'not-started';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            {plan.milestones.map((milestone, index) => {
 | 
				
			||||||
 | 
					                const isNotLastMilestone = index < plan.milestones.length - 1;
 | 
				
			||||||
 | 
					                const shouldShowAutomation =
 | 
				
			||||||
 | 
					                    milestonesWithAutomation.has(milestone.id) ||
 | 
				
			||||||
 | 
					                    milestonesWithDeletedAutomation.has(milestone.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const showAutomation =
 | 
				
			||||||
 | 
					                    isNotLastMilestone && shouldShowAutomation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const hasPendingDelete = milestonesWithDeletedAutomation.has(
 | 
				
			||||||
 | 
					                    milestone.id,
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const badge = hasPendingDelete ? (
 | 
				
			||||||
 | 
					                    <Badge color='error'>Deleted in draft</Badge>
 | 
				
			||||||
 | 
					                ) : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const automationSection =
 | 
				
			||||||
 | 
					                    showAutomation && milestone.transitionCondition ? (
 | 
				
			||||||
 | 
					                        <MilestoneAutomationSection status={status}>
 | 
				
			||||||
 | 
					                            <MilestoneTransitionDisplay
 | 
				
			||||||
 | 
					                                intervalMinutes={
 | 
				
			||||||
 | 
					                                    milestone.transitionCondition
 | 
				
			||||||
 | 
					                                        .intervalMinutes
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                onSave={async (payload) => {
 | 
				
			||||||
 | 
					                                    await onUpdateAutomation?.(
 | 
				
			||||||
 | 
					                                        milestone.id,
 | 
				
			||||||
 | 
					                                        payload,
 | 
				
			||||||
 | 
					                                    );
 | 
				
			||||||
 | 
					                                    return { shouldReset: true };
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                                onDelete={() =>
 | 
				
			||||||
 | 
					                                    onDeleteAutomation?.(milestone.id)
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                milestoneName={milestone.name}
 | 
				
			||||||
 | 
					                                status={status}
 | 
				
			||||||
 | 
					                                badge={badge}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                        </MilestoneAutomationSection>
 | 
				
			||||||
 | 
					                    ) : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return (
 | 
				
			||||||
 | 
					                    <div key={milestone.id}>
 | 
				
			||||||
 | 
					                        <ReleasePlanMilestone
 | 
				
			||||||
 | 
					                            readonly={readonly}
 | 
				
			||||||
 | 
					                            milestone={milestone}
 | 
				
			||||||
 | 
					                            automationSection={automationSection}
 | 
				
			||||||
 | 
					                            allMilestones={plan.milestones}
 | 
				
			||||||
 | 
					                            activeMilestoneId={plan.activeMilestoneId}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        {isNotLastMilestone && <StyledConnection />}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					import type { FC, ReactNode } from 'react';
 | 
				
			||||||
 | 
					import { Typography } from '@mui/material';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    ChangeRequestState,
 | 
				
			||||||
 | 
					    IChangeRequestCreateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestUpdateMilestoneProgression,
 | 
				
			||||||
 | 
					} from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
 | 
					import type { IReleasePlan } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
				
			||||||
 | 
					import { EventDiff } from 'component/events/EventDiff/EventDiff';
 | 
				
			||||||
 | 
					import { Tab, TabList, TabPanel, Tabs } from './ChangeTabComponents.tsx';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Action,
 | 
				
			||||||
 | 
					    Added,
 | 
				
			||||||
 | 
					    ChangeItemInfo,
 | 
				
			||||||
 | 
					    ChangeItemWrapper,
 | 
				
			||||||
 | 
					} from './Change.styles.tsx';
 | 
				
			||||||
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
 | 
					import { MilestoneListRenderer } from './MilestoneListRenderer.tsx';
 | 
				
			||||||
 | 
					import { applyProgressionChanges } from './applyProgressionChanges.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledTabs = styled(Tabs)(({ theme }) => ({
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    flexFlow: 'column',
 | 
				
			||||||
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ProgressionChangeProps {
 | 
				
			||||||
 | 
					    change:
 | 
				
			||||||
 | 
					        | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
 | 
					        | IChangeRequestUpdateMilestoneProgression;
 | 
				
			||||||
 | 
					    currentReleasePlan?: IReleasePlan;
 | 
				
			||||||
 | 
					    actions?: ReactNode;
 | 
				
			||||||
 | 
					    changeRequestState: ChangeRequestState;
 | 
				
			||||||
 | 
					    onUpdateChangeRequestSubmit?: (
 | 
				
			||||||
 | 
					        sourceMilestoneId: string,
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => Promise<void>;
 | 
				
			||||||
 | 
					    onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ProgressionChange: FC<ProgressionChangeProps> = ({
 | 
				
			||||||
 | 
					    change,
 | 
				
			||||||
 | 
					    currentReleasePlan,
 | 
				
			||||||
 | 
					    actions,
 | 
				
			||||||
 | 
					    changeRequestState,
 | 
				
			||||||
 | 
					    onUpdateChangeRequestSubmit,
 | 
				
			||||||
 | 
					    onDeleteChangeRequestSubmit,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const basePlan = change.payload.snapshot || currentReleasePlan;
 | 
				
			||||||
 | 
					    if (!basePlan) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isCreate = change.action === 'createMilestoneProgression';
 | 
				
			||||||
 | 
					    const sourceId = change.payload.sourceMilestone;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!sourceId) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const sourceMilestone = basePlan.milestones.find(
 | 
				
			||||||
 | 
					        (milestone) => milestone.id === sourceId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const sourceMilestoneName = sourceMilestone?.name || sourceId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const targetMilestoneName = isCreate
 | 
				
			||||||
 | 
					        ? basePlan.milestones.find(
 | 
				
			||||||
 | 
					              (milestone) => milestone.id === change.payload.targetMilestone,
 | 
				
			||||||
 | 
					          )?.name || change.payload.targetMilestone
 | 
				
			||||||
 | 
					        : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const modifiedPlan = applyProgressionChanges(basePlan, [change]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const previousMilestone = sourceMilestone;
 | 
				
			||||||
 | 
					    const newMilestone = modifiedPlan.milestones.find(
 | 
				
			||||||
 | 
					        (milestone) => milestone.id === sourceId,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <StyledTabs>
 | 
				
			||||||
 | 
					            <ChangeItemWrapper>
 | 
				
			||||||
 | 
					                <ChangeItemInfo>
 | 
				
			||||||
 | 
					                    {isCreate ? (
 | 
				
			||||||
 | 
					                        <>
 | 
				
			||||||
 | 
					                            <Added>Adding automation to release plan</Added>
 | 
				
			||||||
 | 
					                            <Typography component='span'>
 | 
				
			||||||
 | 
					                                {sourceMilestoneName} → {targetMilestoneName}
 | 
				
			||||||
 | 
					                            </Typography>
 | 
				
			||||||
 | 
					                        </>
 | 
				
			||||||
 | 
					                    ) : (
 | 
				
			||||||
 | 
					                        <>
 | 
				
			||||||
 | 
					                            <Action>Updating automation in release plan</Action>
 | 
				
			||||||
 | 
					                            <Typography component='span'>
 | 
				
			||||||
 | 
					                                {sourceMilestoneName}
 | 
				
			||||||
 | 
					                            </Typography>
 | 
				
			||||||
 | 
					                        </>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                </ChangeItemInfo>
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <TabList>
 | 
				
			||||||
 | 
					                        <Tab>View change</Tab>
 | 
				
			||||||
 | 
					                        <Tab>View diff</Tab>
 | 
				
			||||||
 | 
					                    </TabList>
 | 
				
			||||||
 | 
					                    {actions}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </ChangeItemWrapper>
 | 
				
			||||||
 | 
					            <TabPanel>
 | 
				
			||||||
 | 
					                <MilestoneListRenderer
 | 
				
			||||||
 | 
					                    plan={modifiedPlan}
 | 
				
			||||||
 | 
					                    changeRequestState={changeRequestState}
 | 
				
			||||||
 | 
					                    milestonesWithAutomation={new Set([sourceId])}
 | 
				
			||||||
 | 
					                    onUpdateAutomation={onUpdateChangeRequestSubmit}
 | 
				
			||||||
 | 
					                    onDeleteAutomation={onDeleteChangeRequestSubmit}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </TabPanel>
 | 
				
			||||||
 | 
					            <TabPanel variant='diff'>
 | 
				
			||||||
 | 
					                <EventDiff
 | 
				
			||||||
 | 
					                    entry={{
 | 
				
			||||||
 | 
					                        preData: previousMilestone,
 | 
				
			||||||
 | 
					                        data: newMilestone,
 | 
				
			||||||
 | 
					                    }}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </TabPanel>
 | 
				
			||||||
 | 
					        </StyledTabs>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -5,6 +5,9 @@ import type {
 | 
				
			|||||||
    IChangeRequestAddReleasePlan,
 | 
					    IChangeRequestAddReleasePlan,
 | 
				
			||||||
    IChangeRequestDeleteReleasePlan,
 | 
					    IChangeRequestDeleteReleasePlan,
 | 
				
			||||||
    IChangeRequestStartMilestone,
 | 
					    IChangeRequestStartMilestone,
 | 
				
			||||||
 | 
					    IChangeRequestCreateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestUpdateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestDeleteMilestoneProgression,
 | 
				
			||||||
} from 'component/changeRequest/changeRequest.types';
 | 
					} from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
 | 
					import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview';
 | 
				
			||||||
import { useFeatureReleasePlans } from 'hooks/api/getters/useFeatureReleasePlans/useFeatureReleasePlans';
 | 
					import { useFeatureReleasePlans } from 'hooks/api/getters/useFeatureReleasePlans/useFeatureReleasePlans';
 | 
				
			||||||
@ -21,6 +24,12 @@ import {
 | 
				
			|||||||
    ChangeItemWrapper,
 | 
					    ChangeItemWrapper,
 | 
				
			||||||
    Deleted,
 | 
					    Deleted,
 | 
				
			||||||
} from './Change.styles.tsx';
 | 
					} from './Change.styles.tsx';
 | 
				
			||||||
 | 
					import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
				
			||||||
 | 
					import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
				
			||||||
 | 
					import useToast from 'hooks/useToast';
 | 
				
			||||||
 | 
					import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
				
			||||||
 | 
					import { ProgressionChange } from './ProgressionChange.tsx';
 | 
				
			||||||
 | 
					import { ConsolidatedProgressionChanges } from './ConsolidatedProgressionChanges.tsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
 | 
					const StyledTabs = styled(Tabs)(({ theme }) => ({
 | 
				
			||||||
    display: 'flex',
 | 
					    display: 'flex',
 | 
				
			||||||
@ -235,11 +244,16 @@ export const ReleasePlanChange: FC<{
 | 
				
			|||||||
    change:
 | 
					    change:
 | 
				
			||||||
        | IChangeRequestAddReleasePlan
 | 
					        | IChangeRequestAddReleasePlan
 | 
				
			||||||
        | IChangeRequestDeleteReleasePlan
 | 
					        | IChangeRequestDeleteReleasePlan
 | 
				
			||||||
        | IChangeRequestStartMilestone;
 | 
					        | IChangeRequestStartMilestone
 | 
				
			||||||
 | 
					        | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
 | 
					        | IChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					        | IChangeRequestDeleteMilestoneProgression;
 | 
				
			||||||
    environmentName: string;
 | 
					    environmentName: string;
 | 
				
			||||||
    featureName: string;
 | 
					    featureName: string;
 | 
				
			||||||
    projectId: string;
 | 
					    projectId: string;
 | 
				
			||||||
    changeRequestState: ChangeRequestState;
 | 
					    changeRequestState: ChangeRequestState;
 | 
				
			||||||
 | 
					    feature?: any; // Optional feature object for consolidated progression changes
 | 
				
			||||||
 | 
					    onRefetch?: () => void;
 | 
				
			||||||
}> = ({
 | 
					}> = ({
 | 
				
			||||||
    actions,
 | 
					    actions,
 | 
				
			||||||
    change,
 | 
					    change,
 | 
				
			||||||
@ -247,13 +261,100 @@ export const ReleasePlanChange: FC<{
 | 
				
			|||||||
    environmentName,
 | 
					    environmentName,
 | 
				
			||||||
    projectId,
 | 
					    projectId,
 | 
				
			||||||
    changeRequestState,
 | 
					    changeRequestState,
 | 
				
			||||||
 | 
					    feature,
 | 
				
			||||||
 | 
					    onRefetch,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const { releasePlans } = useFeatureReleasePlans(
 | 
					    const { releasePlans, refetch } = useFeatureReleasePlans(
 | 
				
			||||||
        projectId,
 | 
					        projectId,
 | 
				
			||||||
        featureName,
 | 
					        featureName,
 | 
				
			||||||
        environmentName,
 | 
					        environmentName,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const currentReleasePlan = releasePlans[0];
 | 
					    const currentReleasePlan = releasePlans[0];
 | 
				
			||||||
 | 
					    const { addChange } = useChangeRequestApi();
 | 
				
			||||||
 | 
					    const { refetch: refetchChangeRequests } =
 | 
				
			||||||
 | 
					        usePendingChangeRequests(projectId);
 | 
				
			||||||
 | 
					    const { setToastData } = useToast();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleUpdateChangeRequestSubmit = async (
 | 
				
			||||||
 | 
					        sourceMilestoneId: string,
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        await addChange(projectId, environmentName, {
 | 
				
			||||||
 | 
					            feature: featureName,
 | 
				
			||||||
 | 
					            action: 'updateMilestoneProgression',
 | 
				
			||||||
 | 
					            payload: {
 | 
				
			||||||
 | 
					                sourceMilestone: sourceMilestoneId,
 | 
				
			||||||
 | 
					                ...payload,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await refetchChangeRequests();
 | 
				
			||||||
 | 
					        setToastData({
 | 
				
			||||||
 | 
					            type: 'success',
 | 
				
			||||||
 | 
					            text: 'Added to draft',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (onRefetch) {
 | 
				
			||||||
 | 
					            await onRefetch();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleDeleteChangeRequestSubmit = async (
 | 
				
			||||||
 | 
					        sourceMilestoneId: string,
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        await addChange(projectId, environmentName, {
 | 
				
			||||||
 | 
					            feature: featureName,
 | 
				
			||||||
 | 
					            action: 'deleteMilestoneProgression',
 | 
				
			||||||
 | 
					            payload: {
 | 
				
			||||||
 | 
					                sourceMilestone: sourceMilestoneId,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await refetchChangeRequests();
 | 
				
			||||||
 | 
					        setToastData({
 | 
				
			||||||
 | 
					            type: 'success',
 | 
				
			||||||
 | 
					            text: 'Added to draft',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (onRefetch) {
 | 
				
			||||||
 | 
					            await onRefetch();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If this is a progression change and we have the full feature object,
 | 
				
			||||||
 | 
					    // check if we should consolidate with other progression changes
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        feature &&
 | 
				
			||||||
 | 
					        (change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					            change.action === 'updateMilestoneProgression' ||
 | 
				
			||||||
 | 
					            change.action === 'deleteMilestoneProgression')
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        const progressionChanges = feature.changes.filter(
 | 
				
			||||||
 | 
					            (
 | 
				
			||||||
 | 
					                change,
 | 
				
			||||||
 | 
					            ): change is
 | 
				
			||||||
 | 
					                | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
 | 
					                | IChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					                | IChangeRequestDeleteMilestoneProgression =>
 | 
				
			||||||
 | 
					                change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					                change.action === 'updateMilestoneProgression' ||
 | 
				
			||||||
 | 
					                change.action === 'deleteMilestoneProgression',
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Only render if this is the first progression change
 | 
				
			||||||
 | 
					        const isFirstProgression =
 | 
				
			||||||
 | 
					            progressionChanges.length > 0 && progressionChanges[0] === change;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!isFirstProgression) {
 | 
				
			||||||
 | 
					            return null; // Skip rendering, will be handled by the first one
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <ConsolidatedProgressionChanges
 | 
				
			||||||
 | 
					                feature={feature}
 | 
				
			||||||
 | 
					                currentReleasePlan={currentReleasePlan}
 | 
				
			||||||
 | 
					                changeRequestState={changeRequestState}
 | 
				
			||||||
 | 
					                onUpdateChangeRequestSubmit={handleUpdateChangeRequestSubmit}
 | 
				
			||||||
 | 
					                onDeleteChangeRequestSubmit={handleDeleteChangeRequestSubmit}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
@ -282,6 +383,21 @@ export const ReleasePlanChange: FC<{
 | 
				
			|||||||
                    actions={actions}
 | 
					                    actions={actions}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
 | 
					            {(change.action === 'createMilestoneProgression' ||
 | 
				
			||||||
 | 
					                change.action === 'updateMilestoneProgression') && (
 | 
				
			||||||
 | 
					                <ProgressionChange
 | 
				
			||||||
 | 
					                    change={change}
 | 
				
			||||||
 | 
					                    currentReleasePlan={currentReleasePlan}
 | 
				
			||||||
 | 
					                    actions={actions}
 | 
				
			||||||
 | 
					                    changeRequestState={changeRequestState}
 | 
				
			||||||
 | 
					                    onUpdateChangeRequestSubmit={
 | 
				
			||||||
 | 
					                        handleUpdateChangeRequestSubmit
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    onDeleteChangeRequestSubmit={
 | 
				
			||||||
 | 
					                        handleDeleteChangeRequestSubmit
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
        </>
 | 
					        </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import type { IReleasePlan } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    IChangeRequestCreateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestUpdateMilestoneProgression,
 | 
				
			||||||
 | 
					    IChangeRequestDeleteMilestoneProgression,
 | 
				
			||||||
 | 
					} from 'component/changeRequest/changeRequest.types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProgressionChange =
 | 
				
			||||||
 | 
					    | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
 | 
					    | IChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					    | IChangeRequestDeleteMilestoneProgression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const applyProgressionChanges = (
 | 
				
			||||||
 | 
					    basePlan: IReleasePlan,
 | 
				
			||||||
 | 
					    progressionChanges: ProgressionChange[],
 | 
				
			||||||
 | 
					): IReleasePlan => {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        ...basePlan,
 | 
				
			||||||
 | 
					        milestones: basePlan.milestones.map((milestone) => {
 | 
				
			||||||
 | 
					            const createChange = progressionChanges.find(
 | 
				
			||||||
 | 
					                (change): change is IChangeRequestCreateMilestoneProgression =>
 | 
				
			||||||
 | 
					                    change.action === 'createMilestoneProgression' &&
 | 
				
			||||||
 | 
					                    change.payload.sourceMilestone === milestone.id,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            const updateChange = progressionChanges.find(
 | 
				
			||||||
 | 
					                (change): change is IChangeRequestUpdateMilestoneProgression =>
 | 
				
			||||||
 | 
					                    change.action === 'updateMilestoneProgression' &&
 | 
				
			||||||
 | 
					                    change.payload.sourceMilestone === milestone.id,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            const deleteChange = progressionChanges.find(
 | 
				
			||||||
 | 
					                (change): change is IChangeRequestDeleteMilestoneProgression =>
 | 
				
			||||||
 | 
					                    change.action === 'deleteMilestoneProgression' &&
 | 
				
			||||||
 | 
					                    change.payload.sourceMilestone === milestone.id,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (deleteChange) {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    ...milestone,
 | 
				
			||||||
 | 
					                    transitionCondition: null,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const change = updateChange || createChange;
 | 
				
			||||||
 | 
					            if (change) {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    ...milestone,
 | 
				
			||||||
 | 
					                    transitionCondition: change.payload.transitionCondition,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return milestone;
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -137,7 +137,8 @@ type ChangeRequestPayload =
 | 
				
			|||||||
    | ChangeRequestDeleteReleasePlan
 | 
					    | ChangeRequestDeleteReleasePlan
 | 
				
			||||||
    | ChangeRequestStartMilestone
 | 
					    | ChangeRequestStartMilestone
 | 
				
			||||||
    | ChangeRequestCreateMilestoneProgression
 | 
					    | ChangeRequestCreateMilestoneProgression
 | 
				
			||||||
    | ChangeRequestUpdateMilestoneProgression;
 | 
					    | ChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					    | ChangeRequestDeleteMilestoneProgression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
 | 
					export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
 | 
				
			||||||
    action: 'addStrategy';
 | 
					    action: 'addStrategy';
 | 
				
			||||||
@ -206,6 +207,12 @@ export interface IChangeRequestUpdateMilestoneProgression
 | 
				
			|||||||
    payload: ChangeRequestUpdateMilestoneProgression;
 | 
					    payload: ChangeRequestUpdateMilestoneProgression;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IChangeRequestDeleteMilestoneProgression
 | 
				
			||||||
 | 
					    extends IChangeRequestChangeBase {
 | 
				
			||||||
 | 
					    action: 'deleteMilestoneProgression';
 | 
				
			||||||
 | 
					    payload: ChangeRequestDeleteMilestoneProgression;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IChangeRequestReorderStrategy
 | 
					export interface IChangeRequestReorderStrategy
 | 
				
			||||||
    extends IChangeRequestChangeBase {
 | 
					    extends IChangeRequestChangeBase {
 | 
				
			||||||
    action: 'reorderStrategy';
 | 
					    action: 'reorderStrategy';
 | 
				
			||||||
@ -255,7 +262,8 @@ export type IFeatureChange =
 | 
				
			|||||||
    | IChangeRequestDeleteReleasePlan
 | 
					    | IChangeRequestDeleteReleasePlan
 | 
				
			||||||
    | IChangeRequestStartMilestone
 | 
					    | IChangeRequestStartMilestone
 | 
				
			||||||
    | IChangeRequestCreateMilestoneProgression
 | 
					    | IChangeRequestCreateMilestoneProgression
 | 
				
			||||||
    | IChangeRequestUpdateMilestoneProgression;
 | 
					    | IChangeRequestUpdateMilestoneProgression
 | 
				
			||||||
 | 
					    | IChangeRequestDeleteMilestoneProgression;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ISegmentChange =
 | 
					export type ISegmentChange =
 | 
				
			||||||
    | IChangeRequestUpdateSegment
 | 
					    | IChangeRequestUpdateSegment
 | 
				
			||||||
@ -288,13 +296,24 @@ type ChangeRequestStartMilestone = {
 | 
				
			|||||||
    snapshot?: IReleasePlan;
 | 
					    snapshot?: IReleasePlan;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ChangeRequestCreateMilestoneProgression = CreateMilestoneProgressionSchema;
 | 
					type ChangeRequestCreateMilestoneProgression =
 | 
				
			||||||
 | 
					    CreateMilestoneProgressionSchema & {
 | 
				
			||||||
 | 
					        snapshot?: IReleasePlan;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ChangeRequestUpdateMilestoneProgression =
 | 
					type ChangeRequestUpdateMilestoneProgression =
 | 
				
			||||||
    UpdateMilestoneProgressionSchema & {
 | 
					    UpdateMilestoneProgressionSchema & {
 | 
				
			||||||
        sourceMilestoneId: string;
 | 
					        sourceMilestoneId?: string;
 | 
				
			||||||
 | 
					        sourceMilestone?: string; // Backward compatibility for existing change requests
 | 
				
			||||||
 | 
					        snapshot?: IReleasePlan;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ChangeRequestDeleteMilestoneProgression = {
 | 
				
			||||||
 | 
					    sourceMilestoneId?: string;
 | 
				
			||||||
 | 
					    sourceMilestone?: string; // Backward compatibility for existing change requests
 | 
				
			||||||
 | 
					    snapshot?: IReleasePlan;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ChangeRequestAddStrategy = Pick<
 | 
					export type ChangeRequestAddStrategy = Pick<
 | 
				
			||||||
    IFeatureStrategy,
 | 
					    IFeatureStrategy,
 | 
				
			||||||
    | 'parameters'
 | 
					    | 'parameters'
 | 
				
			||||||
@ -334,4 +353,5 @@ export type ChangeRequestAction =
 | 
				
			|||||||
    | 'deleteReleasePlan'
 | 
					    | 'deleteReleasePlan'
 | 
				
			||||||
    | 'startMilestone'
 | 
					    | 'startMilestone'
 | 
				
			||||||
    | 'createMilestoneProgression'
 | 
					    | 'createMilestoneProgression'
 | 
				
			||||||
    | 'updateMilestoneProgression';
 | 
					    | 'updateMilestoneProgression'
 | 
				
			||||||
 | 
					    | 'deleteMilestoneProgression';
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,7 @@
 | 
				
			|||||||
import { useState } from 'react';
 | 
					 | 
				
			||||||
import { Button, styled } from '@mui/material';
 | 
					import { Button, styled } from '@mui/material';
 | 
				
			||||||
import BoltIcon from '@mui/icons-material/Bolt';
 | 
					import BoltIcon from '@mui/icons-material/Bolt';
 | 
				
			||||||
import { useMilestoneProgressionForm } from '../hooks/useMilestoneProgressionForm.js';
 | 
					import { useMilestoneProgressionForm } from '../hooks/useMilestoneProgressionForm.js';
 | 
				
			||||||
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
 | 
					 | 
				
			||||||
import useToast from 'hooks/useToast';
 | 
					 | 
				
			||||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
					 | 
				
			||||||
import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
 | 
					import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
 | 
				
			||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
					 | 
				
			||||||
import type { CreateMilestoneProgressionSchema } from 'openapi';
 | 
					import type { CreateMilestoneProgressionSchema } from 'openapi';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledFormContainer = styled('div')(({ theme }) => ({
 | 
					const StyledFormContainer = styled('div')(({ theme }) => ({
 | 
				
			||||||
@ -60,74 +55,27 @@ const StyledErrorMessage = styled('span')(({ theme }) => ({
 | 
				
			|||||||
interface IMilestoneProgressionFormProps {
 | 
					interface IMilestoneProgressionFormProps {
 | 
				
			||||||
    sourceMilestoneId: string;
 | 
					    sourceMilestoneId: string;
 | 
				
			||||||
    targetMilestoneId: string;
 | 
					    targetMilestoneId: string;
 | 
				
			||||||
    projectId: string;
 | 
					    onSubmit: (payload: CreateMilestoneProgressionSchema) => Promise<void>;
 | 
				
			||||||
    environment: string;
 | 
					 | 
				
			||||||
    featureName: string;
 | 
					 | 
				
			||||||
    onSave: () => void;
 | 
					 | 
				
			||||||
    onCancel: () => void;
 | 
					    onCancel: () => void;
 | 
				
			||||||
    onChangeRequestSubmit?: (
 | 
					 | 
				
			||||||
        progressionPayload: CreateMilestoneProgressionSchema,
 | 
					 | 
				
			||||||
    ) => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MilestoneProgressionForm = ({
 | 
					export const MilestoneProgressionForm = ({
 | 
				
			||||||
    sourceMilestoneId,
 | 
					    sourceMilestoneId,
 | 
				
			||||||
    targetMilestoneId,
 | 
					    targetMilestoneId,
 | 
				
			||||||
    projectId,
 | 
					    onSubmit,
 | 
				
			||||||
    environment,
 | 
					 | 
				
			||||||
    featureName,
 | 
					 | 
				
			||||||
    onSave,
 | 
					 | 
				
			||||||
    onCancel,
 | 
					    onCancel,
 | 
				
			||||||
    onChangeRequestSubmit,
 | 
					 | 
				
			||||||
}: IMilestoneProgressionFormProps) => {
 | 
					}: IMilestoneProgressionFormProps) => {
 | 
				
			||||||
    const form = useMilestoneProgressionForm(
 | 
					    const form = useMilestoneProgressionForm(
 | 
				
			||||||
        sourceMilestoneId,
 | 
					        sourceMilestoneId,
 | 
				
			||||||
        targetMilestoneId,
 | 
					        targetMilestoneId,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const { createMilestoneProgression } = useMilestoneProgressionsApi();
 | 
					 | 
				
			||||||
    const { setToastData, setToastApiError } = useToast();
 | 
					 | 
				
			||||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const [isSubmitting, setIsSubmitting] = useState(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleChangeRequestSubmit = () => {
 | 
					 | 
				
			||||||
        const progressionPayload = form.getProgressionPayload();
 | 
					 | 
				
			||||||
        onChangeRequestSubmit?.(progressionPayload);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleDirectSubmit = async () => {
 | 
					 | 
				
			||||||
        setIsSubmitting(true);
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            await createMilestoneProgression(
 | 
					 | 
				
			||||||
                projectId,
 | 
					 | 
				
			||||||
                environment,
 | 
					 | 
				
			||||||
                featureName,
 | 
					 | 
				
			||||||
                form.getProgressionPayload(),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            setToastData({
 | 
					 | 
				
			||||||
                type: 'success',
 | 
					 | 
				
			||||||
                text: 'Automation configured successfully',
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            onSave();
 | 
					 | 
				
			||||||
        } catch (error: unknown) {
 | 
					 | 
				
			||||||
            setToastApiError(formatUnknownError(error));
 | 
					 | 
				
			||||||
        } finally {
 | 
					 | 
				
			||||||
            setIsSubmitting(false);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleSubmit = async () => {
 | 
					    const handleSubmit = async () => {
 | 
				
			||||||
        if (isSubmitting) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!form.validate()) {
 | 
					        if (!form.validate()) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isChangeRequestConfigured(environment) && onChangeRequestSubmit) {
 | 
					        await onSubmit(form.getProgressionPayload());
 | 
				
			||||||
            handleChangeRequestSubmit();
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            await handleDirectSubmit();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleKeyDown = (event: React.KeyboardEvent) => {
 | 
					    const handleKeyDown = (event: React.KeyboardEvent) => {
 | 
				
			||||||
@ -150,19 +98,13 @@ export const MilestoneProgressionForm = ({
 | 
				
			|||||||
                    timeUnit={form.timeUnit}
 | 
					                    timeUnit={form.timeUnit}
 | 
				
			||||||
                    onTimeValueChange={form.handleTimeValueChange}
 | 
					                    onTimeValueChange={form.handleTimeValueChange}
 | 
				
			||||||
                    onTimeUnitChange={form.handleTimeUnitChange}
 | 
					                    onTimeUnitChange={form.handleTimeUnitChange}
 | 
				
			||||||
                    disabled={isSubmitting}
 | 
					 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
            </StyledTopRow>
 | 
					            </StyledTopRow>
 | 
				
			||||||
            <StyledButtonGroup>
 | 
					            <StyledButtonGroup>
 | 
				
			||||||
                {form.errors.time && (
 | 
					                {form.errors.time && (
 | 
				
			||||||
                    <StyledErrorMessage>{form.errors.time}</StyledErrorMessage>
 | 
					                    <StyledErrorMessage>{form.errors.time}</StyledErrorMessage>
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
                <Button
 | 
					                <Button variant='outlined' onClick={onCancel} size='small'>
 | 
				
			||||||
                    variant='outlined'
 | 
					 | 
				
			||||||
                    onClick={onCancel}
 | 
					 | 
				
			||||||
                    size='small'
 | 
					 | 
				
			||||||
                    disabled={isSubmitting}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                    Cancel
 | 
					                    Cancel
 | 
				
			||||||
                </Button>
 | 
					                </Button>
 | 
				
			||||||
                <Button
 | 
					                <Button
 | 
				
			||||||
@ -170,9 +112,8 @@ export const MilestoneProgressionForm = ({
 | 
				
			|||||||
                    color='primary'
 | 
					                    color='primary'
 | 
				
			||||||
                    onClick={handleSubmit}
 | 
					                    onClick={handleSubmit}
 | 
				
			||||||
                    size='small'
 | 
					                    size='small'
 | 
				
			||||||
                    disabled={isSubmitting}
 | 
					 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                    {isSubmitting ? 'Saving...' : 'Save'}
 | 
					                    Save
 | 
				
			||||||
                </Button>
 | 
					                </Button>
 | 
				
			||||||
            </StyledButtonGroup>
 | 
					            </StyledButtonGroup>
 | 
				
			||||||
        </StyledFormContainer>
 | 
					        </StyledFormContainer>
 | 
				
			||||||
 | 
				
			|||||||
@ -13,8 +13,6 @@ import type {
 | 
				
			|||||||
import { useState } from 'react';
 | 
					import { useState } from 'react';
 | 
				
			||||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
					import { formatUnknownError } from 'utils/formatUnknownError';
 | 
				
			||||||
import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog.tsx';
 | 
					import { ReleasePlanRemoveDialog } from './ReleasePlanRemoveDialog.tsx';
 | 
				
			||||||
import { ReleasePlanMilestone } from './ReleasePlanMilestone/ReleasePlanMilestone.tsx';
 | 
					 | 
				
			||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
					 | 
				
			||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
					import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
				
			||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
					import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
 | 
				
			||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
					import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
 | 
				
			||||||
@ -22,13 +20,13 @@ import { ReleasePlanChangeRequestDialog } from './ChangeRequest/ReleasePlanChang
 | 
				
			|||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
					import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
 | 
				
			||||||
import { Truncator } from 'component/common/Truncator/Truncator';
 | 
					import { Truncator } from 'component/common/Truncator/Truncator';
 | 
				
			||||||
import { useUiFlag } from 'hooks/useUiFlag';
 | 
					import { useUiFlag } from 'hooks/useUiFlag';
 | 
				
			||||||
import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx';
 | 
					 | 
				
			||||||
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
 | 
					import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
 | 
				
			||||||
import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx';
 | 
					import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx';
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
    CreateMilestoneProgressionSchema,
 | 
					    CreateMilestoneProgressionSchema,
 | 
				
			||||||
    UpdateMilestoneProgressionSchema,
 | 
					    UpdateMilestoneProgressionSchema,
 | 
				
			||||||
} from 'openapi';
 | 
					} from 'openapi';
 | 
				
			||||||
 | 
					import { ReleasePlanMilestoneItem } from './ReleasePlanMilestoneItem/ReleasePlanMilestoneItem.tsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
					const StyledContainer = styled('div')(({ theme }) => ({
 | 
				
			||||||
    padding: theme.spacing(2),
 | 
					    padding: theme.spacing(2),
 | 
				
			||||||
@ -75,17 +73,6 @@ const StyledBody = styled('div')(({ theme }) => ({
 | 
				
			|||||||
    flexDirection: 'column',
 | 
					    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 {
 | 
					interface IReleasePlanProps {
 | 
				
			||||||
    plan: IReleasePlan;
 | 
					    plan: IReleasePlan;
 | 
				
			||||||
    environmentIsDisabled?: boolean;
 | 
					    environmentIsDisabled?: boolean;
 | 
				
			||||||
@ -140,8 +127,47 @@ export const ReleasePlan = ({
 | 
				
			|||||||
    >(null);
 | 
					    >(null);
 | 
				
			||||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
					    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
				
			||||||
    const { addChange } = useChangeRequestApi();
 | 
					    const { addChange } = useChangeRequestApi();
 | 
				
			||||||
    const { refetch: refetchChangeRequests } =
 | 
					    const { data: pendingChangeRequests, refetch: refetchChangeRequests } =
 | 
				
			||||||
        usePendingChangeRequests(projectId);
 | 
					        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 milestoneProgressionsEnabled = useUiFlag('milestoneProgression');
 | 
				
			||||||
    const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState<
 | 
					    const [progressionFormOpenIndex, setProgressionFormOpenIndex] = useState<
 | 
				
			||||||
        number | null
 | 
					        number | null
 | 
				
			||||||
@ -181,7 +207,6 @@ export const ReleasePlan = ({
 | 
				
			|||||||
                    action: 'createMilestoneProgression',
 | 
					                    action: 'createMilestoneProgression',
 | 
				
			||||||
                    payload: changeRequestAction.payload,
 | 
					                    payload: changeRequestAction.payload,
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                setProgressionFormOpenIndex(null);
 | 
					 | 
				
			||||||
                break;
 | 
					                break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            case 'updateMilestoneProgression':
 | 
					            case 'updateMilestoneProgression':
 | 
				
			||||||
@ -214,6 +239,7 @@ export const ReleasePlan = ({
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        setChangeRequestAction(null);
 | 
					        setChangeRequestAction(null);
 | 
				
			||||||
 | 
					        setProgressionFormOpenIndex(null);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const confirmRemoveReleasePlan = () => {
 | 
					    const confirmRemoveReleasePlan = () => {
 | 
				
			||||||
@ -288,33 +314,19 @@ export const ReleasePlan = ({
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleProgressionSave = async () => {
 | 
					    const handleAddToChangeRequest = (
 | 
				
			||||||
        setProgressionFormOpenIndex(null);
 | 
					        action:
 | 
				
			||||||
        await refetch();
 | 
					            | {
 | 
				
			||||||
    };
 | 
					                  type: 'createMilestoneProgression';
 | 
				
			||||||
 | 
					                  payload: CreateMilestoneProgressionSchema;
 | 
				
			||||||
    const handleProgressionCancel = () => {
 | 
					              }
 | 
				
			||||||
        setProgressionFormOpenIndex(null);
 | 
					            | {
 | 
				
			||||||
    };
 | 
					                  type: 'updateMilestoneProgression';
 | 
				
			||||||
 | 
					                  sourceMilestoneId: string;
 | 
				
			||||||
    const handleProgressionChangeRequestSubmit = (
 | 
					                  payload: UpdateMilestoneProgressionSchema;
 | 
				
			||||||
        payload: CreateMilestoneProgressionSchema,
 | 
					              },
 | 
				
			||||||
    ) => {
 | 
					    ) => {
 | 
				
			||||||
        setChangeRequestAction({
 | 
					        setChangeRequestAction(action);
 | 
				
			||||||
            type: 'createMilestoneProgression',
 | 
					 | 
				
			||||||
            payload,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleUpdateProgressionChangeRequestSubmit = (
 | 
					 | 
				
			||||||
        sourceMilestoneId: string,
 | 
					 | 
				
			||||||
        payload: UpdateMilestoneProgressionSchema,
 | 
					 | 
				
			||||||
    ) => {
 | 
					 | 
				
			||||||
        setChangeRequestAction({
 | 
					 | 
				
			||||||
            type: 'updateMilestoneProgression',
 | 
					 | 
				
			||||||
            sourceMilestoneId,
 | 
					 | 
				
			||||||
            payload,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleDeleteProgression = (milestone: IReleasePlanMilestone) => {
 | 
					    const handleDeleteProgression = (milestone: IReleasePlanMilestone) => {
 | 
				
			||||||
@ -392,80 +404,35 @@ export const ReleasePlan = ({
 | 
				
			|||||||
                )}
 | 
					                )}
 | 
				
			||||||
            </StyledHeader>
 | 
					            </StyledHeader>
 | 
				
			||||||
            <StyledBody>
 | 
					            <StyledBody>
 | 
				
			||||||
                {milestones.map((milestone, index) => {
 | 
					                {milestones.map((milestone, index) => (
 | 
				
			||||||
                    const isNotLastMilestone = index < milestones.length - 1;
 | 
					                    <ReleasePlanMilestoneItem
 | 
				
			||||||
                    const isProgressionFormOpen =
 | 
					                        key={milestone.id}
 | 
				
			||||||
                        progressionFormOpenIndex === index;
 | 
					 | 
				
			||||||
                    const nextMilestoneId = milestones[index + 1]?.id || '';
 | 
					 | 
				
			||||||
                    const handleOpenProgressionForm = () =>
 | 
					 | 
				
			||||||
                        setProgressionFormOpenIndex(index);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return (
 | 
					 | 
				
			||||||
                        <div key={milestone.id}>
 | 
					 | 
				
			||||||
                            <ReleasePlanMilestone
 | 
					 | 
				
			||||||
                                readonly={readonly}
 | 
					 | 
				
			||||||
                        milestone={milestone}
 | 
					                        milestone={milestone}
 | 
				
			||||||
                                status={
 | 
					                        index={index}
 | 
				
			||||||
                                    milestone.id === activeMilestoneId
 | 
					                        milestones={milestones}
 | 
				
			||||||
                                        ? environmentIsDisabled
 | 
					                        activeMilestoneId={activeMilestoneId}
 | 
				
			||||||
                                            ? 'paused'
 | 
					                        activeIndex={activeIndex}
 | 
				
			||||||
                                            : 'active'
 | 
					                        environmentIsDisabled={environmentIsDisabled}
 | 
				
			||||||
                                        : index < activeIndex
 | 
					                        readonly={readonly}
 | 
				
			||||||
                                          ? 'completed'
 | 
					                        milestoneProgressionsEnabled={
 | 
				
			||||||
                                          : 'not-started'
 | 
					                            milestoneProgressionsEnabled
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        progressionFormOpenIndex={progressionFormOpenIndex}
 | 
				
			||||||
 | 
					                        onSetProgressionFormOpenIndex={
 | 
				
			||||||
 | 
					                            setProgressionFormOpenIndex
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        onStartMilestone={onStartMilestone}
 | 
					                        onStartMilestone={onStartMilestone}
 | 
				
			||||||
                                showAutomation={
 | 
					                        onDeleteProgression={handleDeleteProgression}
 | 
				
			||||||
                                    milestoneProgressionsEnabled &&
 | 
					                        onAddToChangeRequest={handleAddToChangeRequest}
 | 
				
			||||||
                                    isNotLastMilestone &&
 | 
					                        getPendingProgressionChange={
 | 
				
			||||||
                                    !readonly
 | 
					                            getPendingProgressionChange
 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                onAddAutomation={handleOpenProgressionForm}
 | 
					 | 
				
			||||||
                                onDeleteAutomation={
 | 
					 | 
				
			||||||
                                    milestone.transitionCondition
 | 
					 | 
				
			||||||
                                        ? () =>
 | 
					 | 
				
			||||||
                                              handleDeleteProgression(milestone)
 | 
					 | 
				
			||||||
                                        : undefined
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                automationForm={
 | 
					 | 
				
			||||||
                                    isProgressionFormOpen ? (
 | 
					 | 
				
			||||||
                                        <MilestoneProgressionForm
 | 
					 | 
				
			||||||
                                            sourceMilestoneId={milestone.id}
 | 
					 | 
				
			||||||
                                            targetMilestoneId={nextMilestoneId}
 | 
					 | 
				
			||||||
                                            projectId={projectId}
 | 
					 | 
				
			||||||
                                            environment={environment}
 | 
					 | 
				
			||||||
                                            featureName={featureName}
 | 
					 | 
				
			||||||
                                            onSave={handleProgressionSave}
 | 
					 | 
				
			||||||
                                            onCancel={handleProgressionCancel}
 | 
					 | 
				
			||||||
                                            onChangeRequestSubmit={(payload) =>
 | 
					 | 
				
			||||||
                                                handleProgressionChangeRequestSubmit(
 | 
					 | 
				
			||||||
                                                    payload,
 | 
					 | 
				
			||||||
                                                )
 | 
					 | 
				
			||||||
                                            }
 | 
					 | 
				
			||||||
                                        />
 | 
					 | 
				
			||||||
                                    ) : undefined
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        projectId={projectId}
 | 
					                        projectId={projectId}
 | 
				
			||||||
                        environment={environment}
 | 
					                        environment={environment}
 | 
				
			||||||
                        featureName={featureName}
 | 
					                        featureName={featureName}
 | 
				
			||||||
                        onUpdate={refetch}
 | 
					                        onUpdate={refetch}
 | 
				
			||||||
                                onUpdateChangeRequestSubmit={
 | 
					 | 
				
			||||||
                                    handleUpdateProgressionChangeRequestSubmit
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                allMilestones={milestones}
 | 
					 | 
				
			||||||
                                activeMilestoneId={activeMilestoneId}
 | 
					 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                            <ConditionallyRender
 | 
					                ))}
 | 
				
			||||||
                                condition={isNotLastMilestone}
 | 
					 | 
				
			||||||
                                show={
 | 
					 | 
				
			||||||
                                    <StyledConnection
 | 
					 | 
				
			||||||
                                        isCompleted={index < activeIndex}
 | 
					 | 
				
			||||||
                                    />
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                            />
 | 
					 | 
				
			||||||
                        </div>
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                })}
 | 
					 | 
				
			||||||
            </StyledBody>
 | 
					            </StyledBody>
 | 
				
			||||||
            <ReleasePlanRemoveDialog
 | 
					            <ReleasePlanRemoveDialog
 | 
				
			||||||
                plan={plan}
 | 
					                plan={plan}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,5 @@
 | 
				
			|||||||
import Add from '@mui/icons-material/Add';
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
import { Button, styled } from '@mui/material';
 | 
					 | 
				
			||||||
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
 | 
					import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
 | 
				
			||||||
import { MilestoneTransitionDisplay } from './MilestoneTransitionDisplay.tsx';
 | 
					 | 
				
			||||||
import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledAutomationContainer = styled('div', {
 | 
					const StyledAutomationContainer = styled('div', {
 | 
				
			||||||
    shouldForwardProp: (prop) => prop !== 'status',
 | 
					    shouldForwardProp: (prop) => prop !== 'status',
 | 
				
			||||||
@ -24,97 +21,18 @@ const StyledAutomationContainer = styled('div', {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
 | 
					 | 
				
			||||||
    textTransform: 'none',
 | 
					 | 
				
			||||||
    fontWeight: theme.typography.fontWeightBold,
 | 
					 | 
				
			||||||
    fontSize: theme.typography.body2.fontSize,
 | 
					 | 
				
			||||||
    padding: 0,
 | 
					 | 
				
			||||||
    minWidth: 'auto',
 | 
					 | 
				
			||||||
    gap: theme.spacing(1),
 | 
					 | 
				
			||||||
    '&:hover': {
 | 
					 | 
				
			||||||
        backgroundColor: 'transparent',
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    '& .MuiButton-startIcon': {
 | 
					 | 
				
			||||||
        margin: 0,
 | 
					 | 
				
			||||||
        width: 20,
 | 
					 | 
				
			||||||
        height: 20,
 | 
					 | 
				
			||||||
        border: `1px solid ${theme.palette.primary.main}`,
 | 
					 | 
				
			||||||
        backgroundColor: theme.palette.background.elevation2,
 | 
					 | 
				
			||||||
        borderRadius: '50%',
 | 
					 | 
				
			||||||
        display: 'flex',
 | 
					 | 
				
			||||||
        alignItems: 'center',
 | 
					 | 
				
			||||||
        justifyContent: 'center',
 | 
					 | 
				
			||||||
        '& svg': {
 | 
					 | 
				
			||||||
            fontSize: 14,
 | 
					 | 
				
			||||||
            color: theme.palette.primary.main,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface IMilestoneAutomationSectionProps {
 | 
					interface IMilestoneAutomationSectionProps {
 | 
				
			||||||
    showAutomation?: boolean;
 | 
					 | 
				
			||||||
    status?: MilestoneStatus;
 | 
					    status?: MilestoneStatus;
 | 
				
			||||||
    onAddAutomation?: () => void;
 | 
					    children: React.ReactNode;
 | 
				
			||||||
    onDeleteAutomation?: () => void;
 | 
					 | 
				
			||||||
    automationForm?: React.ReactNode;
 | 
					 | 
				
			||||||
    transitionCondition?: {
 | 
					 | 
				
			||||||
        intervalMinutes: number;
 | 
					 | 
				
			||||||
    } | null;
 | 
					 | 
				
			||||||
    milestoneName: string;
 | 
					 | 
				
			||||||
    projectId: string;
 | 
					 | 
				
			||||||
    environment: string;
 | 
					 | 
				
			||||||
    featureName: string;
 | 
					 | 
				
			||||||
    sourceMilestoneId: string;
 | 
					 | 
				
			||||||
    onUpdate: () => void;
 | 
					 | 
				
			||||||
    onUpdateChangeRequestSubmit?: (
 | 
					 | 
				
			||||||
        sourceMilestoneId: string,
 | 
					 | 
				
			||||||
        payload: UpdateMilestoneProgressionSchema,
 | 
					 | 
				
			||||||
    ) => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MilestoneAutomationSection = ({
 | 
					export const MilestoneAutomationSection = ({
 | 
				
			||||||
    showAutomation,
 | 
					 | 
				
			||||||
    status,
 | 
					    status,
 | 
				
			||||||
    onAddAutomation,
 | 
					    children,
 | 
				
			||||||
    onDeleteAutomation,
 | 
					 | 
				
			||||||
    automationForm,
 | 
					 | 
				
			||||||
    transitionCondition,
 | 
					 | 
				
			||||||
    milestoneName,
 | 
					 | 
				
			||||||
    projectId,
 | 
					 | 
				
			||||||
    environment,
 | 
					 | 
				
			||||||
    featureName,
 | 
					 | 
				
			||||||
    sourceMilestoneId,
 | 
					 | 
				
			||||||
    onUpdate,
 | 
					 | 
				
			||||||
    onUpdateChangeRequestSubmit,
 | 
					 | 
				
			||||||
}: IMilestoneAutomationSectionProps) => {
 | 
					}: IMilestoneAutomationSectionProps) => {
 | 
				
			||||||
    if (!showAutomation) return null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <StyledAutomationContainer status={status}>
 | 
					        <StyledAutomationContainer status={status}>
 | 
				
			||||||
            {automationForm ? (
 | 
					            {children}
 | 
				
			||||||
                automationForm
 | 
					 | 
				
			||||||
            ) : transitionCondition ? (
 | 
					 | 
				
			||||||
                <MilestoneTransitionDisplay
 | 
					 | 
				
			||||||
                    intervalMinutes={transitionCondition.intervalMinutes}
 | 
					 | 
				
			||||||
                    onDelete={onDeleteAutomation!}
 | 
					 | 
				
			||||||
                    milestoneName={milestoneName}
 | 
					 | 
				
			||||||
                    status={status}
 | 
					 | 
				
			||||||
                    projectId={projectId}
 | 
					 | 
				
			||||||
                    environment={environment}
 | 
					 | 
				
			||||||
                    featureName={featureName}
 | 
					 | 
				
			||||||
                    sourceMilestoneId={sourceMilestoneId}
 | 
					 | 
				
			||||||
                    onUpdate={onUpdate}
 | 
					 | 
				
			||||||
                    onChangeRequestSubmit={onUpdateChangeRequestSubmit}
 | 
					 | 
				
			||||||
                />
 | 
					 | 
				
			||||||
            ) : (
 | 
					 | 
				
			||||||
                <StyledAddAutomationButton
 | 
					 | 
				
			||||||
                    onClick={onAddAutomation}
 | 
					 | 
				
			||||||
                    color='primary'
 | 
					 | 
				
			||||||
                    startIcon={<Add />}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                    Add automation
 | 
					 | 
				
			||||||
                </StyledAddAutomationButton>
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
        </StyledAutomationContainer>
 | 
					        </StyledAutomationContainer>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -2,17 +2,14 @@ import BoltIcon from '@mui/icons-material/Bolt';
 | 
				
			|||||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
 | 
					import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
 | 
				
			||||||
import { Button, IconButton, styled } from '@mui/material';
 | 
					import { Button, IconButton, styled } from '@mui/material';
 | 
				
			||||||
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
 | 
					import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
 | 
				
			||||||
import { useState } from 'react';
 | 
					 | 
				
			||||||
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
 | 
					 | 
				
			||||||
import useToast from 'hooks/useToast';
 | 
					 | 
				
			||||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
					 | 
				
			||||||
import { MilestoneProgressionTimeInput } from '../MilestoneProgressionForm/MilestoneProgressionTimeInput.tsx';
 | 
					import { MilestoneProgressionTimeInput } from '../MilestoneProgressionForm/MilestoneProgressionTimeInput.tsx';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
    useMilestoneProgressionForm,
 | 
					    useMilestoneProgressionForm,
 | 
				
			||||||
    getTimeValueAndUnitFromMinutes,
 | 
					    getTimeValueAndUnitFromMinutes,
 | 
				
			||||||
} from '../hooks/useMilestoneProgressionForm.js';
 | 
					} from '../hooks/useMilestoneProgressionForm.js';
 | 
				
			||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
					 | 
				
			||||||
import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
					import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
				
			||||||
 | 
					import type { ReactNode } from 'react';
 | 
				
			||||||
 | 
					import { useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledDisplayContainer = styled('div')(({ theme }) => ({
 | 
					const StyledDisplayContainer = styled('div')(({ theme }) => ({
 | 
				
			||||||
    display: 'flex',
 | 
					    display: 'flex',
 | 
				
			||||||
@ -61,52 +58,44 @@ const StyledButtonGroup = styled('div')(({ theme }) => ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface IMilestoneTransitionDisplayProps {
 | 
					interface IMilestoneTransitionDisplayProps {
 | 
				
			||||||
    intervalMinutes: number;
 | 
					    intervalMinutes: number;
 | 
				
			||||||
 | 
					    onSave: (
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => Promise<{ shouldReset?: boolean }>;
 | 
				
			||||||
    onDelete: () => void;
 | 
					    onDelete: () => void;
 | 
				
			||||||
    milestoneName: string;
 | 
					    milestoneName: string;
 | 
				
			||||||
    status?: MilestoneStatus;
 | 
					    status?: MilestoneStatus;
 | 
				
			||||||
    projectId: string;
 | 
					    badge?: ReactNode;
 | 
				
			||||||
    environment: string;
 | 
					 | 
				
			||||||
    featureName: string;
 | 
					 | 
				
			||||||
    sourceMilestoneId: string;
 | 
					 | 
				
			||||||
    onUpdate: () => void;
 | 
					 | 
				
			||||||
    onChangeRequestSubmit?: (
 | 
					 | 
				
			||||||
        sourceMilestoneId: string,
 | 
					 | 
				
			||||||
        payload: UpdateMilestoneProgressionSchema,
 | 
					 | 
				
			||||||
    ) => void;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MilestoneTransitionDisplay = ({
 | 
					export const MilestoneTransitionDisplay = ({
 | 
				
			||||||
    intervalMinutes,
 | 
					    intervalMinutes,
 | 
				
			||||||
 | 
					    onSave,
 | 
				
			||||||
    onDelete,
 | 
					    onDelete,
 | 
				
			||||||
    milestoneName,
 | 
					    milestoneName,
 | 
				
			||||||
    status,
 | 
					    status,
 | 
				
			||||||
    projectId,
 | 
					    badge,
 | 
				
			||||||
    environment,
 | 
					 | 
				
			||||||
    featureName,
 | 
					 | 
				
			||||||
    sourceMilestoneId,
 | 
					 | 
				
			||||||
    onUpdate,
 | 
					 | 
				
			||||||
    onChangeRequestSubmit,
 | 
					 | 
				
			||||||
}: IMilestoneTransitionDisplayProps) => {
 | 
					}: IMilestoneTransitionDisplayProps) => {
 | 
				
			||||||
    const { updateMilestoneProgression } = useMilestoneProgressionsApi();
 | 
					 | 
				
			||||||
    const { setToastData, setToastApiError } = useToast();
 | 
					 | 
				
			||||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const initial = getTimeValueAndUnitFromMinutes(intervalMinutes);
 | 
					    const initial = getTimeValueAndUnitFromMinutes(intervalMinutes);
 | 
				
			||||||
    const form = useMilestoneProgressionForm(
 | 
					    const form = useMilestoneProgressionForm(
 | 
				
			||||||
        sourceMilestoneId,
 | 
					        '', // sourceMilestoneId not needed for display
 | 
				
			||||||
        sourceMilestoneId, // We don't need targetMilestone for edit, just reuse source
 | 
					        '', // targetMilestoneId not needed for display
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            timeValue: initial.value,
 | 
					            timeValue: initial.value,
 | 
				
			||||||
            timeUnit: initial.unit,
 | 
					            timeUnit: initial.unit,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const [isSubmitting, setIsSubmitting] = useState(false);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const currentIntervalMinutes = form.getIntervalMinutes();
 | 
					    const currentIntervalMinutes = form.getIntervalMinutes();
 | 
				
			||||||
    const hasChanged = currentIntervalMinutes !== intervalMinutes;
 | 
					    const hasChanged = currentIntervalMinutes !== intervalMinutes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        const newInitial = getTimeValueAndUnitFromMinutes(intervalMinutes);
 | 
				
			||||||
 | 
					        form.setTimeValue(newInitial.value);
 | 
				
			||||||
 | 
					        form.setTimeUnit(newInitial.unit);
 | 
				
			||||||
 | 
					    }, [intervalMinutes]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleSave = async () => {
 | 
					    const handleSave = async () => {
 | 
				
			||||||
        if (isSubmitting || !hasChanged) return;
 | 
					        if (!hasChanged) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const payload: UpdateMilestoneProgressionSchema = {
 | 
					        const payload: UpdateMilestoneProgressionSchema = {
 | 
				
			||||||
            transitionCondition: {
 | 
					            transitionCondition: {
 | 
				
			||||||
@ -114,29 +103,10 @@ export const MilestoneTransitionDisplay = ({
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (isChangeRequestConfigured(environment) && onChangeRequestSubmit) {
 | 
					        const result = await onSave(payload);
 | 
				
			||||||
            onChangeRequestSubmit(sourceMilestoneId, payload);
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        setIsSubmitting(true);
 | 
					        if (result?.shouldReset) {
 | 
				
			||||||
        try {
 | 
					            handleReset();
 | 
				
			||||||
            await updateMilestoneProgression(
 | 
					 | 
				
			||||||
                projectId,
 | 
					 | 
				
			||||||
                environment,
 | 
					 | 
				
			||||||
                featureName,
 | 
					 | 
				
			||||||
                sourceMilestoneId,
 | 
					 | 
				
			||||||
                payload,
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            setToastData({
 | 
					 | 
				
			||||||
                type: 'success',
 | 
					 | 
				
			||||||
                text: 'Automation updated successfully',
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            onUpdate();
 | 
					 | 
				
			||||||
        } catch (error: unknown) {
 | 
					 | 
				
			||||||
            setToastApiError(formatUnknownError(error));
 | 
					 | 
				
			||||||
        } finally {
 | 
					 | 
				
			||||||
            setIsSubmitting(false);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -168,7 +138,6 @@ export const MilestoneTransitionDisplay = ({
 | 
				
			|||||||
                    timeUnit={form.timeUnit}
 | 
					                    timeUnit={form.timeUnit}
 | 
				
			||||||
                    onTimeValueChange={form.handleTimeValueChange}
 | 
					                    onTimeValueChange={form.handleTimeValueChange}
 | 
				
			||||||
                    onTimeUnitChange={form.handleTimeUnitChange}
 | 
					                    onTimeUnitChange={form.handleTimeUnitChange}
 | 
				
			||||||
                    disabled={isSubmitting}
 | 
					 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
            </StyledContentGroup>
 | 
					            </StyledContentGroup>
 | 
				
			||||||
            <StyledButtonGroup>
 | 
					            <StyledButtonGroup>
 | 
				
			||||||
@ -178,17 +147,16 @@ export const MilestoneTransitionDisplay = ({
 | 
				
			|||||||
                        color='primary'
 | 
					                        color='primary'
 | 
				
			||||||
                        onClick={handleSave}
 | 
					                        onClick={handleSave}
 | 
				
			||||||
                        size='small'
 | 
					                        size='small'
 | 
				
			||||||
                        disabled={isSubmitting}
 | 
					 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                        {isSubmitting ? 'Saving...' : 'Save'}
 | 
					                        Save
 | 
				
			||||||
                    </Button>
 | 
					                    </Button>
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
 | 
					                {badge}
 | 
				
			||||||
                <IconButton
 | 
					                <IconButton
 | 
				
			||||||
                    onClick={onDelete}
 | 
					                    onClick={onDelete}
 | 
				
			||||||
                    size='small'
 | 
					                    size='small'
 | 
				
			||||||
                    aria-label={`Delete automation for ${milestoneName}`}
 | 
					                    aria-label={`Delete automation for ${milestoneName}`}
 | 
				
			||||||
                    sx={{ padding: 0.5 }}
 | 
					                    sx={{ padding: 0.5 }}
 | 
				
			||||||
                    disabled={isSubmitting}
 | 
					 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                    <DeleteOutlineIcon fontSize='small' />
 | 
					                    <DeleteOutlineIcon fontSize='small' />
 | 
				
			||||||
                </IconButton>
 | 
					                </IconButton>
 | 
				
			||||||
 | 
				
			|||||||
@ -17,9 +17,7 @@ import { StrategySeparator } from 'component/common/StrategySeparator/StrategySe
 | 
				
			|||||||
import { StrategyItem } from '../../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx';
 | 
					import { StrategyItem } from '../../FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyItem.tsx';
 | 
				
			||||||
import { StrategyList } from 'component/common/StrategyList/StrategyList';
 | 
					import { StrategyList } from 'component/common/StrategyList/StrategyList';
 | 
				
			||||||
import { StrategyListItem } from 'component/common/StrategyList/StrategyListItem';
 | 
					import { StrategyListItem } from 'component/common/StrategyList/StrategyListItem';
 | 
				
			||||||
import { MilestoneAutomationSection } from './MilestoneAutomationSection.tsx';
 | 
					 | 
				
			||||||
import { formatDateYMDHMS } from 'utils/formatDate';
 | 
					import { formatDateYMDHMS } from 'utils/formatDate';
 | 
				
			||||||
import type { UpdateMilestoneProgressionSchema } from 'openapi';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledAccordion = styled(Accordion, {
 | 
					const StyledAccordion = styled(Accordion, {
 | 
				
			||||||
    shouldForwardProp: (prop) => prop !== 'status' && prop !== 'hasAutomation',
 | 
					    shouldForwardProp: (prop) => prop !== 'status' && prop !== 'hasAutomation',
 | 
				
			||||||
@ -100,18 +98,7 @@ interface IReleasePlanMilestoneProps {
 | 
				
			|||||||
    status?: MilestoneStatus;
 | 
					    status?: MilestoneStatus;
 | 
				
			||||||
    onStartMilestone?: (milestone: IReleasePlanMilestone) => void;
 | 
					    onStartMilestone?: (milestone: IReleasePlanMilestone) => void;
 | 
				
			||||||
    readonly?: boolean;
 | 
					    readonly?: boolean;
 | 
				
			||||||
    showAutomation?: boolean;
 | 
					    automationSection?: React.ReactNode;
 | 
				
			||||||
    onAddAutomation?: () => void;
 | 
					 | 
				
			||||||
    onDeleteAutomation?: () => void;
 | 
					 | 
				
			||||||
    automationForm?: React.ReactNode;
 | 
					 | 
				
			||||||
    projectId?: string;
 | 
					 | 
				
			||||||
    environment?: string;
 | 
					 | 
				
			||||||
    featureName?: string;
 | 
					 | 
				
			||||||
    onUpdate?: () => void;
 | 
					 | 
				
			||||||
    onUpdateChangeRequestSubmit?: (
 | 
					 | 
				
			||||||
        sourceMilestoneId: string,
 | 
					 | 
				
			||||||
        payload: UpdateMilestoneProgressionSchema,
 | 
					 | 
				
			||||||
    ) => void;
 | 
					 | 
				
			||||||
    allMilestones: IReleasePlanMilestone[];
 | 
					    allMilestones: IReleasePlanMilestone[];
 | 
				
			||||||
    activeMilestoneId?: string;
 | 
					    activeMilestoneId?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -121,24 +108,17 @@ export const ReleasePlanMilestone = ({
 | 
				
			|||||||
    status = 'not-started',
 | 
					    status = 'not-started',
 | 
				
			||||||
    onStartMilestone,
 | 
					    onStartMilestone,
 | 
				
			||||||
    readonly,
 | 
					    readonly,
 | 
				
			||||||
    showAutomation,
 | 
					    automationSection,
 | 
				
			||||||
    onAddAutomation,
 | 
					 | 
				
			||||||
    onDeleteAutomation,
 | 
					 | 
				
			||||||
    automationForm,
 | 
					 | 
				
			||||||
    projectId,
 | 
					 | 
				
			||||||
    environment,
 | 
					 | 
				
			||||||
    featureName,
 | 
					 | 
				
			||||||
    onUpdate,
 | 
					 | 
				
			||||||
    onUpdateChangeRequestSubmit,
 | 
					 | 
				
			||||||
    allMilestones,
 | 
					    allMilestones,
 | 
				
			||||||
    activeMilestoneId,
 | 
					    activeMilestoneId,
 | 
				
			||||||
}: IReleasePlanMilestoneProps) => {
 | 
					}: IReleasePlanMilestoneProps) => {
 | 
				
			||||||
    const [expanded, setExpanded] = useState(false);
 | 
					    const [expanded, setExpanded] = useState(false);
 | 
				
			||||||
 | 
					    const hasAutomation = Boolean(automationSection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!milestone.strategies.length) {
 | 
					    if (!milestone.strategies.length) {
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            <StyledMilestoneContainer>
 | 
					            <StyledMilestoneContainer>
 | 
				
			||||||
                <StyledAccordion status={status} hasAutomation={showAutomation}>
 | 
					                <StyledAccordion status={status} hasAutomation={hasAutomation}>
 | 
				
			||||||
                    <StyledAccordionSummary>
 | 
					                    <StyledAccordionSummary>
 | 
				
			||||||
                        <StyledTitleContainer>
 | 
					                        <StyledTitleContainer>
 | 
				
			||||||
                            <StyledTitle status={status}>
 | 
					                            <StyledTitle status={status}>
 | 
				
			||||||
@ -181,29 +161,7 @@ export const ReleasePlanMilestone = ({
 | 
				
			|||||||
                        </StyledSecondaryLabel>
 | 
					                        </StyledSecondaryLabel>
 | 
				
			||||||
                    </StyledAccordionSummary>
 | 
					                    </StyledAccordionSummary>
 | 
				
			||||||
                </StyledAccordion>
 | 
					                </StyledAccordion>
 | 
				
			||||||
                {showAutomation &&
 | 
					                {automationSection}
 | 
				
			||||||
                    projectId &&
 | 
					 | 
				
			||||||
                    environment &&
 | 
					 | 
				
			||||||
                    featureName &&
 | 
					 | 
				
			||||||
                    onUpdate && (
 | 
					 | 
				
			||||||
                        <MilestoneAutomationSection
 | 
					 | 
				
			||||||
                            showAutomation={showAutomation}
 | 
					 | 
				
			||||||
                            status={status}
 | 
					 | 
				
			||||||
                            onAddAutomation={onAddAutomation}
 | 
					 | 
				
			||||||
                            onDeleteAutomation={onDeleteAutomation}
 | 
					 | 
				
			||||||
                            automationForm={automationForm}
 | 
					 | 
				
			||||||
                            transitionCondition={milestone.transitionCondition}
 | 
					 | 
				
			||||||
                            milestoneName={milestone.name}
 | 
					 | 
				
			||||||
                            projectId={projectId}
 | 
					 | 
				
			||||||
                            environment={environment}
 | 
					 | 
				
			||||||
                            featureName={featureName}
 | 
					 | 
				
			||||||
                            sourceMilestoneId={milestone.id}
 | 
					 | 
				
			||||||
                            onUpdate={onUpdate}
 | 
					 | 
				
			||||||
                            onUpdateChangeRequestSubmit={
 | 
					 | 
				
			||||||
                                onUpdateChangeRequestSubmit
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        />
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
            </StyledMilestoneContainer>
 | 
					            </StyledMilestoneContainer>
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -212,7 +170,7 @@ export const ReleasePlanMilestone = ({
 | 
				
			|||||||
        <StyledMilestoneContainer>
 | 
					        <StyledMilestoneContainer>
 | 
				
			||||||
            <StyledAccordion
 | 
					            <StyledAccordion
 | 
				
			||||||
                status={status}
 | 
					                status={status}
 | 
				
			||||||
                hasAutomation={showAutomation}
 | 
					                hasAutomation={hasAutomation}
 | 
				
			||||||
                onChange={(evt, expanded) => setExpanded(expanded)}
 | 
					                onChange={(evt, expanded) => setExpanded(expanded)}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                <StyledAccordionSummary expandIcon={<ExpandMore />}>
 | 
					                <StyledAccordionSummary expandIcon={<ExpandMore />}>
 | 
				
			||||||
@ -274,29 +232,7 @@ export const ReleasePlanMilestone = ({
 | 
				
			|||||||
                    </StrategyList>
 | 
					                    </StrategyList>
 | 
				
			||||||
                </StyledAccordionDetails>
 | 
					                </StyledAccordionDetails>
 | 
				
			||||||
            </StyledAccordion>
 | 
					            </StyledAccordion>
 | 
				
			||||||
            {showAutomation &&
 | 
					            {automationSection}
 | 
				
			||||||
                projectId &&
 | 
					 | 
				
			||||||
                environment &&
 | 
					 | 
				
			||||||
                featureName &&
 | 
					 | 
				
			||||||
                onUpdate && (
 | 
					 | 
				
			||||||
                    <MilestoneAutomationSection
 | 
					 | 
				
			||||||
                        showAutomation={showAutomation}
 | 
					 | 
				
			||||||
                        status={status}
 | 
					 | 
				
			||||||
                        onAddAutomation={onAddAutomation}
 | 
					 | 
				
			||||||
                        onDeleteAutomation={onDeleteAutomation}
 | 
					 | 
				
			||||||
                        automationForm={automationForm}
 | 
					 | 
				
			||||||
                        transitionCondition={milestone.transitionCondition}
 | 
					 | 
				
			||||||
                        milestoneName={milestone.name}
 | 
					 | 
				
			||||||
                        projectId={projectId}
 | 
					 | 
				
			||||||
                        environment={environment}
 | 
					 | 
				
			||||||
                        featureName={featureName}
 | 
					 | 
				
			||||||
                        sourceMilestoneId={milestone.id}
 | 
					 | 
				
			||||||
                        onUpdate={onUpdate}
 | 
					 | 
				
			||||||
                        onUpdateChangeRequestSubmit={
 | 
					 | 
				
			||||||
                            onUpdateChangeRequestSubmit
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
        </StyledMilestoneContainer>
 | 
					        </StyledMilestoneContainer>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,141 @@
 | 
				
			|||||||
 | 
					import Add from '@mui/icons-material/Add';
 | 
				
			||||||
 | 
					import { Button, styled } from '@mui/material';
 | 
				
			||||||
 | 
					import { Badge } from 'component/common/Badge/Badge';
 | 
				
			||||||
 | 
					import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    CreateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					} from 'openapi';
 | 
				
			||||||
 | 
					import { MilestoneAutomationSection } from '../ReleasePlanMilestone/MilestoneAutomationSection.tsx';
 | 
				
			||||||
 | 
					import { MilestoneTransitionDisplay } from '../ReleasePlanMilestone/MilestoneTransitionDisplay.tsx';
 | 
				
			||||||
 | 
					import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
 | 
				
			||||||
 | 
					import { MilestoneProgressionForm } from '../MilestoneProgressionForm/MilestoneProgressionForm.tsx';
 | 
				
			||||||
 | 
					import type { PendingProgressionChange } from './ReleasePlanMilestoneItem.tsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
 | 
				
			||||||
 | 
					    textTransform: 'none',
 | 
				
			||||||
 | 
					    fontWeight: theme.typography.fontWeightBold,
 | 
				
			||||||
 | 
					    fontSize: theme.typography.body2.fontSize,
 | 
				
			||||||
 | 
					    padding: 0,
 | 
				
			||||||
 | 
					    minWidth: 'auto',
 | 
				
			||||||
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
 | 
					    '&:hover': {
 | 
				
			||||||
 | 
					        backgroundColor: 'transparent',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    '& .MuiButton-startIcon': {
 | 
				
			||||||
 | 
					        margin: 0,
 | 
				
			||||||
 | 
					        width: 20,
 | 
				
			||||||
 | 
					        height: 20,
 | 
				
			||||||
 | 
					        border: `1px solid ${theme.palette.primary.main}`,
 | 
				
			||||||
 | 
					        backgroundColor: theme.palette.background.elevation2,
 | 
				
			||||||
 | 
					        borderRadius: '50%',
 | 
				
			||||||
 | 
					        display: 'flex',
 | 
				
			||||||
 | 
					        alignItems: 'center',
 | 
				
			||||||
 | 
					        justifyContent: 'center',
 | 
				
			||||||
 | 
					        '& svg': {
 | 
				
			||||||
 | 
					            fontSize: 14,
 | 
				
			||||||
 | 
					            color: theme.palette.primary.main,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const StyledAddAutomationContainer = styled('div')(({ theme }) => ({
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    alignItems: 'center',
 | 
				
			||||||
 | 
					    gap: theme.spacing(1),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MilestoneAutomationProps {
 | 
				
			||||||
 | 
					    milestone: IReleasePlanMilestone;
 | 
				
			||||||
 | 
					    status: MilestoneStatus;
 | 
				
			||||||
 | 
					    isNotLastMilestone: boolean;
 | 
				
			||||||
 | 
					    nextMilestoneId: string;
 | 
				
			||||||
 | 
					    milestoneProgressionsEnabled: boolean;
 | 
				
			||||||
 | 
					    readonly: boolean | undefined;
 | 
				
			||||||
 | 
					    isProgressionFormOpen: boolean;
 | 
				
			||||||
 | 
					    effectiveTransitionCondition: IReleasePlanMilestone['transitionCondition'];
 | 
				
			||||||
 | 
					    pendingProgressionChange: PendingProgressionChange | null;
 | 
				
			||||||
 | 
					    onOpenProgressionForm: () => void;
 | 
				
			||||||
 | 
					    onCloseProgressionForm: () => void;
 | 
				
			||||||
 | 
					    onCreateProgression: (
 | 
				
			||||||
 | 
					        payload: CreateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => Promise<void>;
 | 
				
			||||||
 | 
					    onUpdateProgression: (
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => Promise<{ shouldReset?: boolean }>;
 | 
				
			||||||
 | 
					    onDeleteProgression: (milestone: IReleasePlanMilestone) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MilestoneAutomation = ({
 | 
				
			||||||
 | 
					    milestone,
 | 
				
			||||||
 | 
					    status,
 | 
				
			||||||
 | 
					    isNotLastMilestone,
 | 
				
			||||||
 | 
					    nextMilestoneId,
 | 
				
			||||||
 | 
					    milestoneProgressionsEnabled,
 | 
				
			||||||
 | 
					    readonly,
 | 
				
			||||||
 | 
					    isProgressionFormOpen,
 | 
				
			||||||
 | 
					    effectiveTransitionCondition,
 | 
				
			||||||
 | 
					    pendingProgressionChange,
 | 
				
			||||||
 | 
					    onOpenProgressionForm,
 | 
				
			||||||
 | 
					    onCloseProgressionForm,
 | 
				
			||||||
 | 
					    onCreateProgression,
 | 
				
			||||||
 | 
					    onUpdateProgression,
 | 
				
			||||||
 | 
					    onDeleteProgression,
 | 
				
			||||||
 | 
					}: MilestoneAutomationProps) => {
 | 
				
			||||||
 | 
					    const showAutomation =
 | 
				
			||||||
 | 
					        milestoneProgressionsEnabled && isNotLastMilestone && !readonly;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!showAutomation) {
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hasPendingCreate =
 | 
				
			||||||
 | 
					        pendingProgressionChange?.action === 'createMilestoneProgression';
 | 
				
			||||||
 | 
					    const hasPendingUpdate =
 | 
				
			||||||
 | 
					        pendingProgressionChange?.action === 'updateMilestoneProgression';
 | 
				
			||||||
 | 
					    const hasPendingDelete =
 | 
				
			||||||
 | 
					        pendingProgressionChange?.action === 'deleteMilestoneProgression';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const badge = hasPendingDelete ? (
 | 
				
			||||||
 | 
					        <Badge color='error'>Deleted in draft</Badge>
 | 
				
			||||||
 | 
					    ) : hasPendingUpdate ? (
 | 
				
			||||||
 | 
					        <Badge color='warning'>Modified in draft</Badge>
 | 
				
			||||||
 | 
					    ) : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <MilestoneAutomationSection status={status}>
 | 
				
			||||||
 | 
					            {isProgressionFormOpen ? (
 | 
				
			||||||
 | 
					                <MilestoneProgressionForm
 | 
				
			||||||
 | 
					                    sourceMilestoneId={milestone.id}
 | 
				
			||||||
 | 
					                    targetMilestoneId={nextMilestoneId}
 | 
				
			||||||
 | 
					                    onSubmit={onCreateProgression}
 | 
				
			||||||
 | 
					                    onCancel={onCloseProgressionForm}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            ) : effectiveTransitionCondition ? (
 | 
				
			||||||
 | 
					                <MilestoneTransitionDisplay
 | 
				
			||||||
 | 
					                    intervalMinutes={
 | 
				
			||||||
 | 
					                        effectiveTransitionCondition.intervalMinutes
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    onSave={onUpdateProgression}
 | 
				
			||||||
 | 
					                    onDelete={() => onDeleteProgression(milestone)}
 | 
				
			||||||
 | 
					                    milestoneName={milestone.name}
 | 
				
			||||||
 | 
					                    status={status}
 | 
				
			||||||
 | 
					                    badge={badge}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            ) : (
 | 
				
			||||||
 | 
					                <StyledAddAutomationContainer>
 | 
				
			||||||
 | 
					                    <StyledAddAutomationButton
 | 
				
			||||||
 | 
					                        onClick={onOpenProgressionForm}
 | 
				
			||||||
 | 
					                        color='primary'
 | 
				
			||||||
 | 
					                        startIcon={<Add />}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        Add automation
 | 
				
			||||||
 | 
					                    </StyledAddAutomationButton>
 | 
				
			||||||
 | 
					                    {hasPendingCreate && (
 | 
				
			||||||
 | 
					                        <Badge color='warning'>Modified in draft</Badge>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                </StyledAddAutomationContainer>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					        </MilestoneAutomationSection>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,213 @@
 | 
				
			|||||||
 | 
					import { styled } from '@mui/material';
 | 
				
			||||||
 | 
					import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
 | 
				
			||||||
 | 
					import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    CreateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					} from 'openapi';
 | 
				
			||||||
 | 
					import { ReleasePlanMilestone } from '../ReleasePlanMilestone/ReleasePlanMilestone.tsx';
 | 
				
			||||||
 | 
					import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
 | 
				
			||||||
 | 
					import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
				
			||||||
 | 
					import useToast from 'hooks/useToast';
 | 
				
			||||||
 | 
					import { formatUnknownError } from 'utils/formatUnknownError';
 | 
				
			||||||
 | 
					import { calculateMilestoneStatus } from './milestoneStatusUtils.js';
 | 
				
			||||||
 | 
					import { usePendingProgressionChanges } from './usePendingProgressionChanges.js';
 | 
				
			||||||
 | 
					import { MilestoneAutomation } from './MilestoneAutomation.tsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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),
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PendingProgressionChange {
 | 
				
			||||||
 | 
					    action: string;
 | 
				
			||||||
 | 
					    payload: any;
 | 
				
			||||||
 | 
					    changeRequestId: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IReleasePlanMilestoneItemProps {
 | 
				
			||||||
 | 
					    milestone: IReleasePlanMilestone;
 | 
				
			||||||
 | 
					    index: number;
 | 
				
			||||||
 | 
					    milestones: IReleasePlanMilestone[];
 | 
				
			||||||
 | 
					    activeMilestoneId?: string;
 | 
				
			||||||
 | 
					    activeIndex: number;
 | 
				
			||||||
 | 
					    environmentIsDisabled?: boolean;
 | 
				
			||||||
 | 
					    readonly?: boolean;
 | 
				
			||||||
 | 
					    milestoneProgressionsEnabled: boolean;
 | 
				
			||||||
 | 
					    progressionFormOpenIndex: number | null;
 | 
				
			||||||
 | 
					    onSetProgressionFormOpenIndex: (index: number | null) => void;
 | 
				
			||||||
 | 
					    onStartMilestone?: (milestone: IReleasePlanMilestone) => void;
 | 
				
			||||||
 | 
					    onDeleteProgression: (milestone: IReleasePlanMilestone) => void;
 | 
				
			||||||
 | 
					    onAddToChangeRequest: (
 | 
				
			||||||
 | 
					        action:
 | 
				
			||||||
 | 
					            | {
 | 
				
			||||||
 | 
					                  type: 'createMilestoneProgression';
 | 
				
			||||||
 | 
					                  payload: CreateMilestoneProgressionSchema;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            | {
 | 
				
			||||||
 | 
					                  type: 'updateMilestoneProgression';
 | 
				
			||||||
 | 
					                  sourceMilestoneId: string;
 | 
				
			||||||
 | 
					                  payload: UpdateMilestoneProgressionSchema;
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					    ) => void;
 | 
				
			||||||
 | 
					    getPendingProgressionChange: (
 | 
				
			||||||
 | 
					        sourceMilestoneId: string,
 | 
				
			||||||
 | 
					    ) => PendingProgressionChange | null;
 | 
				
			||||||
 | 
					    projectId: string;
 | 
				
			||||||
 | 
					    environment: string;
 | 
				
			||||||
 | 
					    featureName: string;
 | 
				
			||||||
 | 
					    onUpdate: () => void | Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const ReleasePlanMilestoneItem = ({
 | 
				
			||||||
 | 
					    milestone,
 | 
				
			||||||
 | 
					    index,
 | 
				
			||||||
 | 
					    milestones,
 | 
				
			||||||
 | 
					    activeMilestoneId,
 | 
				
			||||||
 | 
					    activeIndex,
 | 
				
			||||||
 | 
					    environmentIsDisabled,
 | 
				
			||||||
 | 
					    readonly,
 | 
				
			||||||
 | 
					    milestoneProgressionsEnabled,
 | 
				
			||||||
 | 
					    progressionFormOpenIndex,
 | 
				
			||||||
 | 
					    onSetProgressionFormOpenIndex,
 | 
				
			||||||
 | 
					    onStartMilestone,
 | 
				
			||||||
 | 
					    onDeleteProgression,
 | 
				
			||||||
 | 
					    onAddToChangeRequest,
 | 
				
			||||||
 | 
					    getPendingProgressionChange,
 | 
				
			||||||
 | 
					    projectId,
 | 
				
			||||||
 | 
					    environment,
 | 
				
			||||||
 | 
					    featureName,
 | 
				
			||||||
 | 
					    onUpdate,
 | 
				
			||||||
 | 
					}: IReleasePlanMilestoneItemProps) => {
 | 
				
			||||||
 | 
					    const { createMilestoneProgression, updateMilestoneProgression } =
 | 
				
			||||||
 | 
					        useMilestoneProgressionsApi();
 | 
				
			||||||
 | 
					    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
				
			||||||
 | 
					    const { setToastData, setToastApiError } = useToast();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const isNotLastMilestone = index < milestones.length - 1;
 | 
				
			||||||
 | 
					    const isProgressionFormOpen = progressionFormOpenIndex === index;
 | 
				
			||||||
 | 
					    const nextMilestoneId = milestones[index + 1]?.id || '';
 | 
				
			||||||
 | 
					    const handleOpenProgressionForm = () =>
 | 
				
			||||||
 | 
					        onSetProgressionFormOpenIndex(index);
 | 
				
			||||||
 | 
					    const handleCloseProgressionForm = () =>
 | 
				
			||||||
 | 
					        onSetProgressionFormOpenIndex(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleCreateProgression = async (
 | 
				
			||||||
 | 
					        payload: CreateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        if (isChangeRequestConfigured(environment)) {
 | 
				
			||||||
 | 
					            onAddToChangeRequest({
 | 
				
			||||||
 | 
					                type: 'createMilestoneProgression',
 | 
				
			||||||
 | 
					                payload,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            handleCloseProgressionForm();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await createMilestoneProgression(
 | 
				
			||||||
 | 
					                projectId,
 | 
				
			||||||
 | 
					                environment,
 | 
				
			||||||
 | 
					                featureName,
 | 
				
			||||||
 | 
					                payload,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            setToastData({
 | 
				
			||||||
 | 
					                type: 'success',
 | 
				
			||||||
 | 
					                text: 'Automation configured successfully',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            handleCloseProgressionForm();
 | 
				
			||||||
 | 
					            await onUpdate();
 | 
				
			||||||
 | 
					        } catch (error: unknown) {
 | 
				
			||||||
 | 
					            setToastApiError(formatUnknownError(error));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleUpdateProgression = async (
 | 
				
			||||||
 | 
					        payload: UpdateMilestoneProgressionSchema,
 | 
				
			||||||
 | 
					    ): Promise<{ shouldReset?: boolean }> => {
 | 
				
			||||||
 | 
					        if (isChangeRequestConfigured(environment)) {
 | 
				
			||||||
 | 
					            onAddToChangeRequest({
 | 
				
			||||||
 | 
					                type: 'updateMilestoneProgression',
 | 
				
			||||||
 | 
					                sourceMilestoneId: milestone.id,
 | 
				
			||||||
 | 
					                payload,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return { shouldReset: true };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await updateMilestoneProgression(
 | 
				
			||||||
 | 
					                projectId,
 | 
				
			||||||
 | 
					                environment,
 | 
				
			||||||
 | 
					                featureName,
 | 
				
			||||||
 | 
					                milestone.id,
 | 
				
			||||||
 | 
					                payload,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            setToastData({
 | 
				
			||||||
 | 
					                type: 'success',
 | 
				
			||||||
 | 
					                text: 'Automation updated successfully',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            await onUpdate();
 | 
				
			||||||
 | 
					            return {};
 | 
				
			||||||
 | 
					        } catch (error: unknown) {
 | 
				
			||||||
 | 
					            setToastApiError(formatUnknownError(error));
 | 
				
			||||||
 | 
					            return {};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const status = calculateMilestoneStatus(
 | 
				
			||||||
 | 
					        milestone,
 | 
				
			||||||
 | 
					        activeMilestoneId,
 | 
				
			||||||
 | 
					        index,
 | 
				
			||||||
 | 
					        activeIndex,
 | 
				
			||||||
 | 
					        environmentIsDisabled,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { pendingProgressionChange, effectiveTransitionCondition } =
 | 
				
			||||||
 | 
					        usePendingProgressionChanges(milestone, getPendingProgressionChange);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const shouldShowAutomation =
 | 
				
			||||||
 | 
					        isNotLastMilestone && milestoneProgressionsEnabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const automationSection = shouldShowAutomation ? (
 | 
				
			||||||
 | 
					        <MilestoneAutomation
 | 
				
			||||||
 | 
					            milestone={milestone}
 | 
				
			||||||
 | 
					            status={status}
 | 
				
			||||||
 | 
					            isNotLastMilestone={isNotLastMilestone}
 | 
				
			||||||
 | 
					            nextMilestoneId={nextMilestoneId}
 | 
				
			||||||
 | 
					            milestoneProgressionsEnabled={milestoneProgressionsEnabled}
 | 
				
			||||||
 | 
					            readonly={readonly}
 | 
				
			||||||
 | 
					            isProgressionFormOpen={isProgressionFormOpen}
 | 
				
			||||||
 | 
					            effectiveTransitionCondition={effectiveTransitionCondition}
 | 
				
			||||||
 | 
					            pendingProgressionChange={pendingProgressionChange}
 | 
				
			||||||
 | 
					            onOpenProgressionForm={handleOpenProgressionForm}
 | 
				
			||||||
 | 
					            onCloseProgressionForm={handleCloseProgressionForm}
 | 
				
			||||||
 | 
					            onCreateProgression={handleCreateProgression}
 | 
				
			||||||
 | 
					            onUpdateProgression={handleUpdateProgression}
 | 
				
			||||||
 | 
					            onDeleteProgression={onDeleteProgression}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    ) : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div key={milestone.id}>
 | 
				
			||||||
 | 
					            <ReleasePlanMilestone
 | 
				
			||||||
 | 
					                readonly={readonly}
 | 
				
			||||||
 | 
					                milestone={milestone}
 | 
				
			||||||
 | 
					                status={status}
 | 
				
			||||||
 | 
					                onStartMilestone={onStartMilestone}
 | 
				
			||||||
 | 
					                automationSection={automationSection}
 | 
				
			||||||
 | 
					                allMilestones={milestones}
 | 
				
			||||||
 | 
					                activeMilestoneId={activeMilestoneId}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					            <ConditionallyRender
 | 
				
			||||||
 | 
					                condition={isNotLastMilestone}
 | 
				
			||||||
 | 
					                show={<StyledConnection isCompleted={index < activeIndex} />}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const calculateMilestoneStatus = (
 | 
				
			||||||
 | 
					    milestone: IReleasePlanMilestone,
 | 
				
			||||||
 | 
					    activeMilestoneId: string | undefined,
 | 
				
			||||||
 | 
					    index: number,
 | 
				
			||||||
 | 
					    activeIndex: number,
 | 
				
			||||||
 | 
					    environmentIsDisabled: boolean | undefined,
 | 
				
			||||||
 | 
					): MilestoneStatus => {
 | 
				
			||||||
 | 
					    if (milestone.id === activeMilestoneId) {
 | 
				
			||||||
 | 
					        return environmentIsDisabled ? 'paused' : 'active';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (index < activeIndex) {
 | 
				
			||||||
 | 
					        return 'completed';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 'not-started';
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					import type { IReleasePlanMilestone } from 'interfaces/releasePlans';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    IReleasePlanMilestoneItemProps,
 | 
				
			||||||
 | 
					    PendingProgressionChange,
 | 
				
			||||||
 | 
					} from './ReleasePlanMilestoneItem.jsx';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface PendingProgressionChangeResult {
 | 
				
			||||||
 | 
					    pendingProgressionChange: PendingProgressionChange | null;
 | 
				
			||||||
 | 
					    effectiveTransitionCondition: IReleasePlanMilestone['transitionCondition'];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const usePendingProgressionChanges = (
 | 
				
			||||||
 | 
					    milestone: IReleasePlanMilestone,
 | 
				
			||||||
 | 
					    getPendingProgressionChange: IReleasePlanMilestoneItemProps['getPendingProgressionChange'],
 | 
				
			||||||
 | 
					): PendingProgressionChangeResult => {
 | 
				
			||||||
 | 
					    const pendingProgressionChange = getPendingProgressionChange(milestone.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Determine effective transition condition (use pending create if exists)
 | 
				
			||||||
 | 
					    let effectiveTransitionCondition = milestone.transitionCondition;
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					        pendingProgressionChange?.action === 'createMilestoneProgression' &&
 | 
				
			||||||
 | 
					        'transitionCondition' in pendingProgressionChange.payload &&
 | 
				
			||||||
 | 
					        pendingProgressionChange.payload.transitionCondition
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        effectiveTransitionCondition =
 | 
				
			||||||
 | 
					            pendingProgressionChange.payload.transitionCondition;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        pendingProgressionChange,
 | 
				
			||||||
 | 
					        effectiveTransitionCondition,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user