From addda5b022e054156ba5b1b3ce1cdea92a18aa65 Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Tue, 7 Nov 2023 10:59:49 +0200 Subject: [PATCH] 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 Co-authored-by: Thomas Heartman --- .../ChangeRequestOverview.tsx | 66 +++++++++-- ...e.tsx => ChangeRequestScheduledDialog.tsx} | 12 +- .../ScheduleChangeRequestDialog.tsx | 107 ++++++++++++++++++ .../changeRequestScheduledDialogs.tsx | 21 ++-- .../useChangeRequestApi.ts | 2 + 5 files changed, 179 insertions(+), 29 deletions(-) rename frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/{ChangeRequestScheduledDialogue.tsx => ChangeRequestScheduledDialog.tsx} (84%) create mode 100644 frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/ScheduleChangeRequestDialog.tsx diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx index c1bd1a266d..43aac2d721 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx @@ -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 @@ -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={ <> + { /> ({ @@ -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 > diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/ScheduleChangeRequestDialog.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/ScheduleChangeRequestDialog.tsx new file mode 100644 index 0000000000..039acb9467 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/ScheduleChangeRequestDialog.tsx @@ -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 = + ({ + open, + onConfirm, + onClose, + title, + primaryButtonText, + projectId, + environment, + disabled, + scheduledAt, + }) => { + const [selectedDate, setSelectedDate] = useState( + scheduledAt ? new Date(scheduledAt) : new Date(), + ); + const [error, setError] = useState(undefined); + + const timezone = getBrowserTimezoneInHumanReadableUTCOffset(); + + return ( + onConfirm(selectedDate)} + permissionButton={ + onConfirm(selectedDate)} + projectId={projectId} + permission={APPLY_CHANGE_REQUEST} + environmentId={environment} + disabled={disabled} + > + {primaryButtonText} + + } + fullWidth + > + theme.spacing(2) }} + > + The time shown below is based on your browser's time zone. + + 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. + + + { + 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 + /> + + Your browser's time zone is {timezone} + + + + ); + }; diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/changeRequestScheduledDialogs.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/changeRequestScheduledDialogs.tsx index 3cfd98bb73..466fe507cf 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/changeRequestScheduledDialogs.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestScheduledDialogs/changeRequestScheduledDialogs.tsx @@ -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 ( - > = ({ ...rest }) => { @@ -55,7 +52,7 @@ export const ChangeRequestRejectScheduledDialogue: FC< const primaryButtonText = 'Reject changes'; return ( - { state: | 'Approved' | 'Applied' + | 'Scheduled' | 'Cancelled' | 'In review' | 'Rejected'; comment?: string; + scheduledAt?: string; }, ) => { trackEvent('change_request', {