mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
refactor: release plan change
This commit is contained in:
parent
64d5727c45
commit
ecb85a8a45
@ -0,0 +1,97 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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 &&
|
||||||
|
milestone.transitionCondition;
|
||||||
|
|
||||||
|
const hasPendingDelete = milestonesWithDeletedAutomation.has(
|
||||||
|
milestone.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
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}
|
||||||
|
hasPendingUpdate={false}
|
||||||
|
hasPendingDelete={hasPendingDelete}
|
||||||
|
/>
|
||||||
|
</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,126 @@
|
|||||||
|
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 { useModifiedReleasePlan } from './useModifiedReleasePlan.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 = isCreate
|
||||||
|
? change.payload.sourceMilestone
|
||||||
|
: change.payload.sourceMilestoneId || 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 = useModifiedReleasePlan(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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -29,9 +29,9 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh
|
|||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import type { UpdateMilestoneProgressionSchema } from 'openapi';
|
import type { UpdateMilestoneProgressionSchema } from 'openapi';
|
||||||
import { MilestoneAutomationSection } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx';
|
import { MilestoneListRenderer } from './MilestoneListRenderer.tsx';
|
||||||
import { MilestoneTransitionDisplay } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneTransitionDisplay.tsx';
|
import { useModifiedReleasePlan } from './useModifiedReleasePlan.ts';
|
||||||
import type { MilestoneStatus } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
import { ProgressionChange } from './ProgressionChange.tsx';
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -39,13 +39,6 @@ const StyledTabs = styled(Tabs)(({ theme }) => ({
|
|||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledConnection = styled('div')(({ theme }) => ({
|
|
||||||
width: 2,
|
|
||||||
height: theme.spacing(2),
|
|
||||||
backgroundColor: theme.palette.divider,
|
|
||||||
marginLeft: theme.spacing(3.25),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const DeleteReleasePlan: FC<{
|
const DeleteReleasePlan: FC<{
|
||||||
change: IChangeRequestDeleteReleasePlan;
|
change: IChangeRequestDeleteReleasePlan;
|
||||||
currentReleasePlan?: IReleasePlan;
|
currentReleasePlan?: IReleasePlan;
|
||||||
@ -246,289 +239,6 @@ const AddReleasePlan: FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CreateMilestoneProgression: FC<{
|
|
||||||
change: IChangeRequestCreateMilestoneProgression;
|
|
||||||
currentReleasePlan?: IReleasePlan;
|
|
||||||
actions?: ReactNode;
|
|
||||||
changeRequestState: ChangeRequestState;
|
|
||||||
onUpdateChangeRequestSubmit?: (
|
|
||||||
sourceMilestoneId: string,
|
|
||||||
payload: UpdateMilestoneProgressionSchema,
|
|
||||||
) => void;
|
|
||||||
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void;
|
|
||||||
}> = ({
|
|
||||||
change,
|
|
||||||
currentReleasePlan,
|
|
||||||
actions,
|
|
||||||
changeRequestState,
|
|
||||||
onUpdateChangeRequestSubmit,
|
|
||||||
onDeleteChangeRequestSubmit,
|
|
||||||
}) => {
|
|
||||||
// Use snapshot if available (for Applied state) or if the change has a snapshot
|
|
||||||
const basePlan = change.payload.snapshot || currentReleasePlan;
|
|
||||||
if (!basePlan) return null;
|
|
||||||
|
|
||||||
// Create a modified release plan with the progression added
|
|
||||||
const modifiedPlan: IReleasePlan = {
|
|
||||||
...basePlan,
|
|
||||||
milestones: basePlan.milestones.map((milestone) => {
|
|
||||||
if (milestone.id === change.payload.sourceMilestone) {
|
|
||||||
return {
|
|
||||||
...milestone,
|
|
||||||
transitionCondition: change.payload.transitionCondition,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return milestone;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const sourceMilestone = basePlan.milestones.find(
|
|
||||||
(milestone) => milestone.id === change.payload.sourceMilestone,
|
|
||||||
);
|
|
||||||
|
|
||||||
const sourceMilestoneName =
|
|
||||||
sourceMilestone?.name || change.payload.sourceMilestone;
|
|
||||||
|
|
||||||
const targetMilestoneName =
|
|
||||||
basePlan.milestones.find(
|
|
||||||
(milestone) => milestone.id === change.payload.targetMilestone,
|
|
||||||
)?.name || change.payload.targetMilestone;
|
|
||||||
|
|
||||||
// Get the milestone before and after for diff
|
|
||||||
const previousMilestone = sourceMilestone;
|
|
||||||
const newMilestone = modifiedPlan.milestones.find(
|
|
||||||
(milestone) => milestone.id === change.payload.sourceMilestone,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledTabs>
|
|
||||||
<ChangeItemWrapper>
|
|
||||||
<ChangeItemInfo>
|
|
||||||
<Added>Adding automation to release plan</Added>
|
|
||||||
<Typography component='span'>
|
|
||||||
{sourceMilestoneName} → {targetMilestoneName}
|
|
||||||
</Typography>
|
|
||||||
</ChangeItemInfo>
|
|
||||||
<div>
|
|
||||||
<TabList>
|
|
||||||
<Tab>View change</Tab>
|
|
||||||
<Tab>View diff</Tab>
|
|
||||||
</TabList>
|
|
||||||
{actions}
|
|
||||||
</div>
|
|
||||||
</ChangeItemWrapper>
|
|
||||||
<TabPanel>
|
|
||||||
{modifiedPlan.milestones.map((milestone, index) => {
|
|
||||||
const isNotLastMilestone =
|
|
||||||
index < modifiedPlan.milestones.length - 1;
|
|
||||||
const isTargetMilestone =
|
|
||||||
milestone.id === change.payload.sourceMilestone;
|
|
||||||
const hasProgression = Boolean(
|
|
||||||
milestone.transitionCondition,
|
|
||||||
);
|
|
||||||
const showAutomation =
|
|
||||||
isTargetMilestone &&
|
|
||||||
isNotLastMilestone &&
|
|
||||||
hasProgression;
|
|
||||||
|
|
||||||
const readonly =
|
|
||||||
changeRequestState === 'Applied' ||
|
|
||||||
changeRequestState === 'Cancelled';
|
|
||||||
const status: MilestoneStatus = 'not-started'; // In change request view, always not-started
|
|
||||||
|
|
||||||
// Build automation section for this milestone
|
|
||||||
const automationSection =
|
|
||||||
showAutomation && milestone.transitionCondition ? (
|
|
||||||
<MilestoneAutomationSection status={status}>
|
|
||||||
<MilestoneTransitionDisplay
|
|
||||||
intervalMinutes={
|
|
||||||
milestone.transitionCondition
|
|
||||||
.intervalMinutes
|
|
||||||
}
|
|
||||||
onSave={async (payload) => {
|
|
||||||
onUpdateChangeRequestSubmit?.(
|
|
||||||
milestone.id,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return { shouldReset: true };
|
|
||||||
}}
|
|
||||||
onDelete={() =>
|
|
||||||
onDeleteChangeRequestSubmit?.(
|
|
||||||
milestone.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
milestoneName={milestone.name}
|
|
||||||
status={status}
|
|
||||||
hasPendingUpdate={false}
|
|
||||||
hasPendingDelete={false}
|
|
||||||
/>
|
|
||||||
</MilestoneAutomationSection>
|
|
||||||
) : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={milestone.id}>
|
|
||||||
<ReleasePlanMilestone
|
|
||||||
readonly={readonly}
|
|
||||||
milestone={milestone}
|
|
||||||
automationSection={automationSection}
|
|
||||||
allMilestones={modifiedPlan.milestones}
|
|
||||||
activeMilestoneId={
|
|
||||||
modifiedPlan.activeMilestoneId
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isNotLastMilestone && <StyledConnection />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel variant='diff'>
|
|
||||||
<EventDiff
|
|
||||||
entry={{
|
|
||||||
preData: previousMilestone,
|
|
||||||
data: newMilestone,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
</StyledTabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UpdateMilestoneProgression: FC<{
|
|
||||||
change: IChangeRequestUpdateMilestoneProgression;
|
|
||||||
currentReleasePlan?: IReleasePlan;
|
|
||||||
actions?: ReactNode;
|
|
||||||
changeRequestState: ChangeRequestState;
|
|
||||||
onUpdateChangeRequestSubmit?: (
|
|
||||||
sourceMilestoneId: string,
|
|
||||||
payload: UpdateMilestoneProgressionSchema,
|
|
||||||
) => void;
|
|
||||||
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void;
|
|
||||||
}> = ({
|
|
||||||
change,
|
|
||||||
currentReleasePlan,
|
|
||||||
actions,
|
|
||||||
changeRequestState,
|
|
||||||
onUpdateChangeRequestSubmit,
|
|
||||||
onDeleteChangeRequestSubmit,
|
|
||||||
}) => {
|
|
||||||
// Use snapshot if available (for Applied state) or if the change has a snapshot
|
|
||||||
const basePlan = change.payload.snapshot || currentReleasePlan;
|
|
||||||
if (!basePlan) return null;
|
|
||||||
|
|
||||||
const sourceId =
|
|
||||||
change.payload.sourceMilestoneId || change.payload.sourceMilestone;
|
|
||||||
const sourceMilestone = basePlan.milestones.find(
|
|
||||||
(milestone) => milestone.id === sourceId,
|
|
||||||
);
|
|
||||||
const sourceMilestoneName = sourceMilestone?.name || sourceId;
|
|
||||||
|
|
||||||
// Create a modified release plan with the updated progression
|
|
||||||
const modifiedPlan: IReleasePlan = {
|
|
||||||
...basePlan,
|
|
||||||
milestones: basePlan.milestones.map((milestone) => {
|
|
||||||
if (milestone.id === sourceId) {
|
|
||||||
return {
|
|
||||||
...milestone,
|
|
||||||
transitionCondition: change.payload.transitionCondition,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return milestone;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the milestone before and after for diff
|
|
||||||
const previousMilestone = sourceMilestone;
|
|
||||||
const newMilestone = modifiedPlan.milestones.find(
|
|
||||||
(milestone) => milestone.id === change.payload.sourceMilestoneId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledTabs>
|
|
||||||
<ChangeItemWrapper>
|
|
||||||
<ChangeItemInfo>
|
|
||||||
<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>
|
|
||||||
{modifiedPlan.milestones.map((milestone, index) => {
|
|
||||||
const isNotLastMilestone =
|
|
||||||
index < modifiedPlan.milestones.length - 1;
|
|
||||||
const showAutomation =
|
|
||||||
milestone.id === sourceId &&
|
|
||||||
isNotLastMilestone &&
|
|
||||||
Boolean(milestone.transitionCondition);
|
|
||||||
|
|
||||||
const readonly =
|
|
||||||
changeRequestState === 'Applied' ||
|
|
||||||
changeRequestState === 'Cancelled';
|
|
||||||
const status: MilestoneStatus = 'not-started';
|
|
||||||
|
|
||||||
// Build automation section for this milestone
|
|
||||||
const automationSection =
|
|
||||||
showAutomation && milestone.transitionCondition ? (
|
|
||||||
<MilestoneAutomationSection status={status}>
|
|
||||||
<MilestoneTransitionDisplay
|
|
||||||
intervalMinutes={
|
|
||||||
milestone.transitionCondition
|
|
||||||
.intervalMinutes
|
|
||||||
}
|
|
||||||
onSave={async (payload) => {
|
|
||||||
onUpdateChangeRequestSubmit?.(
|
|
||||||
milestone.id,
|
|
||||||
payload,
|
|
||||||
);
|
|
||||||
return { shouldReset: true };
|
|
||||||
}}
|
|
||||||
onDelete={() =>
|
|
||||||
onDeleteChangeRequestSubmit?.(
|
|
||||||
milestone.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
milestoneName={milestone.name}
|
|
||||||
status={status}
|
|
||||||
hasPendingUpdate={false}
|
|
||||||
hasPendingDelete={false}
|
|
||||||
/>
|
|
||||||
</MilestoneAutomationSection>
|
|
||||||
) : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={milestone.id}>
|
|
||||||
<ReleasePlanMilestone
|
|
||||||
readonly={readonly}
|
|
||||||
milestone={milestone}
|
|
||||||
automationSection={automationSection}
|
|
||||||
allMilestones={modifiedPlan.milestones}
|
|
||||||
activeMilestoneId={
|
|
||||||
modifiedPlan.activeMilestoneId
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isNotLastMilestone && <StyledConnection />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel variant='diff'>
|
|
||||||
<EventDiff
|
|
||||||
entry={{
|
|
||||||
preData: previousMilestone,
|
|
||||||
data: newMilestone,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
</StyledTabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConsolidatedProgressionChanges: FC<{
|
const ConsolidatedProgressionChanges: FC<{
|
||||||
feature: IChangeRequestFeature;
|
feature: IChangeRequestFeature;
|
||||||
@ -562,7 +272,6 @@ const ConsolidatedProgressionChanges: FC<{
|
|||||||
if (progressionChanges.length === 0) return null;
|
if (progressionChanges.length === 0) return null;
|
||||||
|
|
||||||
// Use snapshot from first change if available, otherwise use current release plan
|
// Use snapshot from first change if available, otherwise use current release plan
|
||||||
// Prioritize create/update changes over delete changes for snapshot selection
|
|
||||||
const firstChangeWithSnapshot =
|
const firstChangeWithSnapshot =
|
||||||
progressionChanges.find(
|
progressionChanges.find(
|
||||||
(change) =>
|
(change) =>
|
||||||
@ -589,61 +298,36 @@ const ConsolidatedProgressionChanges: FC<{
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all progression changes to the release plan
|
// Apply all progression changes using our hook
|
||||||
const modifiedPlan: IReleasePlan = {
|
const modifiedPlan = useModifiedReleasePlan(basePlan, progressionChanges);
|
||||||
...basePlan,
|
|
||||||
milestones: basePlan.milestones.map((milestone) => {
|
// Collect milestone IDs with automation (modified or original)
|
||||||
// Find if there's a progression change for this milestone
|
const milestonesWithAutomation = new Set(
|
||||||
const createChange = progressionChanges.find(
|
progressionChanges
|
||||||
(change): change is IChangeRequestCreateMilestoneProgression =>
|
.filter(
|
||||||
change.action === 'createMilestoneProgression' &&
|
(change) =>
|
||||||
change.payload.sourceMilestone === milestone.id,
|
change.action === 'createMilestoneProgression' ||
|
||||||
);
|
change.action === 'updateMilestoneProgression',
|
||||||
const updateChange = progressionChanges.find(
|
)
|
||||||
(change): change is IChangeRequestUpdateMilestoneProgression =>
|
.map((change) =>
|
||||||
change.action === 'updateMilestoneProgression' &&
|
change.action === 'createMilestoneProgression'
|
||||||
(change.payload.sourceMilestoneId === milestone.id ||
|
? change.payload.sourceMilestone
|
||||||
change.payload.sourceMilestone === milestone.id),
|
: change.payload.sourceMilestoneId ||
|
||||||
);
|
change.payload.sourceMilestone,
|
||||||
const deleteChange = progressionChanges.find(
|
)
|
||||||
(change): change is IChangeRequestDeleteMilestoneProgression =>
|
.filter((id): id is string => Boolean(id)),
|
||||||
change.action === 'deleteMilestoneProgression' &&
|
|
||||||
(change.payload.sourceMilestoneId === milestone.id ||
|
|
||||||
change.payload.sourceMilestone === milestone.id),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for conflicting changes (delete + create/update for same milestone)
|
const milestonesWithDeletedAutomation = new Set(
|
||||||
if (deleteChange && (createChange || updateChange)) {
|
progressionChanges
|
||||||
console.warn(
|
.filter((change) => change.action === 'deleteMilestoneProgression')
|
||||||
'[ConsolidatedProgressionChanges] Conflicting changes detected for milestone:',
|
.map(
|
||||||
{
|
(change) =>
|
||||||
milestone: milestone.name,
|
change.payload.sourceMilestoneId ||
|
||||||
hasCreate: !!createChange,
|
change.payload.sourceMilestone,
|
||||||
hasUpdate: !!updateChange,
|
)
|
||||||
hasDelete: !!deleteChange,
|
.filter((id): id is string => Boolean(id)),
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a delete change, remove the transition condition
|
|
||||||
// Delete takes precedence over create/update
|
|
||||||
if (deleteChange) {
|
|
||||||
return {
|
|
||||||
...milestone,
|
|
||||||
transitionCondition: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const change = updateChange || createChange;
|
|
||||||
if (change) {
|
|
||||||
return {
|
|
||||||
...milestone,
|
|
||||||
transitionCondition: change.payload.transitionCondition,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return milestone;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeDescriptions = progressionChanges.map((change) => {
|
const changeDescriptions = progressionChanges.map((change) => {
|
||||||
const sourceId =
|
const sourceId =
|
||||||
@ -663,6 +347,18 @@ const ConsolidatedProgressionChanges: FC<{
|
|||||||
return `${action} automation for ${sourceName}`;
|
return `${action} automation for ${sourceName}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For diff view, we need to use basePlan with deleted automations shown
|
||||||
|
const basePlanWithDeletedAutomations: IReleasePlan = {
|
||||||
|
...basePlan,
|
||||||
|
milestones: basePlan.milestones.map((milestone) => {
|
||||||
|
// If this milestone is being deleted, ensure it has its transition condition
|
||||||
|
if (milestonesWithDeletedAutomation.has(milestone.id)) {
|
||||||
|
return milestone;
|
||||||
|
}
|
||||||
|
return milestone;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTabs>
|
<StyledTabs>
|
||||||
<ChangeItemWrapper>
|
<ChangeItemWrapper>
|
||||||
@ -687,93 +383,20 @@ const ConsolidatedProgressionChanges: FC<{
|
|||||||
</div>
|
</div>
|
||||||
</ChangeItemWrapper>
|
</ChangeItemWrapper>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
{modifiedPlan.milestones.map((milestone, index) => {
|
<MilestoneListRenderer
|
||||||
const isNotLastMilestone =
|
plan={
|
||||||
index < modifiedPlan.milestones.length - 1;
|
milestonesWithDeletedAutomation.size > 0
|
||||||
|
? basePlanWithDeletedAutomations
|
||||||
// Check if there's a delete change for this milestone
|
: modifiedPlan
|
||||||
const deleteChange = progressionChanges.find(
|
|
||||||
(
|
|
||||||
change,
|
|
||||||
): change is IChangeRequestDeleteMilestoneProgression =>
|
|
||||||
change.action === 'deleteMilestoneProgression' &&
|
|
||||||
(change.payload.sourceMilestoneId ===
|
|
||||||
milestone.id ||
|
|
||||||
change.payload.sourceMilestone ===
|
|
||||||
milestone.id),
|
|
||||||
);
|
|
||||||
|
|
||||||
// If there's a delete change, use the original milestone from basePlan
|
|
||||||
const originalMilestone = deleteChange
|
|
||||||
? basePlan.milestones.find(
|
|
||||||
(baseMilestone) =>
|
|
||||||
baseMilestone.id === milestone.id,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const displayMilestone =
|
|
||||||
deleteChange && originalMilestone
|
|
||||||
? originalMilestone
|
|
||||||
: milestone;
|
|
||||||
|
|
||||||
// Show automation section for any milestone that has a transition condition
|
|
||||||
// or if there's a delete change (to show what's being deleted)
|
|
||||||
const shouldShowAutomationSection =
|
|
||||||
Boolean(displayMilestone.transitionCondition) ||
|
|
||||||
Boolean(deleteChange);
|
|
||||||
const showAutomation =
|
|
||||||
isNotLastMilestone && shouldShowAutomationSection;
|
|
||||||
|
|
||||||
const readonly =
|
|
||||||
changeRequestState === 'Applied' ||
|
|
||||||
changeRequestState === 'Cancelled';
|
|
||||||
const status: MilestoneStatus = 'not-started';
|
|
||||||
|
|
||||||
// Build automation section for this milestone
|
|
||||||
const automationSection =
|
|
||||||
showAutomation &&
|
|
||||||
displayMilestone.transitionCondition ? (
|
|
||||||
<MilestoneAutomationSection status={status}>
|
|
||||||
<MilestoneTransitionDisplay
|
|
||||||
intervalMinutes={
|
|
||||||
displayMilestone.transitionCondition
|
|
||||||
.intervalMinutes
|
|
||||||
}
|
}
|
||||||
onSave={async (payload) => {
|
changeRequestState={changeRequestState}
|
||||||
onUpdateChangeRequestSubmit?.(
|
milestonesWithAutomation={milestonesWithAutomation}
|
||||||
displayMilestone.id,
|
milestonesWithDeletedAutomation={
|
||||||
payload,
|
milestonesWithDeletedAutomation
|
||||||
);
|
|
||||||
return { shouldReset: true };
|
|
||||||
}}
|
|
||||||
onDelete={() =>
|
|
||||||
onDeleteChangeRequestSubmit?.(
|
|
||||||
displayMilestone.id,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
milestoneName={displayMilestone.name}
|
onUpdateAutomation={onUpdateChangeRequestSubmit}
|
||||||
status={status}
|
onDeleteAutomation={onDeleteChangeRequestSubmit}
|
||||||
hasPendingUpdate={false}
|
|
||||||
hasPendingDelete={Boolean(deleteChange)}
|
|
||||||
/>
|
/>
|
||||||
</MilestoneAutomationSection>
|
|
||||||
) : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={milestone.id}>
|
|
||||||
<ReleasePlanMilestone
|
|
||||||
readonly={readonly}
|
|
||||||
milestone={displayMilestone}
|
|
||||||
automationSection={automationSection}
|
|
||||||
allMilestones={modifiedPlan.milestones}
|
|
||||||
activeMilestoneId={
|
|
||||||
modifiedPlan.activeMilestoneId
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{isNotLastMilestone && <StyledConnection />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel variant='diff'>
|
<TabPanel variant='diff'>
|
||||||
<EventDiff
|
<EventDiff
|
||||||
@ -930,22 +553,9 @@ export const ReleasePlanChange: FC<{
|
|||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{change.action === 'createMilestoneProgression' && (
|
{(change.action === 'createMilestoneProgression' ||
|
||||||
<CreateMilestoneProgression
|
change.action === 'updateMilestoneProgression') && (
|
||||||
change={change}
|
<ProgressionChange
|
||||||
currentReleasePlan={currentReleasePlan}
|
|
||||||
actions={actions}
|
|
||||||
changeRequestState={changeRequestState}
|
|
||||||
onUpdateChangeRequestSubmit={
|
|
||||||
handleUpdateChangeRequestSubmit
|
|
||||||
}
|
|
||||||
onDeleteChangeRequestSubmit={
|
|
||||||
handleDeleteChangeRequestSubmit
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{change.action === 'updateMilestoneProgression' && (
|
|
||||||
<UpdateMilestoneProgression
|
|
||||||
change={change}
|
change={change}
|
||||||
currentReleasePlan={currentReleasePlan}
|
currentReleasePlan={currentReleasePlan}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
|||||||
@ -0,0 +1,71 @@
|
|||||||
|
import type { IReleasePlan } from 'interfaces/releasePlans';
|
||||||
|
import type {
|
||||||
|
IChangeRequestCreateMilestoneProgression,
|
||||||
|
IChangeRequestUpdateMilestoneProgression,
|
||||||
|
IChangeRequestDeleteMilestoneProgression,
|
||||||
|
} from 'component/changeRequest/changeRequest.types';
|
||||||
|
|
||||||
|
type ProgressionChange =
|
||||||
|
| IChangeRequestCreateMilestoneProgression
|
||||||
|
| IChangeRequestUpdateMilestoneProgression
|
||||||
|
| IChangeRequestDeleteMilestoneProgression;
|
||||||
|
|
||||||
|
export const useModifiedReleasePlan = (
|
||||||
|
basePlan: IReleasePlan,
|
||||||
|
progressionChanges: ProgressionChange[],
|
||||||
|
): IReleasePlan => {
|
||||||
|
return {
|
||||||
|
...basePlan,
|
||||||
|
milestones: basePlan.milestones.map((milestone) => {
|
||||||
|
// Find if there's a progression change for this 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.sourceMilestoneId === milestone.id ||
|
||||||
|
change.payload.sourceMilestone === milestone.id),
|
||||||
|
);
|
||||||
|
const deleteChange = progressionChanges.find(
|
||||||
|
(change): change is IChangeRequestDeleteMilestoneProgression =>
|
||||||
|
change.action === 'deleteMilestoneProgression' &&
|
||||||
|
(change.payload.sourceMilestoneId === milestone.id ||
|
||||||
|
change.payload.sourceMilestone === milestone.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for conflicting changes (delete + create/update for same milestone)
|
||||||
|
if (deleteChange && (createChange || updateChange)) {
|
||||||
|
console.warn(
|
||||||
|
'[useModifiedReleasePlan] Conflicting changes detected for milestone:',
|
||||||
|
{
|
||||||
|
milestone: milestone.name,
|
||||||
|
hasCreate: !!createChange,
|
||||||
|
hasUpdate: !!updateChange,
|
||||||
|
hasDelete: !!deleteChange,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's a delete change, remove the transition condition
|
||||||
|
// Delete takes precedence over create/update
|
||||||
|
if (deleteChange) {
|
||||||
|
return {
|
||||||
|
...milestone,
|
||||||
|
transitionCondition: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = updateChange || createChange;
|
||||||
|
if (change) {
|
||||||
|
return {
|
||||||
|
...milestone,
|
||||||
|
transitionCondition: change.payload.transitionCondition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return milestone;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -10,7 +10,7 @@ import { MilestoneAutomationSection } from '../ReleasePlanMilestone/MilestoneAut
|
|||||||
import { MilestoneTransitionDisplay } from '../ReleasePlanMilestone/MilestoneTransitionDisplay.tsx';
|
import { MilestoneTransitionDisplay } from '../ReleasePlanMilestone/MilestoneTransitionDisplay.tsx';
|
||||||
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
import type { MilestoneStatus } from '../ReleasePlanMilestone/ReleasePlanMilestoneStatus.tsx';
|
||||||
import { MilestoneProgressionForm } from '../MilestoneProgressionForm/MilestoneProgressionForm.tsx';
|
import { MilestoneProgressionForm } from '../MilestoneProgressionForm/MilestoneProgressionForm.tsx';
|
||||||
import type { PendingProgressionChange } from './ReleasePlanMilestoneItem';
|
import type { PendingProgressionChange } from './ReleasePlanMilestoneItem.tsx';
|
||||||
|
|
||||||
const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
|
const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user