1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +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"> <StyledHeader variant="h1">
Change request #{changeRequest.id} Change request #{changeRequest.id}
</StyledHeader> </StyledHeader>
<ChangeRequestStatusBadge state={changeRequest.state} />; <ChangeRequestStatusBadge state={changeRequest.state} />
</StyledContainer> </StyledContainer>
<StyledInnerContainer> <StyledInnerContainer>
<Typography variant="body2" sx={{ margin: 'auto 0' }}> <Typography variant="body2" sx={{ margin: 'auto 0' }}>

View File

@ -1,5 +1,6 @@
import { styled } from '@mui/material';
import { FC } from 'react'; 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 { 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';
@ -10,11 +11,42 @@ import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useCh
import { ChangeRequestReviewStatus } from './ChangeRequestReviewStatus/ChangeRequestReviewStatus'; import { ChangeRequestReviewStatus } from './ChangeRequestReviewStatus/ChangeRequestReviewStatus';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError'; 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 = () => { export const ChangeRequestOverview: FC = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const id = useRequiredPathParam('id'); const id = useRequiredPathParam('id');
const { data: changeRequest } = useChangeRequest(projectId, id); const { data: changeRequest, refetchChangeRequest } = useChangeRequest(
projectId,
id
);
const { applyChanges } = useChangeRequestApi(); const { applyChanges } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast(); const { setToastData, setToastApiError } = useToast();
@ -25,10 +57,11 @@ export const ChangeRequestOverview: FC = () => {
const onApplyChanges = async () => { const onApplyChanges = async () => {
try { try {
await applyChanges(projectId, id); await applyChanges(projectId, id);
refetchChangeRequest();
setToastData({ setToastData({
type: 'success', type: 'success',
title: 'Success', title: 'Success',
text: 'Changes appplied', text: 'Changes applied',
}); });
} catch (error: unknown) { } catch (error: unknown) {
setToastApiError(formatUnknownError(error)); setToastApiError(formatUnknownError(error));
@ -39,49 +72,36 @@ export const ChangeRequestOverview: FC = () => {
<> <>
<ChangeRequestHeader changeRequest={changeRequest} /> <ChangeRequestHeader changeRequest={changeRequest} />
<Box sx={{ display: 'flex' }}> <Box sx={{ display: 'flex' }}>
<Box <StyledAsideBox>
sx={{
width: '30%',
display: 'flex',
flexDirection: 'column',
}}
>
<ChangeRequestTimeline state={changeRequest.state} /> <ChangeRequestTimeline state={changeRequest.state} />
<ChangeRequestReviewers /> {/* <ChangeRequestReviewers /> */}
</Box> </StyledAsideBox>
<Paper <StyledPaper elevation={0}>
elevation={0} <StyledInnerContainer>
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),
})}
>
Changes Changes
<ChangeRequest changeRequest={changeRequest} /> <ChangeRequest changeRequest={changeRequest} />
<ChangeRequestReviewStatus <ChangeRequestReviewStatus
approved={ state={changeRequest.state}
changeRequest.state === 'Approved' ||
changeRequest.state === 'Applied'
}
/> />
<Button <StyledButtonBox>
variant="contained" <ConditionallyRender
sx={{ marginTop: 2 }} condition={changeRequest.state === 'In review'}
onClick={onApplyChanges} show={<ReviewButton />}
> />
Apply changes <ConditionallyRender
</Button> condition={changeRequest.state === 'Approved'}
</Box> show={
</Paper> <Button
variant="contained"
onClick={onApplyChanges}
>
Apply changes
</Button>
}
/>
</StyledButtonBox>
</StyledInnerContainer>
</StyledPaper>
</Box> </Box>
</> </>
); );

View File

@ -3,7 +3,12 @@ import { Cancel, CheckCircle } from '@mui/icons-material';
import { Box, Typography, Divider } from '@mui/material'; import { Box, Typography, Divider } from '@mui/material';
const styledComponentPropCheck = () => (prop: string) => 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 }) => ({ export const StyledFlexAlignCenterBox = styled(Box)(({ theme }) => ({
display: 'flex', display: 'flex',
@ -31,11 +36,9 @@ export const StyledOuterContainer = styled(Box)(({ theme }) => ({
export const StyledButtonContainer = styled(Box, { export const StyledButtonContainer = styled(Box, {
shouldForwardProp: styledComponentPropCheck(), shouldForwardProp: styledComponentPropCheck(),
})<{ approved: boolean }>(({ theme, approved }) => ({ })<{ bgColor: string; svgColor: string }>(({ theme, bgColor, svgColor }) => ({
borderRadius: `${theme.shape.borderRadiusMedium}px`, borderRadius: `${theme.shape.borderRadiusMedium}px`,
backgroundColor: approved backgroundColor: bgColor,
? theme.palette.success.main
: theme.palette.tableHeaderBackground,
padding: theme.spacing(1, 2), padding: theme.spacing(1, 2),
marginRight: theme.spacing(2), marginRight: theme.spacing(2),
height: '45px', height: '45px',
@ -44,9 +47,7 @@ export const StyledButtonContainer = styled(Box, {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
['svg']: { ['svg']: {
color: approved color: svgColor,
? theme.palette.tertiary.background
: theme.palette.neutral.main,
}, },
})); }));
@ -56,18 +57,16 @@ export const StyledDivider = styled(Divider)(({ theme }) => ({
export const StyledReviewStatusContainer = styled(Box, { export const StyledReviewStatusContainer = styled(Box, {
shouldForwardProp: styledComponentPropCheck(), shouldForwardProp: styledComponentPropCheck(),
})<{ approved: boolean }>(({ theme, approved }) => ({ })<{ border: string }>(({ theme, border }) => ({
borderRadius: `${theme.shape.borderRadiusLarge}px`, borderRadius: `${theme.shape.borderRadiusLarge}px`,
border: approved border: border,
? `2px solid ${theme.palette.success.main}`
: `1px solid ${theme.palette.tertiary.main}`,
padding: theme.spacing(3), padding: theme.spacing(3),
width: '100%', width: '100%',
})); }));
export const StyledReviewTitle = styled(Typography, { export const StyledReviewTitle = styled(Typography, {
shouldForwardProp: styledComponentPropCheck(), shouldForwardProp: styledComponentPropCheck(),
})<{ approved: boolean }>(({ theme, approved }) => ({ })<{ color: string }>(({ theme, color }) => ({
fontWeight: 'bold', fontWeight: 'bold',
color: approved ? theme.palette.success.main : theme.palette.error.main, color,
})); }));

View File

@ -1,7 +1,6 @@
import { FC } from 'react'; 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 { ReactComponent as ChangesAppliedIcon } from 'assets/icons/merge.svg';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { import {
StyledOuterContainer, StyledOuterContainer,
StyledButtonContainer, StyledButtonContainer,
@ -12,40 +11,98 @@ import {
StyledReviewTitle, StyledReviewTitle,
StyledDivider, StyledDivider,
} from './ChangeRequestReviewStatus.styles'; } from './ChangeRequestReviewStatus.styles';
import { ChangeRequestState } from 'component/changeRequest/changeRequest.types';
interface ISuggestChangeReviewsStatusProps { 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< export const ChangeRequestReviewStatus: FC<
ISuggestChangeReviewsStatusProps ISuggestChangeReviewsStatusProps
> = ({ approved }) => { > = ({ state }) => {
const theme = useTheme();
return ( return (
<StyledOuterContainer> <StyledOuterContainer>
<StyledButtonContainer approved={approved}> <StyledButtonContainer {...resolveIconColors(state, theme)}>
<ChangesAppliedIcon <ChangesAppliedIcon
style={{ style={{
transform: `scale(1.5)`, transform: `scale(1.5)`,
}} }}
/> />
</StyledButtonContainer> </StyledButtonContainer>
<StyledReviewStatusContainer approved={approved}> <StyledReviewStatusContainer border={resolveBorder(state, theme)}>
<ConditionallyRender <ResolveComponent state={state} />
condition={approved}
show={<Approved approved={approved} />}
elseShow={<ReviewRequired approved={approved} />}
/>
</StyledReviewStatusContainer> </StyledReviewStatusContainer>
</StyledOuterContainer> </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 ( return (
<> <>
<StyledFlexAlignCenterBox> <StyledFlexAlignCenterBox>
<StyledSuccessIcon /> <StyledSuccessIcon />
<Box> <Box>
<StyledReviewTitle approved={approved}> <StyledReviewTitle color={theme.palette.success.main}>
Changed approved Changed approved
</StyledReviewTitle> </StyledReviewTitle>
<Typography> <Typography>
@ -59,7 +116,7 @@ const Approved = ({ approved }: ISuggestChangeReviewsStatusProps) => {
<StyledFlexAlignCenterBox> <StyledFlexAlignCenterBox>
<StyledSuccessIcon /> <StyledSuccessIcon />
<Box> <Box>
<StyledReviewTitle approved={approved}> <StyledReviewTitle color={theme.palette.success.main}>
Changes are ready to be applied Changes are ready to be applied
</StyledReviewTitle> </StyledReviewTitle>
</Box> </Box>
@ -68,13 +125,15 @@ const Approved = ({ approved }: ISuggestChangeReviewsStatusProps) => {
); );
}; };
const ReviewRequired = ({ approved }: ISuggestChangeReviewsStatusProps) => { const ReviewRequired = () => {
const theme = useTheme();
return ( return (
<> <>
<StyledFlexAlignCenterBox> <StyledFlexAlignCenterBox>
<StyledErrorIcon /> <StyledErrorIcon />
<Box> <Box>
<StyledReviewTitle approved={approved}> <StyledReviewTitle color={theme.palette.error.main}>
Review required Review required
</StyledReviewTitle> </StyledReviewTitle>
<Typography> <Typography>
@ -88,10 +147,44 @@ const ReviewRequired = ({ approved }: ISuggestChangeReviewsStatusProps) => {
<StyledFlexAlignCenterBox> <StyledFlexAlignCenterBox>
<StyledErrorIcon /> <StyledErrorIcon />
<StyledReviewTitle approved={approved}> <StyledReviewTitle color={theme.palette.error.main}>
Apply changes is blocked Apply changes is blocked
</StyledReviewTitle> </StyledReviewTitle>
</StyledFlexAlignCenterBox> </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': case 'Cancelled':
return ( return (
<Badge <Badge color="error" icon={<Close fontSize={'small'} />}>
color="error"
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
>
Cancelled Cancelled
</Badge> </Badge>
); );