1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: reject change request dialog (#4491)

This commit is contained in:
Mateusz Kwasniewski 2023-08-15 12:49:40 +02:00 committed by GitHub
parent 4ad370450d
commit c58d325173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 69 deletions

View File

@ -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<IAddStrategyMessageProps> = ({
payload,
environment,
}) => (
<>
<Typography component="span">Add </Typography>
<strong>
{formatStrategyName(payload?.name || '')} strategy
</strong> to <strong>{environment}</strong>
</>
);

View File

@ -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={
<ReviewButton
onReject={() =>
setShowRejectDialog(true)
}
onApprove={onApprove}
disabled={!allowChangeRequestActions}
/>
}
@ -278,6 +321,11 @@ export const ChangeRequestOverview: FC = () => {
can't be reopened.
</Typography>
</Dialogue>
<ChangeRequestRejectDialogue
open={showRejectDialog}
onConfirm={onReject}
onClose={onCancelReject}
/>
</ChangeRequestBody>
</>
);

View File

@ -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('<ChangeRequestRejectDialogue />', () => {
test('submits the typed comment to onConfirm', () => {
const handleConfirm = vi.fn();
const handleClose = vi.fn();
render(
<ChangeRequestRejectDialogue
open={true}
onConfirm={handleConfirm}
onClose={handleClose}
/>
);
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');
});
});

View File

@ -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<IChangeRequestDialogueProps> = ({
open,
onConfirm,
onClose,
}) => {
const [commentText, setCommentText] = useState('');
return (
<Dialogue
open={open}
primaryButtonText="Reject changes"
secondaryButtonText="Cancel"
onClick={() => onConfirm(commentText)}
onClose={onClose}
title="Reject changes"
fullWidth
>
<Box>Add an optional comment why you reject those changes</Box>
<TextField
sx={{ mt: 1 }}
variant="outlined"
placeholder="Add your comment here"
fullWidth
multiline
minRows={2}
onChange={e => setCommentText(e.target.value)}
value={commentText}
/>
</Dialogue>
);
};

View File

@ -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<HTMLButtonElement>(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);
};

View File

@ -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,
);
};