From 154dc6f5eb4da9ca24462cf9358275993787c103 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Thu, 16 Oct 2025 11:57:28 +0200 Subject: [PATCH] feat: add change request support for milestone progressions (#10814) --- .../changeRequest/changeRequest.types.ts | 22 ++++-- ...ilestoneProgressionChangeRequestDialog.tsx | 70 +++++++++++++++++++ .../MilestoneProgressionForm.tsx | 32 +++++++-- .../ReleasePlan/ReleasePlan.tsx | 54 ++++++++++++++ .../useChangeRequestApi.ts | 3 +- 5 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts index 33eb9bb78c..04646edc5b 100644 --- a/frontend/src/component/changeRequest/changeRequest.types.ts +++ b/frontend/src/component/changeRequest/changeRequest.types.ts @@ -2,7 +2,10 @@ import type { IFeatureVariant } from 'interfaces/featureToggle'; import type { ISegment } from 'interfaces/segment'; import type { IFeatureStrategy } from '../../interfaces/strategy.js'; import type { IUser } from '../../interfaces/user.js'; -import type { SetStrategySortOrderSchema } from 'openapi'; +import type { + SetStrategySortOrderSchema, + CreateMilestoneProgressionSchema, +} from 'openapi'; import type { IReleasePlan } from 'interfaces/releasePlans'; type BaseChangeRequest = { @@ -131,7 +134,8 @@ type ChangeRequestPayload = | ChangeRequestAddDependency | ChangeRequestAddReleasePlan | ChangeRequestDeleteReleasePlan - | ChangeRequestStartMilestone; + | ChangeRequestStartMilestone + | ChangeRequestCreateMilestoneProgression; export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase { action: 'addStrategy'; @@ -188,6 +192,12 @@ export interface IChangeRequestStartMilestone extends IChangeRequestChangeBase { payload: ChangeRequestStartMilestone; } +export interface IChangeRequestCreateMilestoneProgression + extends IChangeRequestChangeBase { + action: 'createMilestoneProgression'; + payload: ChangeRequestCreateMilestoneProgression; +} + export interface IChangeRequestReorderStrategy extends IChangeRequestChangeBase { action: 'reorderStrategy'; @@ -235,7 +245,8 @@ export type IFeatureChange = | IChangeRequestDeleteDependency | IChangeRequestAddReleasePlan | IChangeRequestDeleteReleasePlan - | IChangeRequestStartMilestone; + | IChangeRequestStartMilestone + | IChangeRequestCreateMilestoneProgression; export type ISegmentChange = | IChangeRequestUpdateSegment @@ -268,6 +279,8 @@ type ChangeRequestStartMilestone = { snapshot?: IReleasePlan; }; +type ChangeRequestCreateMilestoneProgression = CreateMilestoneProgressionSchema; + export type ChangeRequestAddStrategy = Pick< IFeatureStrategy, | 'parameters' @@ -305,4 +318,5 @@ export type ChangeRequestAction = | 'deleteDependency' | 'addReleasePlan' | 'deleteReleasePlan' - | 'startMilestone'; + | 'startMilestone' + | 'createMilestoneProgression'; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx new file mode 100644 index 0000000000..eeb301f306 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx @@ -0,0 +1,70 @@ +import { Dialogue } from 'component/common/Dialogue/Dialogue'; +import { styled, Button } from '@mui/material'; +import type { IReleasePlan } from 'interfaces/releasePlans'; +import type { CreateMilestoneProgressionSchema } from 'openapi'; +import { getTimeValueAndUnitFromMinutes } from '../hooks/useMilestoneProgressionForm.js'; + +const StyledBoldSpan = styled('span')(({ theme }) => ({ + fontWeight: theme.typography.fontWeightBold, +})); + +interface ICreateMilestoneProgressionChangeRequestDialogProps { + environmentId: string; + releasePlan: IReleasePlan; + payload: CreateMilestoneProgressionSchema; + isOpen: boolean; + onConfirm: () => Promise; + onClosing: () => void; +} + +export const CreateMilestoneProgressionChangeRequestDialog = ({ + environmentId, + releasePlan, + payload, + isOpen, + onConfirm, + onClosing, +}: ICreateMilestoneProgressionChangeRequestDialogProps) => { + if (!payload) { + return null; + } + + const sourceMilestone = releasePlan.milestones.find( + (milestone) => milestone.id === payload.sourceMilestone, + ); + const targetMilestone = releasePlan.milestones.find( + (milestone) => milestone.id === payload.targetMilestone, + ); + + const { value, unit } = getTimeValueAndUnitFromMinutes( + payload.transitionCondition.intervalMinutes, + ); + const timeInterval = `${value} ${unit}`; + + return ( + + Add suggestion to draft + + } + > +

+ Create automation to proceed from{' '} + {sourceMilestone?.name} to{' '} + {targetMilestone?.name} after{' '} + {timeInterval} in{' '} + {environmentId} +

+
+ ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/MilestoneProgressionForm/MilestoneProgressionForm.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/MilestoneProgressionForm/MilestoneProgressionForm.tsx index bdee2828c2..2ae484db9b 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/MilestoneProgressionForm/MilestoneProgressionForm.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/MilestoneProgressionForm/MilestoneProgressionForm.tsx @@ -6,6 +6,8 @@ import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgr import useToast from 'hooks/useToast'; import { formatUnknownError } from 'utils/formatUnknownError'; import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx'; +import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; +import type { CreateMilestoneProgressionSchema } from 'openapi'; const StyledFormContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -63,6 +65,9 @@ interface IMilestoneProgressionFormProps { featureName: string; onSave: () => void; onCancel: () => void; + onChangeRequestSubmit?: ( + progressionPayload: CreateMilestoneProgressionSchema, + ) => void; } export const MilestoneProgressionForm = ({ @@ -73,6 +78,7 @@ export const MilestoneProgressionForm = ({ featureName, onSave, onCancel, + onChangeRequestSubmit, }: IMilestoneProgressionFormProps) => { const form = useMilestoneProgressionForm( sourceMilestoneId, @@ -80,16 +86,16 @@ export const MilestoneProgressionForm = ({ ); const { createMilestoneProgression } = useMilestoneProgressionsApi(); const { setToastData, setToastApiError } = useToast(); + const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const [isSubmitting, setIsSubmitting] = useState(false); - const handleSubmit = async () => { - if (isSubmitting) return; - - if (!form.validate()) { - return; - } + const handleChangeRequestSubmit = () => { + const progressionPayload = form.getProgressionPayload(); + onChangeRequestSubmit?.(progressionPayload); + }; + const handleDirectSubmit = async () => { setIsSubmitting(true); try { await createMilestoneProgression( @@ -110,6 +116,20 @@ export const MilestoneProgressionForm = ({ } }; + const handleSubmit = async () => { + if (isSubmitting) return; + + if (!form.validate()) { + return; + } + + if (isChangeRequestConfigured(environment) && onChangeRequestSubmit) { + handleChangeRequestSubmit(); + } else { + await handleDirectSubmit(); + } + }; + const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { event.preventDefault(); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx index b826df3eb7..99b1a89f58 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx @@ -20,12 +20,14 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog.tsx'; import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog.tsx'; +import { CreateMilestoneProgressionChangeRequestDialog } from './ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { Truncator } from 'component/common/Truncator/Truncator'; 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'; const StyledContainer = styled('div')(({ theme }) => ({ padding: theme.spacing(2), @@ -123,10 +125,16 @@ export const ReleasePlan = ({ changeRequestDialogStartMilestoneOpen, setChangeRequestDialogStartMilestoneOpen, ] = useState(false); + const [ + changeRequestDialogCreateProgressionOpen, + setChangeRequestDialogCreateProgressionOpen, + ] = useState(false); const [ milestoneForChangeRequestDialog, setMilestoneForChangeRequestDialog, ] = useState(); + const [progressionDataForCR, setProgressionDataForCR] = + useState(null); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { addChange } = useChangeRequestApi(); const { refetch: refetchChangeRequests } = @@ -178,6 +186,27 @@ export const ReleasePlan = ({ setChangeRequestDialogStartMilestoneOpen(false); }; + const onAddCreateProgressionChangesConfirm = async () => { + if (!progressionDataForCR) return; + + await addChange(projectId, environment, { + feature: featureName, + action: 'createMilestoneProgression', + payload: progressionDataForCR, + }); + + await refetchChangeRequests(); + + setToastData({ + type: 'success', + text: 'Added to draft', + }); + + setChangeRequestDialogCreateProgressionOpen(false); + setProgressionFormOpenIndex(null); + setProgressionDataForCR(null); + }; + const confirmRemoveReleasePlan = () => { if (isChangeRequestConfigured(environment)) { setChangeRequestDialogRemoveOpen(true); @@ -254,6 +283,13 @@ export const ReleasePlan = ({ setProgressionFormOpenIndex(null); }; + const handleProgressionChangeRequestSubmit = ( + payload: CreateMilestoneProgressionSchema, + ) => { + setProgressionDataForCR(payload); + setChangeRequestDialogCreateProgressionOpen(true); + }; + const handleDeleteProgression = (milestone: IReleasePlanMilestone) => { setMilestoneToDeleteProgression(milestone); }; @@ -367,6 +403,11 @@ export const ReleasePlan = ({ featureName={featureName} onSave={handleProgressionSave} onCancel={handleProgressionCancel} + onChangeRequestSubmit={(payload) => + handleProgressionChangeRequestSubmit( + payload, + ) + } /> ) : undefined } @@ -417,6 +458,19 @@ export const ReleasePlan = ({ releasePlan={plan} milestone={milestoneForChangeRequestDialog} /> + {progressionDataForCR && ( + { + setChangeRequestDialogCreateProgressionOpen(false); + setProgressionDataForCR(null); + }} + releasePlan={plan} + payload={progressionDataForCR} + /> + )} {milestoneToDeleteProgression && (