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,11 +296,22 @@ 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<
|
||||||
@ -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