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:
		
							parent
							
								
									224b842b5b
								
							
						
					
					
						commit
						154dc6f5eb
					
				@ -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';
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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<IReleasePlanMilestone>();
 | 
			
		||||
    const [progressionDataForCR, setProgressionDataForCR] =
 | 
			
		||||
        useState<CreateMilestoneProgressionSchema | null>(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 && (
 | 
			
		||||
                <CreateMilestoneProgressionChangeRequestDialog
 | 
			
		||||
                    environmentId={environment}
 | 
			
		||||
                    isOpen={changeRequestDialogCreateProgressionOpen}
 | 
			
		||||
                    onConfirm={onAddCreateProgressionChangesConfirm}
 | 
			
		||||
                    onClosing={() => {
 | 
			
		||||
                        setChangeRequestDialogCreateProgressionOpen(false);
 | 
			
		||||
                        setProgressionDataForCR(null);
 | 
			
		||||
                    }}
 | 
			
		||||
                    releasePlan={plan}
 | 
			
		||||
                    payload={progressionDataForCR}
 | 
			
		||||
                />
 | 
			
		||||
            )}
 | 
			
		||||
            {milestoneToDeleteProgression && (
 | 
			
		||||
                <DeleteProgressionDialog
 | 
			
		||||
                    open={milestoneToDeleteProgression !== null}
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,8 @@ export interface IChangeSchema {
 | 
			
		||||
        | 'deleteDependency'
 | 
			
		||||
        | 'addReleasePlan'
 | 
			
		||||
        | 'deleteReleasePlan'
 | 
			
		||||
        | 'startMilestone';
 | 
			
		||||
        | 'startMilestone'
 | 
			
		||||
        | 'createMilestoneProgression';
 | 
			
		||||
    payload: string | boolean | object | number | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user