1
0
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:
Fredrik Strand Oseberg 2022-11-03 12:43:03 +01:00 committed by GitHub
parent 147408045b
commit d8db33ac7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 318 additions and 77 deletions

View File

@ -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' }}>

View File

@ -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>
</>
);

View File

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

View File

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

View File

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

View File

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