From df67c041fc018e46db15afd89aae78a7a717ed5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Thu, 2 Oct 2025 15:48:27 +0100 Subject: [PATCH] chore: new confirmation dialog for replacing release plans (#10720) https://linear.app/unleash/issue/2-3931/add-a-confirmation-dialog-when-replacing-existing-release-plan Adds a confirmation dialog when replacing an already active release plan. image --- .../FeatureStrategyMenu.tsx | 75 +++++++++++++------ .../ReleasePlanConfirmationDialog.tsx | 35 +++++++++ .../ReleasePlanPreview.tsx | 35 +++++---- 3 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanConfirmationDialog.tsx rename frontend/src/component/feature/{FeatureView/FeatureOverview/ReleasePlan => FeatureStrategy/FeatureStrategyMenu}/ReleasePlanPreview.tsx (81%) diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx index 007ad90b27..8818aedd60 100644 --- a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/FeatureStrategyMenu.tsx @@ -21,12 +21,13 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { formatUnknownError } from 'utils/formatUnknownError'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { LegacyReleasePlanReviewDialog } from 'component/feature/FeatureView/FeatureOverview/ReleasePlan/LegacyReleasePlanReviewDialog.tsx'; -import { ReleasePlanPreview } from '../../FeatureView/FeatureOverview/ReleasePlan/ReleasePlanPreview.tsx'; +import { ReleasePlanPreview } from './ReleasePlanPreview.tsx'; import { FeatureStrategyMenuCards, type StrategyFilterValue, } from './FeatureStrategyMenuCards/FeatureStrategyMenuCards.tsx'; import { useUiFlag } from 'hooks/useUiFlag.ts'; +import { ReleasePlanConfirmationDialog } from './ReleasePlanConfirmationDialog.tsx'; interface IFeatureStrategyMenuProps { label: string; @@ -78,6 +79,8 @@ export const FeatureStrategyMenu = ({ useState(); const [addReleasePlanOpen, setAddReleasePlanOpen] = useState(false); const [releasePlanPreview, setReleasePlanPreview] = useState(false); + const [addReleasePlanConfirmationOpen, setAddReleasePlanConfirmationOpen] = + useState(false); const dialogId = isStrategyMenuDialogOpen ? 'FeatureStrategyMenuDialog' : undefined; @@ -86,13 +89,19 @@ export const FeatureStrategyMenu = ({ const { addChange } = useChangeRequestApi(); const { refetch: refetchChangeRequests } = usePendingChangeRequests(projectId); - const { refetch } = useReleasePlans(projectId, featureId, environmentId); + const { refetch, releasePlans } = useReleasePlans( + projectId, + featureId, + environmentId, + ); const { addReleasePlanToFeature } = useReleasePlansApi(); const { isEnterprise } = useUiConfig(); const displayReleasePlanButton = isEnterprise(); const crProtected = isChangeRequestConfigured(environmentId); const newStrategyModalEnabled = useUiFlag('newStrategyModal'); + const activeReleasePlan = releasePlans[0]; + const onClose = () => { setIsStrategyMenuDialogOpen(false); }; @@ -121,8 +130,15 @@ export const FeatureStrategyMenu = ({ setIsStrategyMenuDialogOpen(true); }; - const addReleasePlan = async (template: IReleasePlanTemplate) => { + const addReleasePlan = async ( + template: IReleasePlanTemplate, + confirmed?: boolean, + ) => { try { + if (newStrategyModalEnabled && !confirmed && activeReleasePlan) { + setAddReleasePlanConfirmationOpen(true); + return; + } if (crProtected) { await addChange(projectId, environmentId, { feature: featureId, @@ -153,18 +169,19 @@ export const FeatureStrategyMenu = ({ refetch(); } + trackEvent('release-management', { props: { eventType: 'add-plan', plan: template.name, }, }); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - } finally { + setAddReleasePlanConfirmationOpen(false); setAddReleasePlanOpen(false); setSelectedTemplate(undefined); onClose(); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); } }; @@ -284,6 +301,7 @@ export const FeatureStrategyMenu = ({ projectId={projectId} featureName={featureId} environment={environmentId} + activeReleasePlan={activeReleasePlan} crProtected={crProtected} onBack={() => setReleasePlanPreview(false)} onConfirm={() => { @@ -329,23 +347,34 @@ export const FeatureStrategyMenu = ({ )} {selectedTemplate && ( - { - setAddReleasePlanOpen(open); - if (!open) { - setIsStrategyMenuDialogOpen(true); - } - }} - onConfirm={() => { - addReleasePlan(selectedTemplate); - }} - template={selectedTemplate} - projectId={projectId} - featureName={featureId} - environment={environmentId} - crProtected={crProtected} - /> + <> + { + setAddReleasePlanOpen(open); + if (!open) { + setIsStrategyMenuDialogOpen(true); + } + }} + onConfirm={() => { + addReleasePlan(selectedTemplate); + }} + template={selectedTemplate} + projectId={projectId} + featureName={featureId} + environment={environmentId} + crProtected={crProtected} + /> + { + addReleasePlan(selectedTemplate, true); + }} + /> + )} ); diff --git a/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanConfirmationDialog.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanConfirmationDialog.tsx new file mode 100644 index 0000000000..93946f7114 --- /dev/null +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanConfirmationDialog.tsx @@ -0,0 +1,35 @@ +import type React from 'react'; +import { Dialogue } from 'component/common/Dialogue/Dialogue'; +import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; + +interface IReleasePlanConfirmationDialogProps { + template: IReleasePlanTemplate; + crProtected: boolean; + open: boolean; + setOpen: React.Dispatch>; + onConfirm: () => void; +} + +export const ReleasePlanConfirmationDialog = ({ + template, + crProtected, + open, + setOpen, + onConfirm, +}: IReleasePlanConfirmationDialogProps) => ( + { + setOpen(false); + }} + > + This environment currently has a release plan added. Do you want to + replace it with {template.name}? + +); diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanPreview.tsx b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanPreview.tsx similarity index 81% rename from frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanPreview.tsx rename to frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanPreview.tsx index b1f29cfd35..037556f196 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ReleasePlan/ReleasePlanPreview.tsx +++ b/frontend/src/component/feature/FeatureStrategy/FeatureStrategyMenu/ReleasePlanPreview.tsx @@ -1,5 +1,8 @@ -import type { IReleasePlanTemplate } from 'interfaces/releasePlans'; -import { ReleasePlan } from './ReleasePlan.tsx'; +import type { + IReleasePlan, + IReleasePlanTemplate, +} from 'interfaces/releasePlans'; +import { ReleasePlan } from '../../FeatureView/FeatureOverview/ReleasePlan/ReleasePlan.tsx'; import { useReleasePlanPreview } from 'hooks/useReleasePlanPreview'; import { styled, @@ -9,9 +12,8 @@ import { DialogActions, Button, } from '@mui/material'; -import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; -import { useReleasePlans } from 'hooks/api/getters/useReleasePlans/useReleasePlans'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature.ts'; const StyledScrollableContent = styled(Box)(({ theme }) => ({ width: theme.breakpoints.values.md, @@ -38,6 +40,8 @@ interface IReleasePlanPreviewProps { projectId: string; featureName: string; environment: string; + environmentEnabled?: boolean; + activeReleasePlan?: IReleasePlan; crProtected?: boolean; onConfirm: () => void; onBack: () => void; @@ -48,19 +52,12 @@ export const ReleasePlanPreview = ({ projectId, featureName, environment, + activeReleasePlan, crProtected, onConfirm, onBack, }: IReleasePlanPreviewProps) => { const { feature } = useFeature(projectId, featureName); - const { releasePlans, loading } = useReleasePlans( - projectId, - featureName, - environment, - ); - - const activeReleasePlan = releasePlans[0]; - const environmentData = feature?.environments.find( ({ name }) => name === environment, ); @@ -72,8 +69,6 @@ export const ReleasePlanPreview = ({ environment, ); - if (loading) return null; - return ( <> @@ -85,13 +80,17 @@ export const ReleasePlanPreview = ({ {activeReleasePlan && ( - + This feature environment currently has{' '} - {activeReleasePlan.name} -{' '} + {activeReleasePlan.name} ( - {activeReleasePlan.milestones[0].name} + {activeReleasePlan.milestones.find( + ({ id }) => + activeReleasePlan.activeMilestoneId === + id, + )?.name ?? activeReleasePlan.milestones[0].name} - {environmentEnabled ? ' running' : ' paused'}. + ){environmentEnabled ? ' running' : ' paused'}. Adding a new release plan will replace the existing release plan.