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

feat: add change request support for milestone progressions (#10814)

This commit is contained in:
Fredrik Strand Oseberg 2025-10-16 11:57:28 +02:00 committed by GitHub
parent 224b842b5b
commit 154dc6f5eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 170 additions and 11 deletions

View File

@ -2,7 +2,10 @@ import type { IFeatureVariant } from 'interfaces/featureToggle';
import type { ISegment } from 'interfaces/segment'; import type { ISegment } from 'interfaces/segment';
import type { IFeatureStrategy } from '../../interfaces/strategy.js'; import type { IFeatureStrategy } from '../../interfaces/strategy.js';
import type { IUser } from '../../interfaces/user.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'; import type { IReleasePlan } from 'interfaces/releasePlans';
type BaseChangeRequest = { type BaseChangeRequest = {
@ -131,7 +134,8 @@ type ChangeRequestPayload =
| ChangeRequestAddDependency | ChangeRequestAddDependency
| ChangeRequestAddReleasePlan | ChangeRequestAddReleasePlan
| ChangeRequestDeleteReleasePlan | ChangeRequestDeleteReleasePlan
| ChangeRequestStartMilestone; | ChangeRequestStartMilestone
| ChangeRequestCreateMilestoneProgression;
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase { export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
action: 'addStrategy'; action: 'addStrategy';
@ -188,6 +192,12 @@ export interface IChangeRequestStartMilestone extends IChangeRequestChangeBase {
payload: ChangeRequestStartMilestone; payload: ChangeRequestStartMilestone;
} }
export interface IChangeRequestCreateMilestoneProgression
extends IChangeRequestChangeBase {
action: 'createMilestoneProgression';
payload: ChangeRequestCreateMilestoneProgression;
}
export interface IChangeRequestReorderStrategy export interface IChangeRequestReorderStrategy
extends IChangeRequestChangeBase { extends IChangeRequestChangeBase {
action: 'reorderStrategy'; action: 'reorderStrategy';
@ -235,7 +245,8 @@ export type IFeatureChange =
| IChangeRequestDeleteDependency | IChangeRequestDeleteDependency
| IChangeRequestAddReleasePlan | IChangeRequestAddReleasePlan
| IChangeRequestDeleteReleasePlan | IChangeRequestDeleteReleasePlan
| IChangeRequestStartMilestone; | IChangeRequestStartMilestone
| IChangeRequestCreateMilestoneProgression;
export type ISegmentChange = export type ISegmentChange =
| IChangeRequestUpdateSegment | IChangeRequestUpdateSegment
@ -268,6 +279,8 @@ type ChangeRequestStartMilestone = {
snapshot?: IReleasePlan; snapshot?: IReleasePlan;
}; };
type ChangeRequestCreateMilestoneProgression = CreateMilestoneProgressionSchema;
export type ChangeRequestAddStrategy = Pick< export type ChangeRequestAddStrategy = Pick<
IFeatureStrategy, IFeatureStrategy,
| 'parameters' | 'parameters'
@ -305,4 +318,5 @@ export type ChangeRequestAction =
| 'deleteDependency' | 'deleteDependency'
| 'addReleasePlan' | 'addReleasePlan'
| 'deleteReleasePlan' | 'deleteReleasePlan'
| 'startMilestone'; | 'startMilestone'
| 'createMilestoneProgression';

View File

@ -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<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

@ -6,6 +6,8 @@ import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgr
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx'; import { MilestoneProgressionTimeInput } from './MilestoneProgressionTimeInput.tsx';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import type { CreateMilestoneProgressionSchema } from 'openapi';
const StyledFormContainer = styled('div')(({ theme }) => ({ const StyledFormContainer = styled('div')(({ theme }) => ({
display: 'flex', display: 'flex',
@ -63,6 +65,9 @@ interface IMilestoneProgressionFormProps {
featureName: string; featureName: string;
onSave: () => void; onSave: () => void;
onCancel: () => void; onCancel: () => void;
onChangeRequestSubmit?: (
progressionPayload: CreateMilestoneProgressionSchema,
) => void;
} }
export const MilestoneProgressionForm = ({ export const MilestoneProgressionForm = ({
@ -73,6 +78,7 @@ export const MilestoneProgressionForm = ({
featureName, featureName,
onSave, onSave,
onCancel, onCancel,
onChangeRequestSubmit,
}: IMilestoneProgressionFormProps) => { }: IMilestoneProgressionFormProps) => {
const form = useMilestoneProgressionForm( const form = useMilestoneProgressionForm(
sourceMilestoneId, sourceMilestoneId,
@ -80,16 +86,16 @@ export const MilestoneProgressionForm = ({
); );
const { createMilestoneProgression } = useMilestoneProgressionsApi(); const { createMilestoneProgression } = useMilestoneProgressionsApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async () => { const handleChangeRequestSubmit = () => {
if (isSubmitting) return; const progressionPayload = form.getProgressionPayload();
onChangeRequestSubmit?.(progressionPayload);
if (!form.validate()) { };
return;
}
const handleDirectSubmit = async () => {
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await createMilestoneProgression( 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) => { const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();

View File

@ -20,12 +20,14 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog.tsx'; import { RemoveReleasePlanChangeRequestDialog } from './ChangeRequest/RemoveReleasePlanChangeRequestDialog.tsx';
import { StartMilestoneChangeRequestDialog } from './ChangeRequest/StartMilestoneChangeRequestDialog.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';
import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx'; import { MilestoneProgressionForm } from './MilestoneProgressionForm/MilestoneProgressionForm.tsx';
import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi'; import { useMilestoneProgressionsApi } from 'hooks/api/actions/useMilestoneProgressionsApi/useMilestoneProgressionsApi';
import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx'; import { DeleteProgressionDialog } from './DeleteProgressionDialog.tsx';
import type { CreateMilestoneProgressionSchema } from 'openapi';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(2), padding: theme.spacing(2),
@ -123,10 +125,16 @@ export const ReleasePlan = ({
changeRequestDialogStartMilestoneOpen, changeRequestDialogStartMilestoneOpen,
setChangeRequestDialogStartMilestoneOpen, setChangeRequestDialogStartMilestoneOpen,
] = useState(false); ] = useState(false);
const [
changeRequestDialogCreateProgressionOpen,
setChangeRequestDialogCreateProgressionOpen,
] = useState(false);
const [ const [
milestoneForChangeRequestDialog, milestoneForChangeRequestDialog,
setMilestoneForChangeRequestDialog, setMilestoneForChangeRequestDialog,
] = useState<IReleasePlanMilestone>(); ] = 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 } =
@ -178,6 +186,27 @@ export const ReleasePlan = ({
setChangeRequestDialogStartMilestoneOpen(false); 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); setChangeRequestDialogRemoveOpen(true);
@ -254,6 +283,13 @@ export const ReleasePlan = ({
setProgressionFormOpenIndex(null); setProgressionFormOpenIndex(null);
}; };
const handleProgressionChangeRequestSubmit = (
payload: CreateMilestoneProgressionSchema,
) => {
setProgressionDataForCR(payload);
setChangeRequestDialogCreateProgressionOpen(true);
};
const handleDeleteProgression = (milestone: IReleasePlanMilestone) => { const handleDeleteProgression = (milestone: IReleasePlanMilestone) => {
setMilestoneToDeleteProgression(milestone); setMilestoneToDeleteProgression(milestone);
}; };
@ -367,6 +403,11 @@ export const ReleasePlan = ({
featureName={featureName} featureName={featureName}
onSave={handleProgressionSave} onSave={handleProgressionSave}
onCancel={handleProgressionCancel} onCancel={handleProgressionCancel}
onChangeRequestSubmit={(payload) =>
handleProgressionChangeRequestSubmit(
payload,
)
}
/> />
) : undefined ) : undefined
} }
@ -417,6 +458,19 @@ export const ReleasePlan = ({
releasePlan={plan} releasePlan={plan}
milestone={milestoneForChangeRequestDialog} milestone={milestoneForChangeRequestDialog}
/> />
{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}

View File

@ -21,7 +21,8 @@ export interface IChangeSchema {
| 'deleteDependency' | 'deleteDependency'
| 'addReleasePlan' | 'addReleasePlan'
| 'deleteReleasePlan' | 'deleteReleasePlan'
| 'startMilestone'; | 'startMilestone'
| 'createMilestoneProgression';
payload: string | boolean | object | number | undefined; payload: string | boolean | object | number | undefined;
} }