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 { 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';
|
||||||
|
|||||||
@ -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 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();
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user