mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: reject change request dialog (#4491)
This commit is contained in:
parent
4ad370450d
commit
c58d325173
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
@ -23,6 +23,7 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
|||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { changesCount } from '../changesCount';
|
import { changesCount } from '../changesCount';
|
||||||
import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
|
import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers';
|
||||||
|
import { ChangeRequestRejectDialogue } from './ChangeRequestRejectDialog/ChangeRequestRejectDialog';
|
||||||
|
|
||||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||||
width: '30%',
|
width: '30%',
|
||||||
@ -65,6 +66,7 @@ const ChangeRequestBody = styled(Box)(({ theme }) => ({
|
|||||||
export const ChangeRequestOverview: FC = () => {
|
export const ChangeRequestOverview: FC = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||||
|
const [showRejectDialog, setShowRejectDialog] = useState(false);
|
||||||
const { user } = useAuthUser();
|
const { user } = useAuthUser();
|
||||||
const { isAdmin } = useContext(AccessContext);
|
const { isAdmin } = useContext(AccessContext);
|
||||||
const [commentText, setCommentText] = useState('');
|
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 onCancel = () => setShowCancelDialog(true);
|
||||||
const onCancelAbort = () => setShowCancelDialog(false);
|
const onCancelAbort = () => setShowCancelDialog(false);
|
||||||
|
const onCancelReject = () => setShowRejectDialog(false);
|
||||||
|
|
||||||
const isSelfReview =
|
const isSelfReview =
|
||||||
changeRequest?.createdBy.id === user?.id &&
|
changeRequest?.createdBy.id === user?.id &&
|
||||||
@ -212,6 +251,10 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
}
|
}
|
||||||
show={
|
show={
|
||||||
<ReviewButton
|
<ReviewButton
|
||||||
|
onReject={() =>
|
||||||
|
setShowRejectDialog(true)
|
||||||
|
}
|
||||||
|
onApprove={onApprove}
|
||||||
disabled={!allowChangeRequestActions}
|
disabled={!allowChangeRequestActions}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -278,6 +321,11 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
can't be reopened.
|
can't be reopened.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Dialogue>
|
</Dialogue>
|
||||||
|
<ChangeRequestRejectDialogue
|
||||||
|
open={showRejectDialog}
|
||||||
|
onConfirm={onReject}
|
||||||
|
onClose={onCancelReject}
|
||||||
|
/>
|
||||||
</ChangeRequestBody>
|
</ChangeRequestBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,17 +1,14 @@
|
|||||||
import React, { FC, useContext } from 'react';
|
import React, { FC, useContext } from 'react';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
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 {
|
import {
|
||||||
|
ClickAwayListener,
|
||||||
Grow,
|
Grow,
|
||||||
Paper,
|
|
||||||
Popper,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
ClickAwayListener,
|
Paper,
|
||||||
|
Popper,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
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 PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 { uiConfig } = useUiConfig();
|
||||||
const { isAdmin } = useContext(AccessContext);
|
const { isAdmin } = useContext(AccessContext);
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const id = useRequiredPathParam('id');
|
const id = useRequiredPathParam('id');
|
||||||
const { user } = useAuthUser();
|
const { user } = useAuthUser();
|
||||||
const { refetchChangeRequest, data } = useChangeRequest(projectId, id);
|
const { data } = useChangeRequest(projectId, id);
|
||||||
const { refetch: refetchChangeRequestOpen } =
|
|
||||||
usePendingChangeRequests(projectId);
|
|
||||||
const { setToastApiError, setToastData } = useToast();
|
|
||||||
|
|
||||||
const { changeState } = useChangeRequestApi();
|
|
||||||
|
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
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 = () => {
|
const onToggle = () => {
|
||||||
setOpen(prevOpen => !prevOpen);
|
setOpen(prevOpen => !prevOpen);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user