mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
Feat/change request overview applied state (#2322)
* feat: review button * feat: add review button * fix: add to box * fix: separate function calls * fix: comment out reviewers * fix: type
This commit is contained in:
parent
147408045b
commit
d8db33ac7f
@ -22,7 +22,7 @@ export const ChangeRequestHeader: FC<{ changeRequest: IChangeRequest }> = ({
|
||||
<StyledHeader variant="h1">
|
||||
Change request #{changeRequest.id}
|
||||
</StyledHeader>
|
||||
<ChangeRequestStatusBadge state={changeRequest.state} />;
|
||||
<ChangeRequestStatusBadge state={changeRequest.state} />
|
||||
</StyledContainer>
|
||||
<StyledInnerContainer>
|
||||
<Typography variant="body2" sx={{ margin: 'auto 0' }}>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { styled } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { Box, Button, Paper } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest';
|
||||
import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader';
|
||||
import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline';
|
||||
@ -10,11 +11,42 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh
|
||||
import { ChangeRequestReviewStatus } from './ChangeRequestReviewStatus/ChangeRequestReviewStatus';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { ReviewButton } from './ReviewButton/ReviewButton';
|
||||
|
||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||
width: '30%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const StyledPaper = styled(Paper)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginLeft: theme.spacing(2),
|
||||
width: '70%',
|
||||
padding: theme.spacing(1, 2),
|
||||
borderRadius: theme.shape.borderRadiusLarge,
|
||||
}));
|
||||
|
||||
const StyledButtonBox = styled(Box)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
}));
|
||||
|
||||
const StyledInnerContainer = styled(Box)(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
}));
|
||||
|
||||
export const ChangeRequestOverview: FC = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const id = useRequiredPathParam('id');
|
||||
const { data: changeRequest } = useChangeRequest(projectId, id);
|
||||
const { data: changeRequest, refetchChangeRequest } = useChangeRequest(
|
||||
projectId,
|
||||
id
|
||||
);
|
||||
const { applyChanges } = useChangeRequestApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
||||
@ -25,10 +57,11 @@ export const ChangeRequestOverview: FC = () => {
|
||||
const onApplyChanges = async () => {
|
||||
try {
|
||||
await applyChanges(projectId, id);
|
||||
refetchChangeRequest();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'Changes appplied',
|
||||
text: 'Changes applied',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
@ -39,49 +72,36 @@ export const ChangeRequestOverview: FC = () => {
|
||||
<>
|
||||
<ChangeRequestHeader changeRequest={changeRequest} />
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: '30%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<StyledAsideBox>
|
||||
<ChangeRequestTimeline state={changeRequest.state} />
|
||||
<ChangeRequestReviewers />
|
||||
</Box>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={theme => ({
|
||||
marginTop: theme.spacing(2),
|
||||
marginLeft: theme.spacing(2),
|
||||
width: '70%',
|
||||
padding: 2,
|
||||
borderRadius: theme =>
|
||||
`${theme.shape.borderRadiusLarge}px`,
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
sx={theme => ({
|
||||
padding: theme.spacing(2),
|
||||
})}
|
||||
>
|
||||
{/* <ChangeRequestReviewers /> */}
|
||||
</StyledAsideBox>
|
||||
<StyledPaper elevation={0}>
|
||||
<StyledInnerContainer>
|
||||
Changes
|
||||
<ChangeRequest changeRequest={changeRequest} />
|
||||
<ChangeRequestReviewStatus
|
||||
approved={
|
||||
changeRequest.state === 'Approved' ||
|
||||
changeRequest.state === 'Applied'
|
||||
}
|
||||
state={changeRequest.state}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ marginTop: 2 }}
|
||||
onClick={onApplyChanges}
|
||||
>
|
||||
Apply changes
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
<StyledButtonBox>
|
||||
<ConditionallyRender
|
||||
condition={changeRequest.state === 'In review'}
|
||||
show={<ReviewButton />}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={changeRequest.state === 'Approved'}
|
||||
show={
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={onApplyChanges}
|
||||
>
|
||||
Apply changes
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</StyledButtonBox>
|
||||
</StyledInnerContainer>
|
||||
</StyledPaper>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@ -3,7 +3,12 @@ import { Cancel, CheckCircle } from '@mui/icons-material';
|
||||
import { Box, Typography, Divider } from '@mui/material';
|
||||
|
||||
const styledComponentPropCheck = () => (prop: string) =>
|
||||
prop !== 'color' && prop !== 'sx' && prop !== 'approved';
|
||||
prop !== 'color' &&
|
||||
prop !== 'sx' &&
|
||||
prop !== 'approved' &&
|
||||
prop !== 'border' &&
|
||||
prop !== 'bgColor' &&
|
||||
prop !== 'svgColor';
|
||||
|
||||
export const StyledFlexAlignCenterBox = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@ -31,11 +36,9 @@ export const StyledOuterContainer = styled(Box)(({ theme }) => ({
|
||||
|
||||
export const StyledButtonContainer = styled(Box, {
|
||||
shouldForwardProp: styledComponentPropCheck(),
|
||||
})<{ approved: boolean }>(({ theme, approved }) => ({
|
||||
})<{ bgColor: string; svgColor: string }>(({ theme, bgColor, svgColor }) => ({
|
||||
borderRadius: `${theme.shape.borderRadiusMedium}px`,
|
||||
backgroundColor: approved
|
||||
? theme.palette.success.main
|
||||
: theme.palette.tableHeaderBackground,
|
||||
backgroundColor: bgColor,
|
||||
padding: theme.spacing(1, 2),
|
||||
marginRight: theme.spacing(2),
|
||||
height: '45px',
|
||||
@ -44,9 +47,7 @@ export const StyledButtonContainer = styled(Box, {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
['svg']: {
|
||||
color: approved
|
||||
? theme.palette.tertiary.background
|
||||
: theme.palette.neutral.main,
|
||||
color: svgColor,
|
||||
},
|
||||
}));
|
||||
|
||||
@ -56,18 +57,16 @@ export const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||
|
||||
export const StyledReviewStatusContainer = styled(Box, {
|
||||
shouldForwardProp: styledComponentPropCheck(),
|
||||
})<{ approved: boolean }>(({ theme, approved }) => ({
|
||||
})<{ border: string }>(({ theme, border }) => ({
|
||||
borderRadius: `${theme.shape.borderRadiusLarge}px`,
|
||||
border: approved
|
||||
? `2px solid ${theme.palette.success.main}`
|
||||
: `1px solid ${theme.palette.tertiary.main}`,
|
||||
border: border,
|
||||
padding: theme.spacing(3),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
export const StyledReviewTitle = styled(Typography, {
|
||||
shouldForwardProp: styledComponentPropCheck(),
|
||||
})<{ approved: boolean }>(({ theme, approved }) => ({
|
||||
})<{ color: string }>(({ theme, color }) => ({
|
||||
fontWeight: 'bold',
|
||||
color: approved ? theme.palette.success.main : theme.palette.error.main,
|
||||
color,
|
||||
}));
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { FC } from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Box, Theme, Typography, useTheme } from '@mui/material';
|
||||
import { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
StyledOuterContainer,
|
||||
StyledButtonContainer,
|
||||
@ -12,40 +11,98 @@ import {
|
||||
StyledReviewTitle,
|
||||
StyledDivider,
|
||||
} from './ChangeRequestReviewStatus.styles';
|
||||
import { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
|
||||
interface ISuggestChangeReviewsStatusProps {
|
||||
approved: boolean;
|
||||
state: ChangeRequestState;
|
||||
}
|
||||
|
||||
const resolveBorder = (state: ChangeRequestState, theme: Theme) => {
|
||||
if (state === 'Approved') {
|
||||
return `2px solid ${theme.palette.success.main}`;
|
||||
}
|
||||
|
||||
if (state === 'Applied') {
|
||||
return `2px solid ${theme.palette.primary.main}`;
|
||||
}
|
||||
|
||||
return `1px solid ${theme.palette.tertiary.main}`;
|
||||
};
|
||||
|
||||
const resolveIconColors = (state: ChangeRequestState, theme: Theme) => {
|
||||
if (state === 'Approved') {
|
||||
return {
|
||||
bgColor: theme.palette.success.main!,
|
||||
svgColor: theme.palette.tertiary.background,
|
||||
};
|
||||
}
|
||||
|
||||
if (state === 'Applied') {
|
||||
return {
|
||||
bgColor: theme.palette.primary.main!,
|
||||
svgColor: theme.palette.tertiary.background,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
bgColor: theme.palette.tableHeaderBackground,
|
||||
svgColor: theme.palette.neutral.main!,
|
||||
};
|
||||
};
|
||||
|
||||
export const ChangeRequestReviewStatus: FC<
|
||||
ISuggestChangeReviewsStatusProps
|
||||
> = ({ approved }) => {
|
||||
> = ({ state }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledOuterContainer>
|
||||
<StyledButtonContainer approved={approved}>
|
||||
<StyledButtonContainer {...resolveIconColors(state, theme)}>
|
||||
<ChangesAppliedIcon
|
||||
style={{
|
||||
transform: `scale(1.5)`,
|
||||
}}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
<StyledReviewStatusContainer approved={approved}>
|
||||
<ConditionallyRender
|
||||
condition={approved}
|
||||
show={<Approved approved={approved} />}
|
||||
elseShow={<ReviewRequired approved={approved} />}
|
||||
/>
|
||||
<StyledReviewStatusContainer border={resolveBorder(state, theme)}>
|
||||
<ResolveComponent state={state} />
|
||||
</StyledReviewStatusContainer>
|
||||
</StyledOuterContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const Approved = ({ approved }: ISuggestChangeReviewsStatusProps) => {
|
||||
interface IResolveComponentProps {
|
||||
state: ChangeRequestState;
|
||||
}
|
||||
|
||||
const ResolveComponent = ({ state }: IResolveComponentProps) => {
|
||||
if (!state) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (state === 'Approved') {
|
||||
return <Approved />;
|
||||
}
|
||||
|
||||
if (state === 'Applied') {
|
||||
return <Applied />;
|
||||
}
|
||||
|
||||
if (state === 'Cancelled') {
|
||||
return <Cancelled />;
|
||||
}
|
||||
|
||||
return <ReviewRequired />;
|
||||
};
|
||||
|
||||
const Approved = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFlexAlignCenterBox>
|
||||
<StyledSuccessIcon />
|
||||
<Box>
|
||||
<StyledReviewTitle approved={approved}>
|
||||
<StyledReviewTitle color={theme.palette.success.main}>
|
||||
Changed approved
|
||||
</StyledReviewTitle>
|
||||
<Typography>
|
||||
@ -59,7 +116,7 @@ const Approved = ({ approved }: ISuggestChangeReviewsStatusProps) => {
|
||||
<StyledFlexAlignCenterBox>
|
||||
<StyledSuccessIcon />
|
||||
<Box>
|
||||
<StyledReviewTitle approved={approved}>
|
||||
<StyledReviewTitle color={theme.palette.success.main}>
|
||||
Changes are ready to be applied
|
||||
</StyledReviewTitle>
|
||||
</Box>
|
||||
@ -68,13 +125,15 @@ const Approved = ({ approved }: ISuggestChangeReviewsStatusProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ReviewRequired = ({ approved }: ISuggestChangeReviewsStatusProps) => {
|
||||
const ReviewRequired = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFlexAlignCenterBox>
|
||||
<StyledErrorIcon />
|
||||
<Box>
|
||||
<StyledReviewTitle approved={approved}>
|
||||
<StyledReviewTitle color={theme.palette.error.main}>
|
||||
Review required
|
||||
</StyledReviewTitle>
|
||||
<Typography>
|
||||
@ -88,10 +147,44 @@ const ReviewRequired = ({ approved }: ISuggestChangeReviewsStatusProps) => {
|
||||
|
||||
<StyledFlexAlignCenterBox>
|
||||
<StyledErrorIcon />
|
||||
<StyledReviewTitle approved={approved}>
|
||||
<StyledReviewTitle color={theme.palette.error.main}>
|
||||
Apply changes is blocked
|
||||
</StyledReviewTitle>
|
||||
</StyledFlexAlignCenterBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Applied = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFlexAlignCenterBox>
|
||||
<StyledSuccessIcon sx={{ color: theme.palette.primary.main }} />
|
||||
<Box>
|
||||
<StyledReviewTitle color={theme.palette.primary.main}>
|
||||
Changes applied
|
||||
</StyledReviewTitle>
|
||||
</Box>
|
||||
</StyledFlexAlignCenterBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Cancelled = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledFlexAlignCenterBox>
|
||||
<StyledErrorIcon />
|
||||
<Box>
|
||||
<StyledReviewTitle color={theme.palette.error.main}>
|
||||
Changes cancelled
|
||||
</StyledReviewTitle>
|
||||
</Box>
|
||||
</StyledFlexAlignCenterBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,132 @@
|
||||
import React 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 {
|
||||
Button,
|
||||
Grow,
|
||||
Paper,
|
||||
Popper,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
ClickAwayListener,
|
||||
} from '@mui/material';
|
||||
|
||||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||||
|
||||
export const ReviewButton = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const id = useRequiredPathParam('id');
|
||||
const { refetchChangeRequest } = useChangeRequest(projectId, id);
|
||||
const { setToastApiError, setToastData } = useToast();
|
||||
|
||||
const { changeState } = useChangeRequestApi();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
const onApprove = async () => {
|
||||
try {
|
||||
await changeState(projectId, Number(id), {
|
||||
state: 'Approved',
|
||||
});
|
||||
refetchChangeRequest();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'Changes approved',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const onReject = async () => {
|
||||
try {
|
||||
await changeState(projectId, Number(id), {
|
||||
state: 'Cancelled',
|
||||
});
|
||||
refetchChangeRequest();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'Changes rejected',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const onToggle = () => {
|
||||
setOpen(prevOpen => !prevOpen);
|
||||
};
|
||||
|
||||
const onClose = (event: Event) => {
|
||||
if (
|
||||
anchorRef.current &&
|
||||
anchorRef.current.contains(event.target as HTMLElement)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
variant="contained"
|
||||
aria-controls={open ? 'review-options-menu' : undefined}
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-label="review changes"
|
||||
aria-haspopup="menu"
|
||||
onClick={onToggle}
|
||||
ref={anchorRef}
|
||||
endIcon={<ArrowDropDownIcon />}
|
||||
>
|
||||
Review changes
|
||||
</Button>
|
||||
<Popper
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin:
|
||||
placement === 'bottom'
|
||||
? 'center top'
|
||||
: 'center bottom',
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={onClose}>
|
||||
<MenuList
|
||||
id="review-options-menu"
|
||||
autoFocusItem
|
||||
>
|
||||
<MenuItem onClick={onApprove}>
|
||||
Approve changes
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onReject}>
|
||||
Reject changes
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
@ -35,10 +35,7 @@ export const ChangeRequestStatusBadge: VFC<IChangeRequestStatusBadgeProps> = ({
|
||||
);
|
||||
case 'Cancelled':
|
||||
return (
|
||||
<Badge
|
||||
color="error"
|
||||
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
||||
>
|
||||
<Badge color="error" icon={<Close fontSize={'small'} />}>
|
||||
Cancelled
|
||||
</Badge>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user