mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: Change request reject UI (#4489)
This commit is contained in:
parent
e2717ab8d3
commit
3227e30f12
@ -12,6 +12,7 @@ const changeRequestWithDefaultChange = (
|
|||||||
) => {
|
) => {
|
||||||
const changeRequest: IChangeRequest = {
|
const changeRequest: IChangeRequest = {
|
||||||
approvals: [],
|
approvals: [],
|
||||||
|
rejections: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
createdBy: {
|
createdBy: {
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { Alert, Button, styled, Typography } from '@mui/material';
|
import { Alert, Box, Button, styled, Typography } from '@mui/material';
|
||||||
import { FC, useContext, useState } from 'react';
|
import { FC, useContext, useState } from 'react';
|
||||||
import { Box } from '@mui/material';
|
|
||||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
||||||
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
|
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
|
||||||
import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
|
import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
|
||||||
import {
|
|
||||||
ChangeRequestReviewers,
|
|
||||||
ChangeRequestReviewersHeader,
|
|
||||||
} from './ChangeRequestReviewers/ChangeRequestReviewers';
|
|
||||||
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
|
import { ChangeRequest } from '../ChangeRequest/ChangeRequest';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
|
||||||
@ -17,7 +12,6 @@ import { formatUnknownError } from 'utils/formatUnknownError';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import { ReviewButton } from './ReviewButton/ReviewButton';
|
import { ReviewButton } from './ReviewButton/ReviewButton';
|
||||||
import { ChangeRequestReviewer } from './ChangeRequestReviewers/ChangeRequestReviewer';
|
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
|
import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions';
|
||||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
@ -28,6 +22,7 @@ import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequ
|
|||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
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';
|
||||||
|
|
||||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||||
width: '30%',
|
width: '30%',
|
||||||
@ -161,25 +156,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
<ChangeRequestBody>
|
<ChangeRequestBody>
|
||||||
<StyledAsideBox>
|
<StyledAsideBox>
|
||||||
<ChangeRequestTimeline state={changeRequest.state} />
|
<ChangeRequestTimeline state={changeRequest.state} />
|
||||||
<ChangeRequestReviewers
|
<ChangeRequestReviewers changeRequest={changeRequest} />
|
||||||
header={
|
|
||||||
<ChangeRequestReviewersHeader
|
|
||||||
actualApprovals={changeRequest.approvals.length}
|
|
||||||
minApprovals={changeRequest.minApprovals}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{changeRequest.approvals?.map(approver => (
|
|
||||||
<ChangeRequestReviewer
|
|
||||||
key={approver.createdBy.username}
|
|
||||||
name={
|
|
||||||
approver.createdBy.username ||
|
|
||||||
'Unknown user'
|
|
||||||
}
|
|
||||||
imageUrl={approver.createdBy.imageUrl}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ChangeRequestReviewers>
|
|
||||||
</StyledAsideBox>
|
</StyledAsideBox>
|
||||||
<StyledPaper elevation={0}>
|
<StyledPaper elevation={0}>
|
||||||
<StyledInnerContainer>
|
<StyledInnerContainer>
|
||||||
@ -262,6 +239,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={
|
||||||
changeRequest.state !== 'Applied' &&
|
changeRequest.state !== 'Applied' &&
|
||||||
|
changeRequest.state !== 'Rejected' &&
|
||||||
changeRequest.state !== 'Cancelled' &&
|
changeRequest.state !== 'Cancelled' &&
|
||||||
(changeRequest.createdBy.id === user?.id ||
|
(changeRequest.createdBy.id === user?.id ||
|
||||||
isAdmin)
|
isAdmin)
|
||||||
|
@ -106,6 +106,10 @@ const ResolveComponent = ({ changeRequest }: IResolveComponentProps) => {
|
|||||||
return <Cancelled />;
|
return <Cancelled />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state === 'Rejected') {
|
||||||
|
return <Rejected />;
|
||||||
|
}
|
||||||
|
|
||||||
return <ReviewRequired minApprovals={changeRequest.minApprovals} />;
|
return <ReviewRequired minApprovals={changeRequest.minApprovals} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -207,3 +211,20 @@ const Cancelled = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Rejected = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledFlexAlignCenterBox>
|
||||||
|
<StyledErrorIcon />
|
||||||
|
<Box>
|
||||||
|
<StyledReviewTitle color={theme.palette.error.main}>
|
||||||
|
Changes rejected
|
||||||
|
</StyledReviewTitle>
|
||||||
|
</Box>
|
||||||
|
</StyledFlexAlignCenterBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ChangeRequestApprover } from './ChangeRequestReviewer';
|
||||||
|
import { IChangeRequestApproval } from '../../changeRequest.types';
|
||||||
|
|
||||||
|
interface ChangeRequestApprovalProps {
|
||||||
|
approvals: IChangeRequestApproval[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeRequestApprovals: FC<ChangeRequestApprovalProps> = ({
|
||||||
|
approvals = [],
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={approvals?.length > 0}
|
||||||
|
show={'Approved by'}
|
||||||
|
elseShow={'No approvals yet'}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
{approvals.map(approver => (
|
||||||
|
<ChangeRequestApprover
|
||||||
|
key={approver.createdBy.username}
|
||||||
|
name={approver.createdBy.username || 'Unknown user'}
|
||||||
|
imageUrl={approver.createdBy.imageUrl}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
@ -0,0 +1,25 @@
|
|||||||
|
import { IChangeRequestApproval } from '../../changeRequest.types';
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Typography } from '@mui/material';
|
||||||
|
import { ChangeRequestRejector } from './ChangeRequestReviewer';
|
||||||
|
|
||||||
|
interface ChangeRequestRejectionProps {
|
||||||
|
rejections: IChangeRequestApproval[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangeRequestRejections: FC<ChangeRequestRejectionProps> = ({
|
||||||
|
rejections = [],
|
||||||
|
}) => (
|
||||||
|
<>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
Rejected by
|
||||||
|
</Typography>
|
||||||
|
{rejections.map(rejector => (
|
||||||
|
<ChangeRequestRejector
|
||||||
|
key={rejector.createdBy.username}
|
||||||
|
name={rejector.createdBy.username || 'Unknown user'}
|
||||||
|
imageUrl={rejector.createdBy.imageUrl}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, styled, Typography } from '@mui/material';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { StyledAvatar } from '../ChangeRequestHeader/ChangeRequestHeader.styles';
|
import { StyledAvatar } from '../ChangeRequestHeader/ChangeRequestHeader.styles';
|
||||||
import { CheckCircle } from '@mui/icons-material';
|
import { CheckCircle, Cancel } from '@mui/icons-material';
|
||||||
|
|
||||||
interface IChangeRequestReviewerProps {
|
interface IChangeRequestReviewerProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -21,26 +21,41 @@ export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({
|
|||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ChangeRequestReviewer: FC<IChangeRequestReviewerProps> = ({
|
export const StyledErrorIcon = styled(Cancel)(({ theme }) => ({
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ReviewerName = styled(Typography)({
|
||||||
|
maxWidth: '170px',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
color: 'text.primary',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ChangeRequestApprover: FC<IChangeRequestReviewerProps> = ({
|
||||||
name,
|
name,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox>
|
||||||
<StyledAvatar src={imageUrl} />
|
<StyledAvatar src={imageUrl} />
|
||||||
<Typography
|
<ReviewerName variant="body1">{name}</ReviewerName>
|
||||||
variant="body1"
|
|
||||||
color="text.primary"
|
|
||||||
sx={{
|
|
||||||
maxWidth: '170px',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</Typography>
|
|
||||||
<StyledSuccessIcon />
|
<StyledSuccessIcon />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ChangeRequestRejector: FC<IChangeRequestReviewerProps> = ({
|
||||||
|
name,
|
||||||
|
imageUrl,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledBox>
|
||||||
|
<StyledAvatar src={imageUrl} />
|
||||||
|
<ReviewerName variant="body1">{name}</ReviewerName>
|
||||||
|
<StyledErrorIcon />
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
import { render } from 'utils/testRenderer';
|
||||||
|
import React from 'react';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { ChangeRequestReviewers } from './ChangeRequestReviewers';
|
||||||
|
|
||||||
|
test('Show approvers', async () => {
|
||||||
|
render(
|
||||||
|
<ChangeRequestReviewers
|
||||||
|
changeRequest={{
|
||||||
|
state: 'Approved',
|
||||||
|
minApprovals: 2,
|
||||||
|
rejections: [],
|
||||||
|
approvals: [
|
||||||
|
{
|
||||||
|
createdBy: {
|
||||||
|
id: 1,
|
||||||
|
username: 'approver',
|
||||||
|
imageUrl: 'approverImg',
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Approved by')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('approver')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Show rejectors', async () => {
|
||||||
|
render(
|
||||||
|
<ChangeRequestReviewers
|
||||||
|
changeRequest={{
|
||||||
|
state: 'Rejected',
|
||||||
|
minApprovals: 2,
|
||||||
|
rejections: [
|
||||||
|
{
|
||||||
|
createdBy: {
|
||||||
|
id: 2,
|
||||||
|
username: 'rejector',
|
||||||
|
imageUrl: 'rejectorImg',
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
approvals: [
|
||||||
|
{
|
||||||
|
createdBy: {
|
||||||
|
id: 1,
|
||||||
|
username: 'approver',
|
||||||
|
imageUrl: 'approverImg',
|
||||||
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Rejected by')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('rejector')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('Approved by')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('approver')).not.toBeInTheDocument();
|
||||||
|
});
|
@ -1,6 +1,9 @@
|
|||||||
import { Box, Paper, styled, Typography } from '@mui/material';
|
import { Box, Paper, styled, Typography } from '@mui/material';
|
||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode } from 'react';
|
||||||
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { ChangeRequestRejections } from './ChangeRequestRejections';
|
||||||
|
import { ChangeRequestApprovals } from './ChangeRequestApprovals';
|
||||||
|
import { IChangeRequest } from '../../changeRequest.types';
|
||||||
|
|
||||||
const StyledBox = styled(Box)(({ theme }) => ({
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
@ -20,7 +23,7 @@ export const ChangeRequestReviewersHeader: FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ChangeRequestReviewers: FC<{ header: ReactNode }> = ({
|
export const ChangeRequestReviewersWrapper: FC<{ header: ReactNode }> = ({
|
||||||
header,
|
header,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
@ -34,14 +37,35 @@ export const ChangeRequestReviewers: FC<{ header: ReactNode }> = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<StyledBox>{header}</StyledBox>
|
<StyledBox>{header}</StyledBox>
|
||||||
<Typography variant="body1" color="text.secondary">
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={React.Children.count(children) > 0}
|
|
||||||
show={'Approved by'}
|
|
||||||
elseShow={'No approvals yet'}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
{children}
|
{children}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ChangeRequestReviewers: FC<{
|
||||||
|
changeRequest: Pick<
|
||||||
|
IChangeRequest,
|
||||||
|
'approvals' | 'rejections' | 'state' | 'minApprovals'
|
||||||
|
>;
|
||||||
|
}> = ({ changeRequest }) => (
|
||||||
|
<ChangeRequestReviewersWrapper
|
||||||
|
header={
|
||||||
|
<ChangeRequestReviewersHeader
|
||||||
|
actualApprovals={changeRequest.approvals.length}
|
||||||
|
minApprovals={changeRequest.minApprovals}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={changeRequest.state === 'Rejected'}
|
||||||
|
show={
|
||||||
|
<ChangeRequestRejections
|
||||||
|
rejections={changeRequest.rejections}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<ChangeRequestApprovals approvals={changeRequest.approvals} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ChangeRequestReviewersWrapper>
|
||||||
|
);
|
||||||
|
@ -20,8 +20,11 @@ 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 { 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 }> = ({ disabled }) => {
|
||||||
|
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');
|
||||||
@ -53,6 +56,23 @@ export const ReviewButton: FC<{ disabled: boolean }> = ({ disabled }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
};
|
};
|
||||||
@ -117,6 +137,16 @@ export const ReviewButton: FC<{ disabled: boolean }> = ({ disabled }) => {
|
|||||||
<MenuItem onClick={onApprove}>
|
<MenuItem onClick={onApprove}>
|
||||||
Approve changes
|
Approve changes
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(
|
||||||
|
uiConfig?.flags?.changeRequestReject
|
||||||
|
)}
|
||||||
|
show={
|
||||||
|
<MenuItem onClick={onReject}>
|
||||||
|
Reject changes
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</MenuList>
|
</MenuList>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
@ -18,6 +18,7 @@ const changeRequest = {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
features: [],
|
features: [],
|
||||||
approvals: [],
|
approvals: [],
|
||||||
|
rejections: [],
|
||||||
comments: [],
|
comments: [],
|
||||||
segments: [],
|
segments: [],
|
||||||
};
|
};
|
||||||
|
@ -41,6 +41,12 @@ export const ChangeRequestStatusBadge: VFC<IChangeRequestStatusBadgeProps> = ({
|
|||||||
Cancelled
|
Cancelled
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
case 'Rejected':
|
||||||
|
return (
|
||||||
|
<Badge color="error" icon={<Close fontSize={'small'} />}>
|
||||||
|
Rejected
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <ReviewRequiredBadge />;
|
return <ReviewRequiredBadge />;
|
||||||
}
|
}
|
||||||
|
@ -72,11 +72,13 @@ export const ChangeRequestsTabs = ({
|
|||||||
const open = changeRequests.filter(
|
const open = changeRequests.filter(
|
||||||
changeRequest =>
|
changeRequest =>
|
||||||
changeRequest.state !== 'Cancelled' &&
|
changeRequest.state !== 'Cancelled' &&
|
||||||
|
changeRequest.state !== 'Rejected' &&
|
||||||
changeRequest.state !== 'Applied'
|
changeRequest.state !== 'Applied'
|
||||||
);
|
);
|
||||||
const closed = changeRequests.filter(
|
const closed = changeRequests.filter(
|
||||||
changeRequest =>
|
changeRequest =>
|
||||||
changeRequest.state === 'Cancelled' ||
|
changeRequest.state === 'Cancelled' ||
|
||||||
|
changeRequest.state === 'Rejected' ||
|
||||||
changeRequest.state === 'Applied'
|
changeRequest.state === 'Applied'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ export interface IChangeRequest {
|
|||||||
features: IChangeRequestFeature[];
|
features: IChangeRequestFeature[];
|
||||||
segments: ISegmentChange[];
|
segments: ISegmentChange[];
|
||||||
approvals: IChangeRequestApproval[];
|
approvals: IChangeRequestApproval[];
|
||||||
|
rejections: IChangeRequestApproval[];
|
||||||
comments: IChangeRequestComment[];
|
comments: IChangeRequestComment[];
|
||||||
conflict?: string;
|
conflict?: string;
|
||||||
}
|
}
|
||||||
@ -66,7 +67,8 @@ export type ChangeRequestState =
|
|||||||
| 'Approved'
|
| 'Approved'
|
||||||
| 'In review'
|
| 'In review'
|
||||||
| 'Applied'
|
| 'Applied'
|
||||||
| 'Cancelled';
|
| 'Cancelled'
|
||||||
|
| 'Rejected';
|
||||||
|
|
||||||
type ChangeRequestPayload =
|
type ChangeRequestPayload =
|
||||||
| ChangeRequestEnabled
|
| ChangeRequestEnabled
|
||||||
|
@ -55,7 +55,12 @@ export const useChangeRequestApi = () => {
|
|||||||
project: string,
|
project: string,
|
||||||
changeRequestId: number,
|
changeRequestId: number,
|
||||||
payload: {
|
payload: {
|
||||||
state: 'Approved' | 'Applied' | 'Cancelled' | 'In review';
|
state:
|
||||||
|
| 'Approved'
|
||||||
|
| 'Applied'
|
||||||
|
| 'Cancelled'
|
||||||
|
| 'In review'
|
||||||
|
| 'Rejected';
|
||||||
comment?: string;
|
comment?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
|
@ -56,6 +56,7 @@ export interface IFlags {
|
|||||||
configurableFeatureTypeLifetimes?: boolean;
|
configurableFeatureTypeLifetimes?: boolean;
|
||||||
frontendNavigationUpdate?: boolean;
|
frontendNavigationUpdate?: boolean;
|
||||||
segmentChangeRequests?: boolean;
|
segmentChangeRequests?: boolean;
|
||||||
|
changeRequestReject?: boolean;
|
||||||
lastSeenByEnvironment?: boolean;
|
lastSeenByEnvironment?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +69,7 @@ exports[`should create default config 1`] = `
|
|||||||
"flags": {
|
"flags": {
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
|
"changeRequestReject": false,
|
||||||
"configurableFeatureTypeLifetimes": false,
|
"configurableFeatureTypeLifetimes": false,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
"demo": false,
|
"demo": false,
|
||||||
@ -106,6 +107,7 @@ exports[`should create default config 1`] = `
|
|||||||
"experiments": {
|
"experiments": {
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
|
"changeRequestReject": false,
|
||||||
"configurableFeatureTypeLifetimes": false,
|
"configurableFeatureTypeLifetimes": false,
|
||||||
"customRootRolesKillSwitch": false,
|
"customRootRolesKillSwitch": false,
|
||||||
"demo": false,
|
"demo": false,
|
||||||
|
@ -29,6 +29,7 @@ export type IFlagKey =
|
|||||||
| 'frontendNavigationUpdate'
|
| 'frontendNavigationUpdate'
|
||||||
| 'lastSeenByEnvironment'
|
| 'lastSeenByEnvironment'
|
||||||
| 'segmentChangeRequests'
|
| 'segmentChangeRequests'
|
||||||
|
| 'changeRequestReject'
|
||||||
| 'customRootRolesKillSwitch';
|
| 'customRootRolesKillSwitch';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
@ -133,6 +134,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_SEGMENT_CHANGE_REQUESTS,
|
process.env.UNLEASH_EXPERIMENTAL_SEGMENT_CHANGE_REQUESTS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
changeRequestReject: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_CHANGE_REQUEST_REJECT,
|
||||||
|
false,
|
||||||
|
),
|
||||||
customRootRolesKillSwitch: parseEnvVarBoolean(
|
customRootRolesKillSwitch: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH,
|
process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH,
|
||||||
false,
|
false,
|
||||||
|
Loading…
Reference in New Issue
Block a user