mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: schedule changes dialog (#5285)
Closes: # [1-1585](https://linear.app/unleash/issue/1-1585/reschedule-changes-dialog) [1-1582](https://linear.app/unleash/issue/1-1582/change-apply-changes-apply-or-schedule-changes) Manually tested e2e -> Approve -> Schedule -> Reschedule -> Apply/Reject: ui tests verifying it in next pr --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai> Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									b3054c9277
								
							
						
					
					
						commit
						addda5b022
					
				@ -30,6 +30,7 @@ import {
 | 
			
		||||
    ChangeRequestApplyScheduledDialogue,
 | 
			
		||||
    ChangeRequestRejectScheduledDialogue,
 | 
			
		||||
} from './ChangeRequestScheduledDialogs/changeRequestScheduledDialogs';
 | 
			
		||||
import { ScheduleChangeRequestDialog } from './ChangeRequestScheduledDialogs/ScheduleChangeRequestDialog';
 | 
			
		||||
 | 
			
		||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
    width: '30%',
 | 
			
		||||
@ -77,6 +78,8 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
    const projectId = useRequiredPathParam('projectId');
 | 
			
		||||
    const [showCancelDialog, setShowCancelDialog] = useState(false);
 | 
			
		||||
    const [showRejectDialog, setShowRejectDialog] = useState(false);
 | 
			
		||||
    const [showScheduleChangesDialog, setShowScheduleChangeDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
    const [showApplyScheduledDialog, setShowApplyScheduledDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
    const [showRejectScheduledDialog, setShowRejectScheduledDialog] =
 | 
			
		||||
@ -111,6 +114,7 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
            await changeState(projectId, Number(id), {
 | 
			
		||||
                state: 'Applied',
 | 
			
		||||
            });
 | 
			
		||||
            setShowApplyScheduledDialog(false);
 | 
			
		||||
            refetchChangeRequest();
 | 
			
		||||
            refetchChangeRequestOpen();
 | 
			
		||||
            setToastData({
 | 
			
		||||
@ -123,6 +127,25 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onScheduleChangeRequest = async (scheduledDate: Date) => {
 | 
			
		||||
        try {
 | 
			
		||||
            await changeState(projectId, Number(id), {
 | 
			
		||||
                state: 'Scheduled',
 | 
			
		||||
                scheduledAt: scheduledDate.toISOString(),
 | 
			
		||||
            });
 | 
			
		||||
            setShowScheduleChangeDialog(false);
 | 
			
		||||
            refetchChangeRequest();
 | 
			
		||||
            refetchChangeRequestOpen();
 | 
			
		||||
            setToastData({
 | 
			
		||||
                type: 'success',
 | 
			
		||||
                title: 'Success',
 | 
			
		||||
                text: 'Changes scheduled',
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            setToastApiError(formatUnknownError(error));
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onAddComment = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            await addComment(projectId, id, commentText);
 | 
			
		||||
@ -196,6 +219,7 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
    const onCancelAbort = () => setShowCancelDialog(false);
 | 
			
		||||
    const onCancelReject = () => setShowRejectDialog(false);
 | 
			
		||||
    const onApplyScheduledAbort = () => setShowApplyScheduledDialog(false);
 | 
			
		||||
    const onScheduleChangeAbort = () => setShowApplyScheduledDialog(false);
 | 
			
		||||
    const onRejectScheduledAbort = () => setShowRejectScheduledDialog(false);
 | 
			
		||||
 | 
			
		||||
    const isSelfReview =
 | 
			
		||||
@ -293,11 +317,11 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
                                                    !allowChangeRequestActions ||
 | 
			
		||||
                                                    loading
 | 
			
		||||
                                                }
 | 
			
		||||
                                                onSchedule={() => {
 | 
			
		||||
                                                    console.log(
 | 
			
		||||
                                                        'I would schedule changes now',
 | 
			
		||||
                                                    );
 | 
			
		||||
                                                }}
 | 
			
		||||
                                                onSchedule={() =>
 | 
			
		||||
                                                    setShowScheduleChangeDialog(
 | 
			
		||||
                                                        true,
 | 
			
		||||
                                                    )
 | 
			
		||||
                                                }
 | 
			
		||||
                                            >
 | 
			
		||||
                                                Apply or schedule changes
 | 
			
		||||
                                            </ApplyButton>
 | 
			
		||||
@ -339,11 +363,9 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
                                            !allowChangeRequestActions ||
 | 
			
		||||
                                            loading
 | 
			
		||||
                                        }
 | 
			
		||||
                                        onSchedule={() => {
 | 
			
		||||
                                            console.log(
 | 
			
		||||
                                                'I would schedule changes now',
 | 
			
		||||
                                            );
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        onSchedule={() =>
 | 
			
		||||
                                            setShowScheduleChangeDialog(true)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        variant={'update'}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Apply or schedule changes
 | 
			
		||||
@ -421,6 +443,28 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
                    condition={scheduleChangeRequests}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <ScheduleChangeRequestDialog
 | 
			
		||||
                                open={showScheduleChangesDialog}
 | 
			
		||||
                                onConfirm={onScheduleChangeRequest}
 | 
			
		||||
                                onClose={onScheduleChangeAbort}
 | 
			
		||||
                                disabled={!allowChangeRequestActions || loading}
 | 
			
		||||
                                projectId={projectId}
 | 
			
		||||
                                environment={changeRequest.environment}
 | 
			
		||||
                                primaryButtonText={
 | 
			
		||||
                                    changeRequest?.schedule?.scheduledAt
 | 
			
		||||
                                        ? 'Update scheduled time'
 | 
			
		||||
                                        : 'Schedule changes'
 | 
			
		||||
                                }
 | 
			
		||||
                                title={
 | 
			
		||||
                                    changeRequest?.schedule?.scheduledAt
 | 
			
		||||
                                        ? 'Update schedule'
 | 
			
		||||
                                        : 'Schedule changes'
 | 
			
		||||
                                }
 | 
			
		||||
                                scheduledAt={
 | 
			
		||||
                                    changeRequest?.schedule?.scheduledAt ||
 | 
			
		||||
                                    undefined
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                            <ChangeRequestApplyScheduledDialogue
 | 
			
		||||
                                open={showApplyScheduledDialog}
 | 
			
		||||
                                onConfirm={onApplyChanges}
 | 
			
		||||
@ -434,7 +478,7 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
                            />
 | 
			
		||||
                            <ChangeRequestRejectScheduledDialogue
 | 
			
		||||
                                open={showRejectScheduledDialog}
 | 
			
		||||
                                onConfirm={onCancelChanges}
 | 
			
		||||
                                onConfirm={onReject}
 | 
			
		||||
                                onClose={onRejectScheduledAbort}
 | 
			
		||||
                                scheduledTime={
 | 
			
		||||
                                    changeRequest?.schedule?.scheduledAt
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { FC, ReactElement } from 'react';
 | 
			
		||||
import { Alert, styled, Typography } from '@mui/material';
 | 
			
		||||
import { Dialogue } from '../../../common/Dialogue/Dialogue';
 | 
			
		||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
 | 
			
		||||
 | 
			
		||||
export interface ChangeRequestScheduleDialogueProps {
 | 
			
		||||
export interface ChangeRequestScheduledDialogProps {
 | 
			
		||||
    title: string;
 | 
			
		||||
    primaryButtonText: string;
 | 
			
		||||
    open: boolean;
 | 
			
		||||
@ -12,8 +12,6 @@ export interface ChangeRequestScheduleDialogueProps {
 | 
			
		||||
    message: string;
 | 
			
		||||
    permissionButton?: ReactElement;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    projectId?: string;
 | 
			
		||||
    environment?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledAlert = styled(Alert)(({ theme }) => ({
 | 
			
		||||
@ -23,8 +21,8 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
 | 
			
		||||
    borderColor: `${theme.palette.neutral.light}!important`,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const ChangeRequestScheduledDialogue: FC<
 | 
			
		||||
    ChangeRequestScheduleDialogueProps
 | 
			
		||||
export const ChangeRequestScheduledDialog: FC<
 | 
			
		||||
    ChangeRequestScheduledDialogProps
 | 
			
		||||
> = ({
 | 
			
		||||
    open,
 | 
			
		||||
    onConfirm,
 | 
			
		||||
@ -33,6 +31,7 @@ export const ChangeRequestScheduledDialogue: FC<
 | 
			
		||||
    primaryButtonText,
 | 
			
		||||
    message,
 | 
			
		||||
    scheduledTime,
 | 
			
		||||
    permissionButton,
 | 
			
		||||
}) => {
 | 
			
		||||
    if (!scheduledTime) return null;
 | 
			
		||||
 | 
			
		||||
@ -44,6 +43,7 @@ export const ChangeRequestScheduledDialogue: FC<
 | 
			
		||||
            open={open}
 | 
			
		||||
            onClose={onClose}
 | 
			
		||||
            onClick={() => onConfirm()}
 | 
			
		||||
            permissionButton={permissionButton}
 | 
			
		||||
            fullWidth
 | 
			
		||||
        >
 | 
			
		||||
            <StyledAlert icon={false}>
 | 
			
		||||
@ -0,0 +1,107 @@
 | 
			
		||||
import { FC, useState } from 'react';
 | 
			
		||||
import { Alert, Box, styled, Typography } from '@mui/material';
 | 
			
		||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
 | 
			
		||||
import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
 | 
			
		||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
 | 
			
		||||
import { DateTimePicker } from 'component/common/DateTimePicker/DateTimePicker';
 | 
			
		||||
import { getBrowserTimezoneInHumanReadableUTCOffset } from '../ChangeRequestReviewStatus/utils';
 | 
			
		||||
 | 
			
		||||
export interface ScheduleChangeRequestDialogProps {
 | 
			
		||||
    title: string;
 | 
			
		||||
    primaryButtonText: string;
 | 
			
		||||
    open: boolean;
 | 
			
		||||
    onConfirm: (selectedDate: Date) => void;
 | 
			
		||||
    onClose: () => void;
 | 
			
		||||
    projectId: string;
 | 
			
		||||
    environment: string;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    scheduledAt?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledContainer = styled(Box)(({ theme }) => ({
 | 
			
		||||
    display: 'flex',
 | 
			
		||||
    flexDirection: 'row',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    gap: theme.spacing(2),
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const ScheduleChangeRequestDialog: FC<ScheduleChangeRequestDialogProps> =
 | 
			
		||||
    ({
 | 
			
		||||
        open,
 | 
			
		||||
        onConfirm,
 | 
			
		||||
        onClose,
 | 
			
		||||
        title,
 | 
			
		||||
        primaryButtonText,
 | 
			
		||||
        projectId,
 | 
			
		||||
        environment,
 | 
			
		||||
        disabled,
 | 
			
		||||
        scheduledAt,
 | 
			
		||||
    }) => {
 | 
			
		||||
        const [selectedDate, setSelectedDate] = useState(
 | 
			
		||||
            scheduledAt ? new Date(scheduledAt) : new Date(),
 | 
			
		||||
        );
 | 
			
		||||
        const [error, setError] = useState<string | undefined>(undefined);
 | 
			
		||||
 | 
			
		||||
        const timezone = getBrowserTimezoneInHumanReadableUTCOffset();
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <Dialogue
 | 
			
		||||
                title={title}
 | 
			
		||||
                primaryButtonText={primaryButtonText}
 | 
			
		||||
                secondaryButtonText='Cancel'
 | 
			
		||||
                open={open}
 | 
			
		||||
                onClose={onClose}
 | 
			
		||||
                onClick={() => onConfirm(selectedDate)}
 | 
			
		||||
                permissionButton={
 | 
			
		||||
                    <PermissionButton
 | 
			
		||||
                        variant='contained'
 | 
			
		||||
                        onClick={() => onConfirm(selectedDate)}
 | 
			
		||||
                        projectId={projectId}
 | 
			
		||||
                        permission={APPLY_CHANGE_REQUEST}
 | 
			
		||||
                        environmentId={environment}
 | 
			
		||||
                        disabled={disabled}
 | 
			
		||||
                    >
 | 
			
		||||
                        {primaryButtonText}
 | 
			
		||||
                    </PermissionButton>
 | 
			
		||||
                }
 | 
			
		||||
                fullWidth
 | 
			
		||||
            >
 | 
			
		||||
                <Alert
 | 
			
		||||
                    severity={'info'}
 | 
			
		||||
                    sx={{ mb: (theme) => theme.spacing(2) }}
 | 
			
		||||
                >
 | 
			
		||||
                    The time shown below is based on your browser's time zone.
 | 
			
		||||
                </Alert>
 | 
			
		||||
                <Typography
 | 
			
		||||
                    variant={'body1'}
 | 
			
		||||
                    sx={{ mb: (theme) => theme.spacing(4) }}
 | 
			
		||||
                >
 | 
			
		||||
                    Select the date and time when these changes should be
 | 
			
		||||
                    applied. If you change your mind later, you can reschedule
 | 
			
		||||
                    the changes or apply the immediately.
 | 
			
		||||
                </Typography>
 | 
			
		||||
                <StyledContainer>
 | 
			
		||||
                    <DateTimePicker
 | 
			
		||||
                        label='Date'
 | 
			
		||||
                        value={selectedDate}
 | 
			
		||||
                        onChange={(date) => {
 | 
			
		||||
                            setError(undefined);
 | 
			
		||||
                            if (date < new Date()) {
 | 
			
		||||
                                setError(
 | 
			
		||||
                                    `The time you provided (${date.toLocaleString()}) is not valid because it's in the past. Please select a time in the future.`,
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                            setSelectedDate(date);
 | 
			
		||||
                        }}
 | 
			
		||||
                        min={new Date()}
 | 
			
		||||
                        error={Boolean(error)}
 | 
			
		||||
                        errorText={error}
 | 
			
		||||
                        required
 | 
			
		||||
                    />
 | 
			
		||||
                    <Typography variant={'body2'}>
 | 
			
		||||
                        Your browser's time zone is {timezone}
 | 
			
		||||
                    </Typography>
 | 
			
		||||
                </StyledContainer>
 | 
			
		||||
            </Dialogue>
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
@ -1,19 +1,16 @@
 | 
			
		||||
import { FC, useState } from 'react';
 | 
			
		||||
import { TextField, Box, Alert, styled, Typography } from '@mui/material';
 | 
			
		||||
import { Dialogue } from '../../../common/Dialogue/Dialogue';
 | 
			
		||||
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import { FC } from 'react';
 | 
			
		||||
import { APPLY_CHANGE_REQUEST } from '../../../providers/AccessProvider/permissions';
 | 
			
		||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
 | 
			
		||||
import {
 | 
			
		||||
    ChangeRequestScheduledDialogue,
 | 
			
		||||
    ChangeRequestScheduleDialogueProps,
 | 
			
		||||
} from './ChangeRequestScheduledDialogue';
 | 
			
		||||
    ChangeRequestScheduledDialog,
 | 
			
		||||
    ChangeRequestScheduledDialogProps,
 | 
			
		||||
} from './ChangeRequestScheduledDialog';
 | 
			
		||||
 | 
			
		||||
export const ChangeRequestApplyScheduledDialogue: FC<
 | 
			
		||||
    Omit<
 | 
			
		||||
        ChangeRequestScheduleDialogueProps,
 | 
			
		||||
        ChangeRequestScheduledDialogProps,
 | 
			
		||||
        'message' | 'title' | 'primaryButtonText' | 'permissionButton'
 | 
			
		||||
    >
 | 
			
		||||
    > & { projectId: string; environment: string }
 | 
			
		||||
> = ({ projectId, environment, disabled, onConfirm, ...rest }) => {
 | 
			
		||||
    const message =
 | 
			
		||||
        'Applying the changes now means the scheduled time will be ignored';
 | 
			
		||||
@ -21,7 +18,7 @@ export const ChangeRequestApplyScheduledDialogue: FC<
 | 
			
		||||
    const primaryButtonText = 'Apply changes now';
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ChangeRequestScheduledDialogue
 | 
			
		||||
        <ChangeRequestScheduledDialog
 | 
			
		||||
            message={message}
 | 
			
		||||
            title={title}
 | 
			
		||||
            primaryButtonText={primaryButtonText}
 | 
			
		||||
@ -45,7 +42,7 @@ export const ChangeRequestApplyScheduledDialogue: FC<
 | 
			
		||||
 | 
			
		||||
export const ChangeRequestRejectScheduledDialogue: FC<
 | 
			
		||||
    Omit<
 | 
			
		||||
        ChangeRequestScheduleDialogueProps,
 | 
			
		||||
        ChangeRequestScheduledDialogProps,
 | 
			
		||||
        'message' | 'title' | 'primaryButtonText'
 | 
			
		||||
    >
 | 
			
		||||
> = ({ ...rest }) => {
 | 
			
		||||
@ -55,7 +52,7 @@ export const ChangeRequestRejectScheduledDialogue: FC<
 | 
			
		||||
    const primaryButtonText = 'Reject changes';
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ChangeRequestScheduledDialogue
 | 
			
		||||
        <ChangeRequestScheduledDialog
 | 
			
		||||
            message={message}
 | 
			
		||||
            title={title}
 | 
			
		||||
            primaryButtonText={primaryButtonText}
 | 
			
		||||
 | 
			
		||||
@ -58,10 +58,12 @@ export const useChangeRequestApi = () => {
 | 
			
		||||
            state:
 | 
			
		||||
                | 'Approved'
 | 
			
		||||
                | 'Applied'
 | 
			
		||||
                | 'Scheduled'
 | 
			
		||||
                | 'Cancelled'
 | 
			
		||||
                | 'In review'
 | 
			
		||||
                | 'Rejected';
 | 
			
		||||
            comment?: string;
 | 
			
		||||
            scheduledAt?: string;
 | 
			
		||||
        },
 | 
			
		||||
    ) => {
 | 
			
		||||
        trackEvent('change_request', {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user