1
0
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:
FredrikOseberg 2025-10-21 14:59:35 +02:00
parent 64d5727c45
commit ecb85a8a45
No known key found for this signature in database
GPG Key ID: 282FD8A6D8F9BCF0
5 changed files with 355 additions and 451 deletions

View File

@ -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>
);
})}
</>
);
};

View File

@ -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>
);
};

View File

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

View File

@ -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;
}),
};
};

View File

@ -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',