mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
feat: inital implementation
This commit is contained in:
parent
0fda3e7cf1
commit
244f6fbd43
@ -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 ? (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { useRef, useState, type FC, type ReactNode } from 'react';
|
import { useRef, useState, type FC, type ReactNode } from 'react';
|
||||||
import { styled, Typography } from '@mui/material';
|
import { Alert, styled, Typography } from '@mui/material';
|
||||||
import type {
|
import type {
|
||||||
ChangeRequestState,
|
ChangeRequestState,
|
||||||
IChangeRequestAddReleasePlan,
|
IChangeRequestAddReleasePlan,
|
||||||
IChangeRequestDeleteReleasePlan,
|
IChangeRequestDeleteReleasePlan,
|
||||||
IChangeRequestStartMilestone,
|
IChangeRequestStartMilestone,
|
||||||
|
IChangeRequestCreateMilestoneProgression,
|
||||||
|
IChangeRequestUpdateMilestoneProgression,
|
||||||
|
IChangeRequestDeleteMilestoneProgression,
|
||||||
|
IChangeRequestFeature,
|
||||||
} 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 +25,41 @@ 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 { ReleasePlanProvider } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanContext.tsx';
|
||||||
|
|
||||||
|
// Indicates that a change is in draft and not yet part of a change request
|
||||||
|
const PENDING_CHANGE_REQUEST_ID = -1;
|
||||||
|
|
||||||
|
// Helper function to create getPendingProgressionChange for context
|
||||||
|
const createGetPendingProgressionChange = (
|
||||||
|
progressionChanges: (IChangeRequestCreateMilestoneProgression | IChangeRequestUpdateMilestoneProgression | IChangeRequestDeleteMilestoneProgression)[]
|
||||||
|
) => {
|
||||||
|
return (sourceMilestoneId: string) => {
|
||||||
|
const change = progressionChanges.find(
|
||||||
|
(progressionChange) =>
|
||||||
|
(progressionChange.action === 'updateMilestoneProgression' &&
|
||||||
|
(progressionChange.payload.sourceMilestoneId === sourceMilestoneId ||
|
||||||
|
progressionChange.payload.sourceMilestone === sourceMilestoneId)) ||
|
||||||
|
(progressionChange.action === 'deleteMilestoneProgression' &&
|
||||||
|
(progressionChange.payload.sourceMilestoneId === sourceMilestoneId ||
|
||||||
|
progressionChange.payload.sourceMilestone === sourceMilestoneId)) ||
|
||||||
|
(progressionChange.action === 'createMilestoneProgression' &&
|
||||||
|
progressionChange.payload.sourceMilestone === sourceMilestoneId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!change) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
action: change.action,
|
||||||
|
payload: change.payload,
|
||||||
|
changeRequestId: PENDING_CHANGE_REQUEST_ID,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
const StyledTabs = styled(Tabs)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -28,6 +67,13 @@ 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;
|
||||||
@ -228,16 +274,512 @@ const AddReleasePlan: FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CreateMilestoneProgression: FC<{
|
||||||
|
change: IChangeRequestCreateMilestoneProgression;
|
||||||
|
currentReleasePlan?: IReleasePlan;
|
||||||
|
actions?: ReactNode;
|
||||||
|
projectId: string;
|
||||||
|
environmentName: string;
|
||||||
|
featureName: string;
|
||||||
|
changeRequestState: ChangeRequestState;
|
||||||
|
onUpdate?: () => void;
|
||||||
|
onUpdateChangeRequestSubmit?: (
|
||||||
|
sourceMilestoneId: string,
|
||||||
|
payload: UpdateMilestoneProgressionSchema,
|
||||||
|
) => void;
|
||||||
|
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void;
|
||||||
|
}> = ({
|
||||||
|
change,
|
||||||
|
currentReleasePlan,
|
||||||
|
actions,
|
||||||
|
projectId,
|
||||||
|
environmentName,
|
||||||
|
featureName,
|
||||||
|
changeRequestState,
|
||||||
|
onUpdate,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a function to get this specific change for the context
|
||||||
|
const getPendingProgressionChange = (sourceMilestoneId: string) => {
|
||||||
|
if (sourceMilestoneId === change.payload.sourceMilestone) {
|
||||||
|
return {
|
||||||
|
action: change.action,
|
||||||
|
payload: change.payload,
|
||||||
|
changeRequestId: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReleasePlanProvider getPendingProgressionChange={getPendingProgressionChange}>
|
||||||
|
<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;
|
||||||
|
|
||||||
|
console.log('[CreateProgression] Milestone:', milestone.name, {
|
||||||
|
isTargetMilestone,
|
||||||
|
isNotLastMilestone,
|
||||||
|
hasProgression,
|
||||||
|
showAutomation,
|
||||||
|
transitionCondition: milestone.transitionCondition,
|
||||||
|
projectId,
|
||||||
|
environment: environmentName,
|
||||||
|
featureName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={milestone.id}>
|
||||||
|
<ReleasePlanMilestone
|
||||||
|
readonly={changeRequestState === 'Applied' || changeRequestState === 'Cancelled'}
|
||||||
|
milestone={milestone}
|
||||||
|
showAutomation={showAutomation}
|
||||||
|
projectId={projectId}
|
||||||
|
environment={environmentName}
|
||||||
|
featureName={featureName}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onUpdateChangeRequestSubmit={onUpdateChangeRequestSubmit}
|
||||||
|
onDeleteAutomation={
|
||||||
|
showAutomation && onDeleteChangeRequestSubmit
|
||||||
|
? () => onDeleteChangeRequestSubmit(milestone.id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
allMilestones={modifiedPlan.milestones}
|
||||||
|
activeMilestoneId={modifiedPlan.activeMilestoneId}
|
||||||
|
/>
|
||||||
|
{isNotLastMilestone && <StyledConnection />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel variant='diff'>
|
||||||
|
<EventDiff
|
||||||
|
entry={{
|
||||||
|
preData: previousMilestone,
|
||||||
|
data: newMilestone,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</StyledTabs>
|
||||||
|
</ReleasePlanProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateMilestoneProgression: FC<{
|
||||||
|
change: IChangeRequestUpdateMilestoneProgression;
|
||||||
|
currentReleasePlan?: IReleasePlan;
|
||||||
|
actions?: ReactNode;
|
||||||
|
projectId: string;
|
||||||
|
environmentName: string;
|
||||||
|
featureName: string;
|
||||||
|
changeRequestState: ChangeRequestState;
|
||||||
|
onUpdate?: () => void;
|
||||||
|
onUpdateChangeRequestSubmit?: (
|
||||||
|
sourceMilestoneId: string,
|
||||||
|
payload: UpdateMilestoneProgressionSchema,
|
||||||
|
) => void;
|
||||||
|
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void;
|
||||||
|
}> = ({
|
||||||
|
change,
|
||||||
|
currentReleasePlan,
|
||||||
|
actions,
|
||||||
|
projectId,
|
||||||
|
environmentName,
|
||||||
|
featureName,
|
||||||
|
changeRequestState,
|
||||||
|
onUpdate,
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a function to get this specific change for the context
|
||||||
|
const getPendingProgressionChange = (sourceMilestoneId: string) => {
|
||||||
|
if (sourceMilestoneId === sourceId) {
|
||||||
|
return {
|
||||||
|
action: change.action,
|
||||||
|
payload: change.payload,
|
||||||
|
changeRequestId: -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReleasePlanProvider getPendingProgressionChange={getPendingProgressionChange}>
|
||||||
|
<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);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={milestone.id}>
|
||||||
|
<ReleasePlanMilestone
|
||||||
|
readonly={changeRequestState === 'Applied' || changeRequestState === 'Cancelled'}
|
||||||
|
milestone={milestone}
|
||||||
|
showAutomation={showAutomation}
|
||||||
|
projectId={projectId}
|
||||||
|
environment={environmentName}
|
||||||
|
featureName={featureName}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onUpdateChangeRequestSubmit={onUpdateChangeRequestSubmit}
|
||||||
|
onDeleteAutomation={
|
||||||
|
showAutomation && onDeleteChangeRequestSubmit
|
||||||
|
? () => onDeleteChangeRequestSubmit(milestone.id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
allMilestones={modifiedPlan.milestones}
|
||||||
|
activeMilestoneId={modifiedPlan.activeMilestoneId}
|
||||||
|
/>
|
||||||
|
{isNotLastMilestone && <StyledConnection />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel variant='diff'>
|
||||||
|
<EventDiff
|
||||||
|
entry={{
|
||||||
|
preData: previousMilestone,
|
||||||
|
data: newMilestone,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</StyledTabs>
|
||||||
|
</ReleasePlanProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConsolidatedProgressionChanges: FC<{
|
||||||
|
feature: IChangeRequestFeature;
|
||||||
|
currentReleasePlan?: IReleasePlan;
|
||||||
|
projectId: string;
|
||||||
|
environmentName: string;
|
||||||
|
featureName: string;
|
||||||
|
changeRequestState: ChangeRequestState;
|
||||||
|
onUpdate?: () => void;
|
||||||
|
onUpdateChangeRequestSubmit?: (
|
||||||
|
sourceMilestoneId: string,
|
||||||
|
payload: UpdateMilestoneProgressionSchema,
|
||||||
|
) => void;
|
||||||
|
onDeleteChangeRequestSubmit?: (sourceMilestoneId: string) => void;
|
||||||
|
}> = ({
|
||||||
|
feature,
|
||||||
|
currentReleasePlan,
|
||||||
|
projectId,
|
||||||
|
environmentName,
|
||||||
|
featureName,
|
||||||
|
changeRequestState,
|
||||||
|
onUpdate,
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Use snapshot from first change if available, otherwise use current release plan
|
||||||
|
// Prioritize create/update changes over delete changes for snapshot selection
|
||||||
|
const firstChangeWithSnapshot = progressionChanges.find((change) =>
|
||||||
|
change.payload?.snapshot && (change.action === 'createMilestoneProgression' || change.action === 'updateMilestoneProgression')
|
||||||
|
) || progressionChanges.find((change) => change.payload?.snapshot);
|
||||||
|
const basePlan = firstChangeWithSnapshot?.payload?.snapshot || currentReleasePlan;
|
||||||
|
|
||||||
|
if (!basePlan) {
|
||||||
|
console.error('[ConsolidatedProgressionChanges] No release plan data available', {
|
||||||
|
hasSnapshot: !!firstChangeWithSnapshot,
|
||||||
|
hasCurrentPlan: !!currentReleasePlan,
|
||||||
|
progressionChanges
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Alert severity="error">
|
||||||
|
Unable to load release plan data. Please refresh the page.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all progression changes to the release plan
|
||||||
|
const modifiedPlan: IReleasePlan = {
|
||||||
|
...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('[ConsolidatedProgressionChanges] 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;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeDescriptions = progressionChanges.map((change) => {
|
||||||
|
const sourceId =
|
||||||
|
change.action === 'createMilestoneProgression'
|
||||||
|
? change.payload.sourceMilestone
|
||||||
|
: (change.payload.sourceMilestoneId || 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}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a function to get pending progression changes for the context
|
||||||
|
const getPendingProgressionChange = createGetPendingProgressionChange(progressionChanges);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReleasePlanProvider getPendingProgressionChange={getPendingProgressionChange}>
|
||||||
|
<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>
|
||||||
|
{modifiedPlan.milestones.map((milestone, index) => {
|
||||||
|
const isNotLastMilestone =
|
||||||
|
index < modifiedPlan.milestones.length - 1;
|
||||||
|
|
||||||
|
// Check if there's a delete change for this milestone
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Warn if we can't find the original milestone for a delete change
|
||||||
|
if (deleteChange && !originalMilestone) {
|
||||||
|
console.error('[ConsolidatedProgressionChanges] Cannot find original milestone for delete', {
|
||||||
|
milestoneId: milestone.id,
|
||||||
|
milestoneName: milestone.name,
|
||||||
|
basePlanMilestones: basePlan.milestones.map(baseMilestone => ({ id: baseMilestone.id, name: baseMilestone.name }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={milestone.id}>
|
||||||
|
<ReleasePlanMilestone
|
||||||
|
readonly={changeRequestState === 'Applied' || changeRequestState === 'Cancelled'}
|
||||||
|
milestone={displayMilestone}
|
||||||
|
showAutomation={showAutomation}
|
||||||
|
projectId={projectId}
|
||||||
|
environment={environmentName}
|
||||||
|
featureName={featureName}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onUpdateChangeRequestSubmit={onUpdateChangeRequestSubmit}
|
||||||
|
onDeleteAutomation={
|
||||||
|
showAutomation && onDeleteChangeRequestSubmit
|
||||||
|
? () => onDeleteChangeRequestSubmit(displayMilestone.id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
allMilestones={modifiedPlan.milestones}
|
||||||
|
activeMilestoneId={modifiedPlan.activeMilestoneId}
|
||||||
|
/>
|
||||||
|
{isNotLastMilestone && <StyledConnection />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel variant='diff'>
|
||||||
|
<EventDiff
|
||||||
|
entry={{
|
||||||
|
preData: basePlan,
|
||||||
|
data: modifiedPlan,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</StyledTabs>
|
||||||
|
</ReleasePlanProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const ReleasePlanChange: FC<{
|
export const ReleasePlanChange: FC<{
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
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,
|
||||||
@ -245,13 +787,102 @@ 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 handleUpdate = async () => {
|
||||||
|
await refetch();
|
||||||
|
if (onRefetch) {
|
||||||
|
await onRefetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentName={environmentName}
|
||||||
|
featureName={featureName}
|
||||||
|
changeRequestState={changeRequestState}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onUpdateChangeRequestSubmit={handleUpdateChangeRequestSubmit}
|
||||||
|
onDeleteChangeRequestSubmit={handleDeleteChangeRequestSubmit}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -280,6 +911,34 @@ export const ReleasePlanChange: FC<{
|
|||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{change.action === 'createMilestoneProgression' && (
|
||||||
|
<CreateMilestoneProgression
|
||||||
|
change={change}
|
||||||
|
currentReleasePlan={currentReleasePlan}
|
||||||
|
actions={actions}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentName={environmentName}
|
||||||
|
featureName={featureName}
|
||||||
|
changeRequestState={changeRequestState}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onUpdateChangeRequestSubmit={handleUpdateChangeRequestSubmit}
|
||||||
|
onDeleteChangeRequestSubmit={handleDeleteChangeRequestSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{change.action === 'updateMilestoneProgression' && (
|
||||||
|
<UpdateMilestoneProgression
|
||||||
|
change={change}
|
||||||
|
currentReleasePlan={currentReleasePlan}
|
||||||
|
actions={actions}
|
||||||
|
projectId={projectId}
|
||||||
|
environmentName={environmentName}
|
||||||
|
featureName={featureName}
|
||||||
|
changeRequestState={changeRequestState}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onUpdateChangeRequestSubmit={handleUpdateChangeRequestSubmit}
|
||||||
|
onDeleteChangeRequestSubmit={handleDeleteChangeRequestSubmit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -137,7 +137,8 @@ type ChangeRequestPayload =
|
|||||||
| ChangeRequestDeleteReleasePlan
|
| ChangeRequestDeleteReleasePlan
|
||||||
| ChangeRequestStartMilestone
|
| ChangeRequestStartMilestone
|
||||||
| ChangeRequestCreateMilestoneProgression
|
| ChangeRequestCreateMilestoneProgression
|
||||||
| ChangeRequestUpdateMilestoneProgression;
|
| ChangeRequestUpdateMilestoneProgression
|
||||||
|
| ChangeRequestDeleteMilestoneProgression;
|
||||||
|
|
||||||
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
|
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
|
||||||
action: 'addStrategy';
|
action: 'addStrategy';
|
||||||
@ -206,6 +207,12 @@ export interface IChangeRequestUpdateMilestoneProgression
|
|||||||
payload: ChangeRequestUpdateMilestoneProgression;
|
payload: ChangeRequestUpdateMilestoneProgression;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IChangeRequestDeleteMilestoneProgression
|
||||||
|
extends IChangeRequestChangeBase {
|
||||||
|
action: 'deleteMilestoneProgression';
|
||||||
|
payload: ChangeRequestDeleteMilestoneProgression;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IChangeRequestReorderStrategy
|
export interface IChangeRequestReorderStrategy
|
||||||
extends IChangeRequestChangeBase {
|
extends IChangeRequestChangeBase {
|
||||||
action: 'reorderStrategy';
|
action: 'reorderStrategy';
|
||||||
@ -255,7 +262,8 @@ export type IFeatureChange =
|
|||||||
| IChangeRequestDeleteReleasePlan
|
| IChangeRequestDeleteReleasePlan
|
||||||
| IChangeRequestStartMilestone
|
| IChangeRequestStartMilestone
|
||||||
| IChangeRequestCreateMilestoneProgression
|
| IChangeRequestCreateMilestoneProgression
|
||||||
| IChangeRequestUpdateMilestoneProgression;
|
| IChangeRequestUpdateMilestoneProgression
|
||||||
|
| IChangeRequestDeleteMilestoneProgression;
|
||||||
|
|
||||||
export type ISegmentChange =
|
export type ISegmentChange =
|
||||||
| IChangeRequestUpdateSegment
|
| IChangeRequestUpdateSegment
|
||||||
@ -288,13 +296,23 @@ type ChangeRequestStartMilestone = {
|
|||||||
snapshot?: IReleasePlan;
|
snapshot?: IReleasePlan;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ChangeRequestCreateMilestoneProgression = CreateMilestoneProgressionSchema;
|
type ChangeRequestCreateMilestoneProgression = CreateMilestoneProgressionSchema & {
|
||||||
|
snapshot?: IReleasePlan;
|
||||||
|
};
|
||||||
|
|
||||||
type ChangeRequestUpdateMilestoneProgression =
|
type ChangeRequestUpdateMilestoneProgression =
|
||||||
UpdateMilestoneProgressionSchema & {
|
UpdateMilestoneProgressionSchema & {
|
||||||
sourceMilestoneId: string;
|
sourceMilestoneId?: string;
|
||||||
|
sourceMilestone?: string; // Backward compatibility for existing change requests
|
||||||
|
snapshot?: IReleasePlan;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ChangeRequestDeleteMilestoneProgression = {
|
||||||
|
sourceMilestoneId?: string;
|
||||||
|
sourceMilestone?: string; // Backward compatibility for existing change requests
|
||||||
|
snapshot?: IReleasePlan;
|
||||||
|
};
|
||||||
|
|
||||||
export type ChangeRequestAddStrategy = Pick<
|
export type ChangeRequestAddStrategy = Pick<
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
| 'parameters'
|
| 'parameters'
|
||||||
@ -334,4 +352,5 @@ export type ChangeRequestAction =
|
|||||||
| 'deleteReleasePlan'
|
| 'deleteReleasePlan'
|
||||||
| 'startMilestone'
|
| 'startMilestone'
|
||||||
| 'createMilestoneProgression'
|
| 'createMilestoneProgression'
|
||||||
| 'updateMilestoneProgression';
|
| 'updateMilestoneProgression'
|
||||||
|
| 'deleteMilestoneProgression';
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import type {
|
|||||||
CreateMilestoneProgressionSchema,
|
CreateMilestoneProgressionSchema,
|
||||||
UpdateMilestoneProgressionSchema,
|
UpdateMilestoneProgressionSchema,
|
||||||
} from 'openapi';
|
} from 'openapi';
|
||||||
|
import { ReleasePlanProvider } from './ReleasePlanContext.tsx';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
@ -140,8 +141,41 @@ 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 cr of pendingChangeRequests) {
|
||||||
|
if (cr.environment !== environment) continue;
|
||||||
|
|
||||||
|
const feature = cr.features.find((f) => f.name === featureName);
|
||||||
|
if (!feature) continue;
|
||||||
|
|
||||||
|
// Look for update or delete progression changes
|
||||||
|
const change = feature.changes.find(
|
||||||
|
(c: any) =>
|
||||||
|
(c.action === 'updateMilestoneProgression' &&
|
||||||
|
(c.payload.sourceMilestoneId === sourceMilestoneId ||
|
||||||
|
c.payload.sourceMilestone === sourceMilestoneId)) ||
|
||||||
|
(c.action === 'deleteMilestoneProgression' &&
|
||||||
|
(c.payload.sourceMilestoneId === sourceMilestoneId ||
|
||||||
|
c.payload.sourceMilestone === sourceMilestoneId)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (change) {
|
||||||
|
return {
|
||||||
|
action: change.action,
|
||||||
|
payload: change.payload,
|
||||||
|
changeRequestId: cr.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 +215,6 @@ export const ReleasePlan = ({
|
|||||||
action: 'createMilestoneProgression',
|
action: 'createMilestoneProgression',
|
||||||
payload: changeRequestAction.payload,
|
payload: changeRequestAction.payload,
|
||||||
});
|
});
|
||||||
setProgressionFormOpenIndex(null);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'updateMilestoneProgression':
|
case 'updateMilestoneProgression':
|
||||||
@ -214,6 +247,7 @@ export const ReleasePlan = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setChangeRequestAction(null);
|
setChangeRequestAction(null);
|
||||||
|
setProgressionFormOpenIndex(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmRemoveReleasePlan = () => {
|
const confirmRemoveReleasePlan = () => {
|
||||||
@ -364,34 +398,37 @@ export const ReleasePlan = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<ReleasePlanProvider
|
||||||
<StyledHeader>
|
getPendingProgressionChange={getPendingProgressionChange}
|
||||||
<StyledHeaderGroup>
|
>
|
||||||
<StyledHeaderTitleLabel>
|
<StyledContainer>
|
||||||
Release plan:{' '}
|
<StyledHeader>
|
||||||
</StyledHeaderTitleLabel>
|
<StyledHeaderGroup>
|
||||||
<StyledHeaderTitle>{name}</StyledHeaderTitle>
|
<StyledHeaderTitleLabel>
|
||||||
<StyledHeaderDescription>
|
Release plan:{' '}
|
||||||
<Truncator lines={2} title={description}>
|
</StyledHeaderTitleLabel>
|
||||||
{description}
|
<StyledHeaderTitle>{name}</StyledHeaderTitle>
|
||||||
</Truncator>
|
<StyledHeaderDescription>
|
||||||
</StyledHeaderDescription>
|
<Truncator lines={2} title={description}>
|
||||||
</StyledHeaderGroup>
|
{description}
|
||||||
{!readonly && (
|
</Truncator>
|
||||||
<PermissionIconButton
|
</StyledHeaderDescription>
|
||||||
onClick={confirmRemoveReleasePlan}
|
</StyledHeaderGroup>
|
||||||
permission={DELETE_FEATURE_STRATEGY}
|
{!readonly && (
|
||||||
environmentId={environment}
|
<PermissionIconButton
|
||||||
projectId={projectId}
|
onClick={confirmRemoveReleasePlan}
|
||||||
tooltipProps={{
|
permission={DELETE_FEATURE_STRATEGY}
|
||||||
title: 'Remove release plan',
|
environmentId={environment}
|
||||||
}}
|
projectId={projectId}
|
||||||
>
|
tooltipProps={{
|
||||||
<Delete />
|
title: 'Remove release plan',
|
||||||
</PermissionIconButton>
|
}}
|
||||||
)}
|
>
|
||||||
</StyledHeader>
|
<Delete />
|
||||||
<StyledBody>
|
</PermissionIconButton>
|
||||||
|
)}
|
||||||
|
</StyledHeader>
|
||||||
|
<StyledBody>
|
||||||
{milestones.map((milestone, index) => {
|
{milestones.map((milestone, index) => {
|
||||||
const isNotLastMilestone = index < milestones.length - 1;
|
const isNotLastMilestone = index < milestones.length - 1;
|
||||||
const isProgressionFormOpen =
|
const isProgressionFormOpen =
|
||||||
@ -493,5 +530,6 @@ export const ReleasePlan = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
</ReleasePlanProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
import { createContext, useContext, type ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface PendingProgressionChange {
|
||||||
|
action: string;
|
||||||
|
payload: any;
|
||||||
|
changeRequestId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReleasePlanContextType {
|
||||||
|
getPendingProgressionChange: (
|
||||||
|
sourceMilestoneId: string,
|
||||||
|
) => PendingProgressionChange | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReleasePlanContext = createContext<ReleasePlanContextType | null>(null);
|
||||||
|
|
||||||
|
export const useReleasePlanContext = () => {
|
||||||
|
const context = useContext(ReleasePlanContext);
|
||||||
|
if (!context) {
|
||||||
|
// Return a fallback context that returns null for all milestone IDs
|
||||||
|
// This allows the component to work without the provider (e.g., in change request views)
|
||||||
|
return {
|
||||||
|
getPendingProgressionChange: () => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ReleasePlanProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
getPendingProgressionChange: (
|
||||||
|
sourceMilestoneId: string,
|
||||||
|
) => PendingProgressionChange | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReleasePlanProvider = ({
|
||||||
|
children,
|
||||||
|
getPendingProgressionChange,
|
||||||
|
}: ReleasePlanProviderProps) => {
|
||||||
|
return (
|
||||||
|
<ReleasePlanContext.Provider value={{ getPendingProgressionChange }}>
|
||||||
|
{children}
|
||||||
|
</ReleasePlanContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -3,6 +3,8 @@ import { Button, styled } from '@mui/material';
|
|||||||
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
|
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
|
||||||
import { MilestoneTransitionDisplay } from './MilestoneTransitionDisplay.tsx';
|
import { MilestoneTransitionDisplay } from './MilestoneTransitionDisplay.tsx';
|
||||||
import type { UpdateMilestoneProgressionSchema } from 'openapi';
|
import type { UpdateMilestoneProgressionSchema } from 'openapi';
|
||||||
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
|
import { useReleasePlanContext } from '../ReleasePlanContext.tsx';
|
||||||
|
|
||||||
const StyledAutomationContainer = styled('div', {
|
const StyledAutomationContainer = styled('div', {
|
||||||
shouldForwardProp: (prop) => prop !== 'status',
|
shouldForwardProp: (prop) => prop !== 'status',
|
||||||
@ -51,6 +53,12 @@ const StyledAddAutomationButton = styled(Button)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const StyledAddAutomationContainer = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
interface IMilestoneAutomationSectionProps {
|
interface IMilestoneAutomationSectionProps {
|
||||||
showAutomation?: boolean;
|
showAutomation?: boolean;
|
||||||
status?: MilestoneStatus;
|
status?: MilestoneStatus;
|
||||||
@ -87,15 +95,25 @@ export const MilestoneAutomationSection = ({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
onUpdateChangeRequestSubmit,
|
onUpdateChangeRequestSubmit,
|
||||||
}: IMilestoneAutomationSectionProps) => {
|
}: IMilestoneAutomationSectionProps) => {
|
||||||
|
const { getPendingProgressionChange } = useReleasePlanContext();
|
||||||
|
const pendingProgressionChange = getPendingProgressionChange(sourceMilestoneId);
|
||||||
|
|
||||||
|
const hasPendingCreate = pendingProgressionChange?.action === 'createMilestoneProgression';
|
||||||
|
|
||||||
|
// For pending create changes, use the transition condition from the pending change
|
||||||
|
const effectiveTransitionCondition = hasPendingCreate && pendingProgressionChange?.payload?.transitionCondition
|
||||||
|
? pendingProgressionChange.payload.transitionCondition
|
||||||
|
: transitionCondition;
|
||||||
|
|
||||||
if (!showAutomation) return null;
|
if (!showAutomation) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAutomationContainer status={status}>
|
<StyledAutomationContainer status={status}>
|
||||||
{automationForm ? (
|
{automationForm ? (
|
||||||
automationForm
|
automationForm
|
||||||
) : transitionCondition ? (
|
) : effectiveTransitionCondition ? (
|
||||||
<MilestoneTransitionDisplay
|
<MilestoneTransitionDisplay
|
||||||
intervalMinutes={transitionCondition.intervalMinutes}
|
intervalMinutes={effectiveTransitionCondition.intervalMinutes}
|
||||||
onDelete={onDeleteAutomation!}
|
onDelete={onDeleteAutomation!}
|
||||||
milestoneName={milestoneName}
|
milestoneName={milestoneName}
|
||||||
status={status}
|
status={status}
|
||||||
@ -107,13 +125,20 @@ export const MilestoneAutomationSection = ({
|
|||||||
onChangeRequestSubmit={onUpdateChangeRequestSubmit}
|
onChangeRequestSubmit={onUpdateChangeRequestSubmit}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<StyledAddAutomationButton
|
<StyledAddAutomationContainer>
|
||||||
onClick={onAddAutomation}
|
<StyledAddAutomationButton
|
||||||
color='primary'
|
onClick={onAddAutomation}
|
||||||
startIcon={<Add />}
|
color='primary'
|
||||||
>
|
startIcon={<Add />}
|
||||||
Add automation
|
>
|
||||||
</StyledAddAutomationButton>
|
Add automation
|
||||||
|
</StyledAddAutomationButton>
|
||||||
|
{hasPendingCreate && (
|
||||||
|
<Badge color='warning'>
|
||||||
|
Modified in draft
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</StyledAddAutomationContainer>
|
||||||
)}
|
)}
|
||||||
</StyledAutomationContainer>
|
</StyledAutomationContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import BoltIcon from '@mui/icons-material/Bolt';
|
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 { Badge } from 'component/common/Badge/Badge';
|
||||||
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
|
import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
|
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
} from '../hooks/useMilestoneProgressionForm.js';
|
} from '../hooks/useMilestoneProgressionForm.js';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import type { UpdateMilestoneProgressionSchema } from 'openapi';
|
import type { UpdateMilestoneProgressionSchema } from 'openapi';
|
||||||
|
import { useReleasePlanContext } from '../ReleasePlanContext.tsx';
|
||||||
|
|
||||||
const StyledDisplayContainer = styled('div')(({ theme }) => ({
|
const StyledDisplayContainer = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -90,6 +92,8 @@ export const MilestoneTransitionDisplay = ({
|
|||||||
const { updateMilestoneProgression } = useMilestoneProgressionsApi();
|
const { updateMilestoneProgression } = useMilestoneProgressionsApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const { getPendingProgressionChange } = useReleasePlanContext();
|
||||||
|
const pendingProgressionChange = getPendingProgressionChange(sourceMilestoneId);
|
||||||
|
|
||||||
const initial = getTimeValueAndUnitFromMinutes(intervalMinutes);
|
const initial = getTimeValueAndUnitFromMinutes(intervalMinutes);
|
||||||
const form = useMilestoneProgressionForm(
|
const form = useMilestoneProgressionForm(
|
||||||
@ -105,6 +109,13 @@ export const MilestoneTransitionDisplay = ({
|
|||||||
const currentIntervalMinutes = form.getIntervalMinutes();
|
const currentIntervalMinutes = form.getIntervalMinutes();
|
||||||
const hasChanged = currentIntervalMinutes !== intervalMinutes;
|
const hasChanged = currentIntervalMinutes !== intervalMinutes;
|
||||||
|
|
||||||
|
// Check if there's a pending change request for this progression
|
||||||
|
const hasPendingUpdate =
|
||||||
|
pendingProgressionChange?.action === 'updateMilestoneProgression';
|
||||||
|
const hasPendingDelete =
|
||||||
|
pendingProgressionChange?.action === 'deleteMilestoneProgression';
|
||||||
|
const showDraftBadge = hasPendingUpdate || hasPendingDelete;
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (isSubmitting || !hasChanged) return;
|
if (isSubmitting || !hasChanged) return;
|
||||||
|
|
||||||
@ -116,6 +127,8 @@ export const MilestoneTransitionDisplay = ({
|
|||||||
|
|
||||||
if (isChangeRequestConfigured(environment) && onChangeRequestSubmit) {
|
if (isChangeRequestConfigured(environment) && onChangeRequestSubmit) {
|
||||||
onChangeRequestSubmit(sourceMilestoneId, payload);
|
onChangeRequestSubmit(sourceMilestoneId, payload);
|
||||||
|
// Reset the form after submitting to change request
|
||||||
|
handleReset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +196,11 @@ export const MilestoneTransitionDisplay = ({
|
|||||||
{isSubmitting ? 'Saving...' : 'Save'}
|
{isSubmitting ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{showDraftBadge && (
|
||||||
|
<Badge color={hasPendingDelete ? 'error' : 'warning'}>
|
||||||
|
{hasPendingDelete ? 'Deleted in draft' : 'Modified in draft'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
size='small'
|
size='small'
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user