diff --git a/frontend/src/component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/AddStrategyMessage.tsx b/frontend/src/component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/AddStrategyMessage.tsx deleted file mode 100644 index 93aaafaa79..0000000000 --- a/frontend/src/component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/AddStrategyMessage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { VFC } from 'react'; -import { Typography } from '@mui/material'; -import { formatStrategyName } from 'utils/strategyNames'; -import { IFeatureStrategyPayload } from 'interfaces/strategy'; - -interface IAddStrategyMessageProps { - payload?: IFeatureStrategyPayload; - environment?: string; -} - -export const AddStrategyMessage: VFC = ({ - payload, - environment, -}) => ( - <> - Add - - {formatStrategyName(payload?.name || '')} strategy - to {environment} - -); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx index 8645398160..5f584ac168 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx @@ -23,6 +23,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { changesCount } from '../changesCount'; import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers'; +import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog/ChangeRequestRejectDialog'; const StyledAsideBox = styled(Box)(({ theme }) => ({ width: '30%', @@ -65,6 +66,7 @@ const ChangeRequestBody = styled(Box)(({ theme }) => ({ export const ChangeRequestOverview: FC = () => { const projectId = useRequiredPathParam('projectId'); const [showCancelDialog, setShowCancelDialog] = useState(false); + const [showRejectDialog, setShowRejectDialog] = useState(false); const { user } = useAuthUser(); const { isAdmin } = useContext(AccessContext); const [commentText, setCommentText] = useState(''); @@ -139,8 +141,45 @@ export const ChangeRequestOverview: FC = () => { } }; + const onReject = async (comment?: string) => { + try { + await changeState(projectId, Number(id), { + state: 'Rejected', + comment, + }); + setShowRejectDialog(false); + refetchChangeRequest(); + refetchChangeRequestOpen(); + setToastData({ + type: 'success', + title: 'Success', + text: 'Changes rejected', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + + const onApprove = async () => { + try { + await changeState(projectId, Number(id), { + state: 'Approved', + }); + refetchChangeRequest(); + refetchChangeRequestOpen(); + setToastData({ + type: 'success', + title: 'Success', + text: 'Changes approved', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + const onCancel = () => setShowCancelDialog(true); const onCancelAbort = () => setShowCancelDialog(false); + const onCancelReject = () => setShowRejectDialog(false); const isSelfReview = changeRequest?.createdBy.id === user?.id && @@ -212,6 +251,10 @@ export const ChangeRequestOverview: FC = () => { } show={ + setShowRejectDialog(true) + } + onApprove={onApprove} disabled={!allowChangeRequestActions} /> } @@ -278,6 +321,11 @@ export const ChangeRequestOverview: FC = () => { can't be reopened. + ); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestRejectDialog/ChangeRequestRejectDialog.test.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestRejectDialog/ChangeRequestRejectDialog.test.tsx new file mode 100644 index 0000000000..c49bd19d34 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestRejectDialog/ChangeRequestRejectDialog.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { vi } from 'vitest'; +import { fireEvent, screen } from '@testing-library/react'; +import { render } from 'utils/testRenderer'; +import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog'; + +describe('', () => { + test('submits the typed comment to onConfirm', () => { + const handleConfirm = vi.fn(); + const handleClose = vi.fn(); + + render( + + ); + + const commentInput = screen.getByPlaceholderText( + 'Add your comment here' + ); + fireEvent.change(commentInput, { target: { value: 'Test Comment' } }); + + const rejectButton = screen.getByRole('button', { + name: /Reject changes/i, + }); + fireEvent.click(rejectButton); + + expect(handleConfirm).toHaveBeenCalledWith('Test Comment'); + }); +}); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestRejectDialog/ChangeRequestRejectDialog.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestRejectDialog/ChangeRequestRejectDialog.tsx new file mode 100644 index 0000000000..d090e6cdf4 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestRejectDialog/ChangeRequestRejectDialog.tsx @@ -0,0 +1,41 @@ +import { FC, useState } from 'react'; +import { TextField, Box } from '@mui/material'; +import { Dialogue } from '../../../common/Dialogue/Dialogue'; + +interface IChangeRequestDialogueProps { + open: boolean; + onConfirm: (comment?: string) => void; + onClose: () => void; +} + +export const ChangeRequestRejectDialogue: FC = ({ + open, + onConfirm, + onClose, +}) => { + const [commentText, setCommentText] = useState(''); + + return ( + onConfirm(commentText)} + onClose={onClose} + title="Reject changes" + fullWidth + > + Add an optional comment why you reject those changes + setCommentText(e.target.value)} + value={commentText} + /> + + ); +}; diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx index dc084c58d8..12689d9ae0 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx @@ -1,17 +1,14 @@ import React, { FC, useContext } from 'react'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest'; -import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; -import { formatUnknownError } from 'utils/formatUnknownError'; -import useToast from 'hooks/useToast'; import { + ClickAwayListener, Grow, - Paper, - Popper, MenuItem, MenuList, - ClickAwayListener, + Paper, + Popper, } from '@mui/material'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; @@ -19,60 +16,24 @@ import { APPROVE_CHANGE_REQUEST } from 'component/providers/AccessProvider/permi import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; import AccessContext from 'contexts/AccessContext'; -import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -export const ReviewButton: FC<{ disabled: boolean }> = ({ disabled }) => { +export const ReviewButton: FC<{ + disabled: boolean; + onReject: () => void; + onApprove: () => void; +}> = ({ disabled, onReject, onApprove }) => { const { uiConfig } = useUiConfig(); const { isAdmin } = useContext(AccessContext); const projectId = useRequiredPathParam('projectId'); const id = useRequiredPathParam('id'); const { user } = useAuthUser(); - const { refetchChangeRequest, data } = useChangeRequest(projectId, id); - const { refetch: refetchChangeRequestOpen } = - usePendingChangeRequests(projectId); - const { setToastApiError, setToastData } = useToast(); - - const { changeState } = useChangeRequestApi(); + const { data } = useChangeRequest(projectId, id); const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); - const onApprove = async () => { - try { - await changeState(projectId, Number(id), { - state: 'Approved', - }); - refetchChangeRequest(); - refetchChangeRequestOpen(); - setToastData({ - type: 'success', - title: 'Success', - text: 'Changes approved', - }); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - } - }; - - const onReject = async () => { - try { - await changeState(projectId, Number(id), { - state: 'Rejected', - }); - refetchChangeRequest(); - refetchChangeRequestOpen(); - setToastData({ - type: 'success', - title: 'Success', - text: 'Changes rejected', - }); - } catch (error: unknown) { - setToastApiError(formatUnknownError(error)); - } - }; - const onToggle = () => { setOpen(prevOpen => !prevOpen); }; diff --git a/src/migrations/20230815065908-change-request-approve-reject-permission.js b/src/migrations/20230815065908-change-request-approve-reject-permission.js new file mode 100644 index 0000000000..e7a2031a9b --- /dev/null +++ b/src/migrations/20230815065908-change-request-approve-reject-permission.js @@ -0,0 +1,19 @@ +'use strict'; + +exports.up = function (db, callback) { + db.runSql( + ` + UPDATE permissions SET display_name = 'Approve/Reject change requests' WHERE permission = 'APPROVE_CHANGE_REQUEST'; + `, + callback, + ); +}; + +exports.down = function (db, callback) { + db.runSql( + ` + UPDATE permissions SET display_name = 'Approve change requests' WHERE permission = 'APPROVE_CHANGE_REQUEST'; + `, + callback, + ); +};