From 795b6741337d248759ff7901175ff0e0d7e978b7 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Thu, 16 Oct 2025 16:52:02 +0200 Subject: [PATCH] feat: add change request support for updating milestone progressions (#10819) --- .../changeRequest/changeRequest.types.ts | 21 +++++++++-- .../ReleasePlanChangeRequestDialog.tsx | 31 +++++++++++++++- .../ReleasePlan/ReleasePlan.tsx | 35 ++++++++++++++++++- .../MilestoneAutomationSection.tsx | 7 ++++ .../MilestoneTransitionDisplay.tsx | 25 ++++++++++--- .../ReleasePlanMilestone.tsx | 12 +++++++ .../useChangeRequestApi.ts | 3 +- 7 files changed, 123 insertions(+), 11 deletions(-) diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts index 04646edc5b..e12f40084c 100644 --- a/frontend/src/component/changeRequest/changeRequest.types.ts +++ b/frontend/src/component/changeRequest/changeRequest.types.ts @@ -5,6 +5,7 @@ import type { IUser } from '../../interfaces/user.js'; import type { SetStrategySortOrderSchema, CreateMilestoneProgressionSchema, + UpdateMilestoneProgressionSchema, } from 'openapi'; import type { IReleasePlan } from 'interfaces/releasePlans'; @@ -135,7 +136,8 @@ type ChangeRequestPayload = | ChangeRequestAddReleasePlan | ChangeRequestDeleteReleasePlan | ChangeRequestStartMilestone - | ChangeRequestCreateMilestoneProgression; + | ChangeRequestCreateMilestoneProgression + | ChangeRequestUpdateMilestoneProgression; export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase { action: 'addStrategy'; @@ -198,6 +200,12 @@ export interface IChangeRequestCreateMilestoneProgression payload: ChangeRequestCreateMilestoneProgression; } +export interface IChangeRequestUpdateMilestoneProgression + extends IChangeRequestChangeBase { + action: 'updateMilestoneProgression'; + payload: ChangeRequestUpdateMilestoneProgression; +} + export interface IChangeRequestReorderStrategy extends IChangeRequestChangeBase { action: 'reorderStrategy'; @@ -246,7 +254,8 @@ export type IFeatureChange = | IChangeRequestAddReleasePlan | IChangeRequestDeleteReleasePlan | IChangeRequestStartMilestone - | IChangeRequestCreateMilestoneProgression; + | IChangeRequestCreateMilestoneProgression + | IChangeRequestUpdateMilestoneProgression; export type ISegmentChange = | IChangeRequestUpdateSegment @@ -281,6 +290,11 @@ type ChangeRequestStartMilestone = { type ChangeRequestCreateMilestoneProgression = CreateMilestoneProgressionSchema; +type ChangeRequestUpdateMilestoneProgression = + UpdateMilestoneProgressionSchema & { + sourceMilestoneId: string; + }; + export type ChangeRequestAddStrategy = Pick< IFeatureStrategy, | 'parameters' @@ -319,4 +333,5 @@ export type ChangeRequestAction = | 'addReleasePlan' | 'deleteReleasePlan' | 'startMilestone' - | 'createMilestoneProgression'; + | 'createMilestoneProgression' + | 'updateMilestoneProgression'; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/ReleasePlanChangeRequestDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/ReleasePlanChangeRequestDialog.tsx index 50849ab4f2..5f6f2fe018 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/ReleasePlanChangeRequestDialog.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/ReleasePlanChangeRequestDialog.tsx @@ -4,7 +4,10 @@ import type { IReleasePlan, IReleasePlanMilestone, } from 'interfaces/releasePlans'; -import type { CreateMilestoneProgressionSchema } from 'openapi'; +import type { + CreateMilestoneProgressionSchema, + UpdateMilestoneProgressionSchema, +} from 'openapi'; import { getTimeValueAndUnitFromMinutes } from '../hooks/useMilestoneProgressionForm.js'; const StyledBoldSpan = styled('span')(({ theme }) => ({ @@ -23,6 +26,11 @@ type ChangeRequestAction = | { type: 'createMilestoneProgression'; payload: CreateMilestoneProgressionSchema; + } + | { + type: 'updateMilestoneProgression'; + sourceMilestoneId: string; + payload: UpdateMilestoneProgressionSchema; }; interface IReleasePlanChangeRequestDialogProps { @@ -105,6 +113,27 @@ export const ReleasePlanChangeRequestDialog = ({

); } + + case 'updateMilestoneProgression': { + const milestone = releasePlan.milestones.find( + (milestone) => milestone.id === action.sourceMilestoneId, + ); + + const { value, unit } = getTimeValueAndUnitFromMinutes( + action.payload.transitionCondition.intervalMinutes, + ); + const timeInterval = `${value} ${unit}`; + + return ( +

+ Update automation for{' '} + {milestone?.name} to + proceed after{' '} + {timeInterval} in{' '} + {environmentId} +

+ ); + } } }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx index 03a83a3e37..f84d4368e5 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx @@ -25,7 +25,10 @@ import { useUiFlag } from 'hooks/useUiFlag'; import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx'; import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi'; import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx'; -import type { CreateMilestoneProgressionSchema } from 'openapi'; +import type { + CreateMilestoneProgressionSchema, + UpdateMilestoneProgressionSchema, +} from 'openapi'; const StyledContainer = styled('div')(({ theme }) => ({ padding: theme.spacing(2), @@ -124,6 +127,11 @@ export const ReleasePlan = ({ type: 'createMilestoneProgression'; payload: CreateMilestoneProgressionSchema; } + | { + type: 'updateMilestoneProgression'; + sourceMilestoneId: string; + payload: UpdateMilestoneProgressionSchema; + } | null >(null); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); @@ -171,6 +179,17 @@ export const ReleasePlan = ({ }); setProgressionFormOpenIndex(null); break; + + case 'updateMilestoneProgression': + await addChange(projectId, environment, { + feature: featureName, + action: 'updateMilestoneProgression', + payload: { + sourceMilestone: changeRequestAction.sourceMilestoneId, + ...changeRequestAction.payload, + }, + }); + break; } await refetchChangeRequests(); @@ -273,6 +292,17 @@ export const ReleasePlan = ({ }); }; + const handleUpdateProgressionChangeRequestSubmit = ( + sourceMilestoneId: string, + payload: UpdateMilestoneProgressionSchema, + ) => { + setChangeRequestAction({ + type: 'updateMilestoneProgression', + sourceMilestoneId, + payload, + }); + }; + const handleDeleteProgression = (milestone: IReleasePlanMilestone) => { setMilestoneToDeleteProgression(milestone); }; @@ -398,6 +428,9 @@ export const ReleasePlan = ({ environment={environment} featureName={featureName} onUpdate={refetch} + onUpdateChangeRequestSubmit={ + handleUpdateProgressionChangeRequestSubmit + } allMilestones={milestones} activeMilestoneId={activeMilestoneId} /> diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx index d3da89530c..1e13a5b9da 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/MilestoneAutomationSection.tsx @@ -2,6 +2,7 @@ import Add from '@mui/icons-material/Add'; import { Button, styled } from '@mui/material'; import type { MilestoneStatus } from './ReleasePlanMilestoneStatus.tsx'; import { MilestoneTransitionDisplay } from './MilestoneTransitionDisplay.tsx'; +import type { UpdateMilestoneProgressionSchema } from 'openapi'; const StyledAutomationContainer = styled('div', { shouldForwardProp: (prop) => prop !== 'status', @@ -65,6 +66,10 @@ interface IMilestoneAutomationSectionProps { featureName: string; sourceMilestoneId: string; onUpdate: () => void; + onUpdateChangeRequestSubmit?: ( + sourceMilestoneId: string, + payload: UpdateMilestoneProgressionSchema, + ) => void; } export const MilestoneAutomationSection = ({ @@ -80,6 +85,7 @@ export const MilestoneAutomationSection = ({ featureName, sourceMilestoneId, onUpdate, + onUpdateChangeRequestSubmit, }: IMilestoneAutomationSectionProps) => { if (!showAutomation) return null; @@ -98,6 +104,7 @@ export const MilestoneAutomationSection = ({ featureName={featureName} sourceMilestoneId={sourceMilestoneId} onUpdate={onUpdate} + onChangeRequestSubmit={onUpdateChangeRequestSubmit} /> ) : ( ({ display: 'flex', @@ -67,6 +69,10 @@ interface IMilestoneTransitionDisplayProps { featureName: string; sourceMilestoneId: string; onUpdate: () => void; + onChangeRequestSubmit?: ( + sourceMilestoneId: string, + payload: UpdateMilestoneProgressionSchema, + ) => void; } export const MilestoneTransitionDisplay = ({ @@ -79,9 +85,11 @@ export const MilestoneTransitionDisplay = ({ featureName, sourceMilestoneId, onUpdate, + onChangeRequestSubmit, }: IMilestoneTransitionDisplayProps) => { const { updateMilestoneProgression } = useMilestoneProgressionsApi(); const { setToastData, setToastApiError } = useToast(); + const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const initial = getTimeValueAndUnitFromMinutes(intervalMinutes); const form = useMilestoneProgressionForm( @@ -100,6 +108,17 @@ export const MilestoneTransitionDisplay = ({ const handleSave = async () => { if (isSubmitting || !hasChanged) return; + const payload: UpdateMilestoneProgressionSchema = { + transitionCondition: { + intervalMinutes: currentIntervalMinutes, + }, + }; + + if (isChangeRequestConfigured(environment) && onChangeRequestSubmit) { + onChangeRequestSubmit(sourceMilestoneId, payload); + return; + } + setIsSubmitting(true); try { await updateMilestoneProgression( @@ -107,11 +126,7 @@ export const MilestoneTransitionDisplay = ({ environment, featureName, sourceMilestoneId, - { - transitionCondition: { - intervalMinutes: currentIntervalMinutes, - }, - }, + payload, ); setToastData({ type: 'success', diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx index 4130e1c75c..9e467ce69c 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanMilestone/ReleasePlanMilestone.tsx @@ -19,6 +19,7 @@ import { StrategyList } from 'component/common/StrategyList/StrategyList'; import { StrategyListItem } from 'component/common/StrategyList/StrategyListItem'; import { MilestoneAutomationSection } from './MilestoneAutomationSection.tsx'; import { formatDateYMDHMS } from 'utils/formatDate'; +import type { UpdateMilestoneProgressionSchema } from 'openapi'; const StyledAccordion = styled(Accordion, { shouldForwardProp: (prop) => prop !== 'status' && prop !== 'hasAutomation', @@ -107,6 +108,10 @@ interface IReleasePlanMilestoneProps { environment?: string; featureName?: string; onUpdate?: () => void; + onUpdateChangeRequestSubmit?: ( + sourceMilestoneId: string, + payload: UpdateMilestoneProgressionSchema, + ) => void; allMilestones: IReleasePlanMilestone[]; activeMilestoneId?: string; } @@ -124,6 +129,7 @@ export const ReleasePlanMilestone = ({ environment, featureName, onUpdate, + onUpdateChangeRequestSubmit, allMilestones, activeMilestoneId, }: IReleasePlanMilestoneProps) => { @@ -193,6 +199,9 @@ export const ReleasePlanMilestone = ({ featureName={featureName} sourceMilestoneId={milestone.id} onUpdate={onUpdate} + onUpdateChangeRequestSubmit={ + onUpdateChangeRequestSubmit + } /> )} @@ -283,6 +292,9 @@ export const ReleasePlanMilestone = ({ featureName={featureName} sourceMilestoneId={milestone.id} onUpdate={onUpdate} + onUpdateChangeRequestSubmit={ + onUpdateChangeRequestSubmit + } /> )} diff --git a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts index 65b9ce1729..6430ba9aa4 100644 --- a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts +++ b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts @@ -22,7 +22,8 @@ export interface IChangeSchema { | 'addReleasePlan' | 'deleteReleasePlan' | 'startMilestone' - | 'createMilestoneProgression'; + | 'createMilestoneProgression' + | 'updateMilestoneProgression'; payload: string | boolean | object | number | undefined; }