1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

refactor: consolidate release plan change request dialogs (#10817)

This commit is contained in:
Fredrik Strand Oseberg 2025-10-16 13:55:52 +02:00 committed by GitHub
parent 9096340afb
commit 045ef5a20e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 193 additions and 293 deletions

View File

@ -1,70 +0,0 @@
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<void>;
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 (
<Dialogue
title='Request changes'
open={isOpen}
secondaryButtonText='Cancel'
onClose={onClosing}
customButton={
<Button
color='primary'
variant='contained'
onClick={onConfirm}
autoFocus={true}
>
Add suggestion to draft
</Button>
}
>
<p>
Create automation to proceed from{' '}
<StyledBoldSpan>{sourceMilestone?.name}</StyledBoldSpan> to{' '}
<StyledBoldSpan>{targetMilestone?.name}</StyledBoldSpan> after{' '}
<StyledBoldSpan>{timeInterval}</StyledBoldSpan> in{' '}
{environmentId}
</p>
</Dialogue>
);
};

View File

@ -0,0 +1,131 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { styled, Button, Alert } from '@mui/material';
import type {
IReleasePlan,
IReleasePlanMilestone,
} from 'interfaces/releasePlans';
import type { CreateMilestoneProgressionSchema } from 'openapi';
import { getTimeValueAndUnitFromMinutes } from '../hooks/useMilestoneProgressionForm.js';
const StyledBoldSpan = styled('span')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}));
type ChangeRequestAction =
| {
type: 'removeReleasePlan';
environmentActive: boolean;
}
| {
type: 'startMilestone';
milestone: IReleasePlanMilestone;
}
| {
type: 'createMilestoneProgression';
payload: CreateMilestoneProgressionSchema;
};
interface IReleasePlanChangeRequestDialogProps {
featureId: string;
environmentId: string;
releasePlan: IReleasePlan;
action: ChangeRequestAction | null;
isOpen: boolean;
onConfirm: () => Promise<void>;
onClose: () => void;
}
export const ReleasePlanChangeRequestDialog = ({
featureId,
environmentId,
releasePlan,
action,
isOpen,
onConfirm,
onClose,
}: IReleasePlanChangeRequestDialogProps) => {
if (!action) return null;
const renderContent = () => {
switch (action.type) {
case 'removeReleasePlan':
return (
<>
{action.environmentActive && (
<Alert severity='error' sx={{ mb: 2 }}>
This release plan currently has one active
milestone. Removing the release plan will change
which users receive access to the feature.
</Alert>
)}
<p>
<StyledBoldSpan>Remove</StyledBoldSpan> release plan{' '}
<StyledBoldSpan>{releasePlan.name}</StyledBoldSpan>{' '}
from <StyledBoldSpan>{featureId}</StyledBoldSpan> in{' '}
<StyledBoldSpan>{environmentId}</StyledBoldSpan>
</p>
</>
);
case 'startMilestone':
return (
<p>
<StyledBoldSpan>Start</StyledBoldSpan> milestone{' '}
<StyledBoldSpan>{action.milestone.name}</StyledBoldSpan>{' '}
in release plan{' '}
<StyledBoldSpan>{releasePlan.name}</StyledBoldSpan> for{' '}
<StyledBoldSpan>{featureId}</StyledBoldSpan> in{' '}
<StyledBoldSpan>{environmentId}</StyledBoldSpan>
</p>
);
case 'createMilestoneProgression': {
const sourceMilestone = releasePlan.milestones.find(
(milestone) =>
milestone.id === action.payload.sourceMilestone,
);
const targetMilestone = releasePlan.milestones.find(
(milestone) =>
milestone.id === action.payload.targetMilestone,
);
const { value, unit } = getTimeValueAndUnitFromMinutes(
action.payload.transitionCondition.intervalMinutes,
);
const timeInterval = `${value} ${unit}`;
return (
<p>
Create automation to proceed from{' '}
<StyledBoldSpan>{sourceMilestone?.name}</StyledBoldSpan>{' '}
to{' '}
<StyledBoldSpan>{targetMilestone?.name}</StyledBoldSpan>{' '}
after <StyledBoldSpan>{timeInterval}</StyledBoldSpan> in{' '}
{environmentId}
</p>
);
}
}
};
return (
<Dialogue
title='Request changes'
open={isOpen}
secondaryButtonText='Cancel'
onClose={onClose}
customButton={
<Button
color='primary'
variant='contained'
onClick={onConfirm}
autoFocus={true}
>
Add suggestion to draft
</Button>
}
>
{renderContent()}
</Dialogue>
);
};

View File

@ -1,62 +0,0 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { styled, Button, Alert } from '@mui/material';
import type { IReleasePlan } from 'interfaces/releasePlans';
const StyledBoldSpan = styled('span')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}));
interface IRemoveReleasePlanChangeRequestDialogProps {
featureId: string;
environmentId: string;
releasePlan?: IReleasePlan | undefined;
environmentActive: boolean;
isOpen: boolean;
onConfirm: () => Promise<void>;
onClosing: () => void;
}
export const RemoveReleasePlanChangeRequestDialog = ({
featureId,
environmentId,
releasePlan,
environmentActive,
isOpen,
onConfirm,
onClosing,
}: IRemoveReleasePlanChangeRequestDialogProps) => {
return (
<Dialogue
title='Request changes'
open={isOpen}
secondaryButtonText='Cancel'
onClose={onClosing}
customButton={
<Button
color='primary'
variant='contained'
onClick={onConfirm}
autoFocus={true}
>
Add suggestion to draft
</Button>
}
>
<>
{environmentActive && (
<Alert severity='error' sx={{ mb: 2 }}>
This release plan currently has one active milestone.
Removing the release plan will change which users
receive access to the feature.
</Alert>
)}
<p>
<StyledBoldSpan>Remove</StyledBoldSpan> release plan{' '}
<StyledBoldSpan>{releasePlan?.name}</StyledBoldSpan> from{' '}
<StyledBoldSpan>{featureId}</StyledBoldSpan> in{' '}
<StyledBoldSpan>{environmentId}</StyledBoldSpan>
</p>
</>
</Dialogue>
);
};

View File

@ -1,57 +0,0 @@
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { styled, Button } from '@mui/material';
import type {
IReleasePlan,
IReleasePlanMilestone,
} from 'interfaces/releasePlans';
const StyledBoldSpan = styled('span')(({ theme }) => ({
fontWeight: theme.typography.fontWeightBold,
}));
interface IStartMilestoneChangeRequestDialogProps {
featureId: string;
environmentId: string;
releasePlan?: IReleasePlan | undefined;
milestone?: IReleasePlanMilestone | undefined;
isOpen: boolean;
onConfirm: () => Promise<void>;
onClosing: () => void;
}
export const StartMilestoneChangeRequestDialog = ({
featureId,
environmentId,
releasePlan,
milestone,
isOpen,
onConfirm,
onClosing,
}: IStartMilestoneChangeRequestDialogProps) => {
return (
<Dialogue
title='Request changes'
open={isOpen}
secondaryButtonText='Cancel'
onClose={onClosing}
customButton={
<Button
color='primary'
variant='contained'
onClick={onConfirm}
autoFocus={true}
>
Add suggestion to draft
</Button>
}
>
<p>
<StyledBoldSpan>Start</StyledBoldSpan> milestone{' '}
<StyledBoldSpan>{milestone?.name}</StyledBoldSpan> in release
plan <StyledBoldSpan>{releasePlan?.name}</StyledBoldSpan> for{' '}
<StyledBoldSpan>{featureId}</StyledBoldSpan> in{' '}
<StyledBoldSpan>{environmentId}</StyledBoldSpan>
</p>
</Dialogue>
);
};

View File

@ -18,9 +18,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
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';
import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog.tsx'; import { ReleasePlanChangeRequestDialog } from './ChangeRequest/ReleasePlanChangeRequestDialog.tsx';
import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog.tsx';
import { CreateMilestoneProgressionChangeRequestDialog } from './ChangeRequest/CreateMilestoneProgressionChangeRequestDialog.tsx';
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';
@ -119,22 +117,15 @@ export const ReleasePlan = ({
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const [removeOpen, setRemoveOpen] = useState(false); const [removeOpen, setRemoveOpen] = useState(false);
const [changeRequestDialogRemoveOpen, setChangeRequestDialogRemoveOpen] = const [changeRequestAction, setChangeRequestAction] = useState<
useState(false); | { type: 'removeReleasePlan'; environmentActive: boolean }
const [ | { type: 'startMilestone'; milestone: IReleasePlanMilestone }
changeRequestDialogStartMilestoneOpen, | {
setChangeRequestDialogStartMilestoneOpen, type: 'createMilestoneProgression';
] = useState(false); payload: CreateMilestoneProgressionSchema;
const [ }
changeRequestDialogCreateProgressionOpen, | null
setChangeRequestDialogCreateProgressionOpen, >(null);
] = useState(false);
const [
milestoneForChangeRequestDialog,
setMilestoneForChangeRequestDialog,
] = useState<IReleasePlanMilestone>();
const [progressionDataForCR, setProgressionDataForCR] =
useState<CreateMilestoneProgressionSchema | null>(null);
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { addChange } = useChangeRequestApi(); const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } = const { refetch: refetchChangeRequests } =
@ -147,14 +138,40 @@ export const ReleasePlan = ({
useState<IReleasePlanMilestone | null>(null); useState<IReleasePlanMilestone | null>(null);
const [isDeletingProgression, setIsDeletingProgression] = useState(false); const [isDeletingProgression, setIsDeletingProgression] = useState(false);
const onAddRemovePlanChangesConfirm = async () => { const onChangeRequestConfirm = async () => {
await addChange(projectId, environment, { if (!changeRequestAction) return;
feature: featureName,
action: 'deleteReleasePlan', switch (changeRequestAction.type) {
payload: { case 'removeReleasePlan':
planId: plan.id, await addChange(projectId, environment, {
}, feature: featureName,
}); action: 'deleteReleasePlan',
payload: {
planId: plan.id,
},
});
break;
case 'startMilestone':
await addChange(projectId, environment, {
feature: featureName,
action: 'startMilestone',
payload: {
planId: plan.id,
milestoneId: changeRequestAction.milestone.id,
},
});
break;
case 'createMilestoneProgression':
await addChange(projectId, environment, {
feature: featureName,
action: 'createMilestoneProgression',
payload: changeRequestAction.payload,
});
setProgressionFormOpenIndex(null);
break;
}
await refetchChangeRequests(); await refetchChangeRequests();
@ -163,53 +180,15 @@ export const ReleasePlan = ({
text: 'Added to draft', text: 'Added to draft',
}); });
setChangeRequestDialogRemoveOpen(false); setChangeRequestAction(null);
};
const onAddStartMilestoneChangesConfirm = async () => {
await addChange(projectId, environment, {
feature: featureName,
action: 'startMilestone',
payload: {
planId: plan.id,
milestoneId: milestoneForChangeRequestDialog?.id,
},
});
await refetchChangeRequests();
setToastData({
type: 'success',
text: 'Added to draft',
});
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 = () => { const confirmRemoveReleasePlan = () => {
if (isChangeRequestConfigured(environment)) { if (isChangeRequestConfigured(environment)) {
setChangeRequestDialogRemoveOpen(true); setChangeRequestAction({
type: 'removeReleasePlan',
environmentActive: !environmentIsDisabled,
});
} else { } else {
setRemoveOpen(true); setRemoveOpen(true);
} }
@ -244,8 +223,10 @@ export const ReleasePlan = ({
const onStartMilestone = async (milestone: IReleasePlanMilestone) => { const onStartMilestone = async (milestone: IReleasePlanMilestone) => {
if (isChangeRequestConfigured(environment)) { if (isChangeRequestConfigured(environment)) {
setMilestoneForChangeRequestDialog(milestone); setChangeRequestAction({
setChangeRequestDialogStartMilestoneOpen(true); type: 'startMilestone',
milestone,
});
} else { } else {
try { try {
await startReleasePlanMilestone( await startReleasePlanMilestone(
@ -286,8 +267,10 @@ export const ReleasePlan = ({
const handleProgressionChangeRequestSubmit = ( const handleProgressionChangeRequestSubmit = (
payload: CreateMilestoneProgressionSchema, payload: CreateMilestoneProgressionSchema,
) => { ) => {
setProgressionDataForCR(payload); setChangeRequestAction({
setChangeRequestDialogCreateProgressionOpen(true); type: 'createMilestoneProgression',
payload,
});
}; };
const handleDeleteProgression = (milestone: IReleasePlanMilestone) => { const handleDeleteProgression = (milestone: IReleasePlanMilestone) => {
@ -437,40 +420,15 @@ export const ReleasePlan = ({
onConfirm={onRemoveConfirm} onConfirm={onRemoveConfirm}
environmentActive={!environmentIsDisabled} environmentActive={!environmentIsDisabled}
/> />
<RemoveReleasePlanChangeRequestDialog <ReleasePlanChangeRequestDialog
environmentId={environment}
featureId={featureName} featureId={featureName}
isOpen={changeRequestDialogRemoveOpen}
onConfirm={onAddRemovePlanChangesConfirm}
onClosing={() => setChangeRequestDialogRemoveOpen(false)}
releasePlan={plan}
environmentActive={!environmentIsDisabled}
/>
<StartMilestoneChangeRequestDialog
environmentId={environment} environmentId={environment}
featureId={featureName}
isOpen={changeRequestDialogStartMilestoneOpen}
onConfirm={onAddStartMilestoneChangesConfirm}
onClosing={() => {
setMilestoneForChangeRequestDialog(undefined);
setChangeRequestDialogStartMilestoneOpen(false);
}}
releasePlan={plan} releasePlan={plan}
milestone={milestoneForChangeRequestDialog} action={changeRequestAction}
isOpen={changeRequestAction !== null}
onConfirm={onChangeRequestConfirm}
onClose={() => setChangeRequestAction(null)}
/> />
{progressionDataForCR && (
<CreateMilestoneProgressionChangeRequestDialog
environmentId={environment}
isOpen={changeRequestDialogCreateProgressionOpen}
onConfirm={onAddCreateProgressionChangesConfirm}
onClosing={() => {
setChangeRequestDialogCreateProgressionOpen(false);
setProgressionDataForCR(null);
}}
releasePlan={plan}
payload={progressionDataForCR}
/>
)}
{milestoneToDeleteProgression && ( {milestoneToDeleteProgression && (
<DeleteProgressionDialog <DeleteProgressionDialog
open={milestoneToDeleteProgression !== null} open={milestoneToDeleteProgression !== null}