From 3227e30f12c1b863febd92d09bf3bb219fec5639 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 15 Aug 2023 09:08:26 +0200 Subject: [PATCH] feat: Change request reject UI (#4489) --- .../ChangeRequest/ChangeRequest.test.tsx | 1 + .../ChangeRequestOverview.tsx | 30 ++------- .../ChangeRequestReviewStatus.tsx | 21 ++++++ .../ChangeRequestApprovals.tsx | 30 +++++++++ .../ChangeRequestRejections.tsx | 25 +++++++ .../ChangeRequestReviewer.tsx | 43 ++++++++---- .../ChangeRequestReviewers.test.tsx | 65 +++++++++++++++++++ .../ChangeRequestReviewers.tsx | 40 +++++++++--- .../ReviewButton/ReviewButton.tsx | 30 +++++++++ .../EnvironmentChangeRequestTitle.test.tsx | 1 + .../ChangeRequestStatusBadge.tsx | 6 ++ .../ChangeRequestsTabs/ChangeRequestsTabs.tsx | 2 + .../changeRequest/changeRequest.types.ts | 4 +- .../useChangeRequestApi.ts | 7 +- frontend/src/interfaces/uiConfig.ts | 1 + .../__snapshots__/create-config.test.ts.snap | 2 + src/lib/types/experimental.ts | 5 ++ 17 files changed, 263 insertions(+), 50 deletions(-) create mode 100644 frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestApprovals.tsx create mode 100644 frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestRejections.tsx create mode 100644 frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.test.tsx diff --git a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx index a46c70e2f0..b32cf8a0ed 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.test.tsx @@ -12,6 +12,7 @@ const changeRequestWithDefaultChange = ( ) => { const changeRequest: IChangeRequest = { approvals: [], + rejections: [], comments: [], createdAt: new Date(), createdBy: { diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx index 323668bc61..8645398160 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestOverview.tsx @@ -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 { Box } from '@mui/material'; import { useChangeRequest } from 'hooks/api/getters/useChangeRequest/useChangeRequest'; import { ChangeRequestHeader } from './ChangeRequestHeader/ChangeRequestHeader'; import { ChangeRequestTimeline } from './ChangeRequestTimeline/ChangeRequestTimeline'; -import { - ChangeRequestReviewers, - ChangeRequestReviewersHeader, -} from './ChangeRequestReviewers/ChangeRequestReviewers'; import { ChangeRequest } from '../ChangeRequest/ChangeRequest'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; 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 Paper from '@mui/material/Paper'; import { ReviewButton } from './ReviewButton/ReviewButton'; -import { ChangeRequestReviewer } from './ChangeRequestReviewers/ChangeRequestReviewer'; import PermissionButton from 'component/common/PermissionButton/PermissionButton'; import { APPLY_CHANGE_REQUEST } from 'component/providers/AccessProvider/permissions'; 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 { Dialogue } from 'component/common/Dialogue/Dialogue'; import { changesCount } from '../changesCount'; +import { ChangeRequestReviewers } from './ChangeRequestReviewers/ChangeRequestReviewers'; const StyledAsideBox = styled(Box)(({ theme }) => ({ width: '30%', @@ -161,25 +156,7 @@ export const ChangeRequestOverview: FC = () => { - - } - > - {changeRequest.approvals?.map(approver => ( - - ))} - + @@ -262,6 +239,7 @@ export const ChangeRequestOverview: FC = () => { { return ; } + if (state === 'Rejected') { + return ; + } + return ; }; @@ -207,3 +211,20 @@ const Cancelled = () => { ); }; + +const Rejected = () => { + const theme = useTheme(); + + return ( + <> + + + + + Changes rejected + + + + + ); +}; diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestApprovals.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestApprovals.tsx new file mode 100644 index 0000000000..46f964f659 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestApprovals.tsx @@ -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 = ({ + approvals = [], +}) => ( + <> + + 0} + show={'Approved by'} + elseShow={'No approvals yet'} + /> + + {approvals.map(approver => ( + + ))} + +); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestRejections.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestRejections.tsx new file mode 100644 index 0000000000..a0a2b34f8c --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestRejections.tsx @@ -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 = ({ + rejections = [], +}) => ( + <> + + Rejected by + + {rejections.map(rejector => ( + + ))} + +); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewer.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewer.tsx index dccc63208b..069e9be147 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewer.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewer.tsx @@ -1,7 +1,7 @@ import { Box, styled, Typography } from '@mui/material'; import React, { FC } from 'react'; import { StyledAvatar } from '../ChangeRequestHeader/ChangeRequestHeader.styles'; -import { CheckCircle } from '@mui/icons-material'; +import { CheckCircle, Cancel } from '@mui/icons-material'; interface IChangeRequestReviewerProps { name?: string; @@ -21,26 +21,41 @@ export const StyledSuccessIcon = styled(CheckCircle)(({ theme }) => ({ marginLeft: 'auto', })); -export const ChangeRequestReviewer: FC = ({ +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 = ({ name, imageUrl, }) => { return ( - - {name} - + {name} ); }; + +export const ChangeRequestRejector: FC = ({ + name, + imageUrl, +}) => { + return ( + + + {name} + + + ); +}; diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.test.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.test.tsx new file mode 100644 index 0000000000..9a96e4f5c5 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.test.tsx @@ -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( + + ); + + expect(screen.getByText('Approved by')).toBeInTheDocument(); + expect(screen.getByText('approver')).toBeInTheDocument(); +}); + +test('Show rejectors', async () => { + render( + + ); + + expect(screen.getByText('Rejected by')).toBeInTheDocument(); + expect(screen.getByText('rejector')).toBeInTheDocument(); + expect(screen.queryByText('Approved by')).not.toBeInTheDocument(); + expect(screen.queryByText('approver')).not.toBeInTheDocument(); +}); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.tsx index e7477aff7a..375b295842 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ChangeRequestReviewers/ChangeRequestReviewers.tsx @@ -1,6 +1,9 @@ import { Box, Paper, styled, Typography } from '@mui/material'; import React, { FC, ReactNode } from 'react'; import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender'; +import { ChangeRequestRejections } from './ChangeRequestRejections'; +import { ChangeRequestApprovals } from './ChangeRequestApprovals'; +import { IChangeRequest } from '../../changeRequest.types'; const StyledBox = styled(Box)(({ theme }) => ({ 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, children, }) => { @@ -34,14 +37,35 @@ export const ChangeRequestReviewers: FC<{ header: ReactNode }> = ({ })} > {header} - - 0} - show={'Approved by'} - elseShow={'No approvals yet'} - /> - {children} ); }; + +export const ChangeRequestReviewers: FC<{ + changeRequest: Pick< + IChangeRequest, + 'approvals' | 'rejections' | 'state' | 'minApprovals' + >; +}> = ({ changeRequest }) => ( + + } + > + + } + elseShow={ + + } + /> + +); diff --git a/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx b/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx index 7fe6892779..dc084c58d8 100644 --- a/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestOverview/ReviewButton/ReviewButton.tsx @@ -20,8 +20,11 @@ import PermissionButton from 'component/common/PermissionButton/PermissionButton import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; import AccessContext from 'contexts/AccessContext'; 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 }) => { + const { uiConfig } = useUiConfig(); const { isAdmin } = useContext(AccessContext); const projectId = useRequiredPathParam('projectId'); 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 = () => { setOpen(prevOpen => !prevOpen); }; @@ -117,6 +137,16 @@ export const ReviewButton: FC<{ disabled: boolean }> = ({ disabled }) => { Approve changes + + Reject changes + + } + /> diff --git a/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequestTitle.test.tsx b/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequestTitle.test.tsx index d38ec15b40..11b91560ac 100644 --- a/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequestTitle.test.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequestTitle.test.tsx @@ -18,6 +18,7 @@ const changeRequest = { createdAt: new Date(), features: [], approvals: [], + rejections: [], comments: [], segments: [], }; diff --git a/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx b/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx index 0bcba8d5ec..9b25fc1ed4 100644 --- a/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx +++ b/frontend/src/component/changeRequest/ChangeRequestStatusBadge/ChangeRequestStatusBadge.tsx @@ -41,6 +41,12 @@ export const ChangeRequestStatusBadge: VFC = ({ Cancelled ); + case 'Rejected': + return ( + }> + Rejected + + ); default: return ; } diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx index 87ec28bfd1..824536ee3b 100644 --- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx +++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx @@ -72,11 +72,13 @@ export const ChangeRequestsTabs = ({ const open = changeRequests.filter( changeRequest => changeRequest.state !== 'Cancelled' && + changeRequest.state !== 'Rejected' && changeRequest.state !== 'Applied' ); const closed = changeRequests.filter( changeRequest => changeRequest.state === 'Cancelled' || + changeRequest.state === 'Rejected' || changeRequest.state === 'Applied' ); diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts index 0c37992733..de6cc0d293 100644 --- a/frontend/src/component/changeRequest/changeRequest.types.ts +++ b/frontend/src/component/changeRequest/changeRequest.types.ts @@ -15,6 +15,7 @@ export interface IChangeRequest { features: IChangeRequestFeature[]; segments: ISegmentChange[]; approvals: IChangeRequestApproval[]; + rejections: IChangeRequestApproval[]; comments: IChangeRequestComment[]; conflict?: string; } @@ -66,7 +67,8 @@ export type ChangeRequestState = | 'Approved' | 'In review' | 'Applied' - | 'Cancelled'; + | 'Cancelled' + | 'Rejected'; type ChangeRequestPayload = | ChangeRequestEnabled diff --git a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts index d247a30cdc..9ca5422d6c 100644 --- a/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts +++ b/frontend/src/hooks/api/actions/useChangeRequestApi/useChangeRequestApi.ts @@ -55,7 +55,12 @@ export const useChangeRequestApi = () => { project: string, changeRequestId: number, payload: { - state: 'Approved' | 'Applied' | 'Cancelled' | 'In review'; + state: + | 'Approved' + | 'Applied' + | 'Cancelled' + | 'In review' + | 'Rejected'; comment?: string; } ) => { diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts index 4554d81565..fcbd073237 100644 --- a/frontend/src/interfaces/uiConfig.ts +++ b/frontend/src/interfaces/uiConfig.ts @@ -56,6 +56,7 @@ export interface IFlags { configurableFeatureTypeLifetimes?: boolean; frontendNavigationUpdate?: boolean; segmentChangeRequests?: boolean; + changeRequestReject?: boolean; lastSeenByEnvironment?: boolean; } diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 345a873436..545d952ef3 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -69,6 +69,7 @@ exports[`should create default config 1`] = ` "flags": { "anonymiseEventLog": false, "caseInsensitiveInOperators": false, + "changeRequestReject": false, "configurableFeatureTypeLifetimes": false, "customRootRolesKillSwitch": false, "demo": false, @@ -106,6 +107,7 @@ exports[`should create default config 1`] = ` "experiments": { "anonymiseEventLog": false, "caseInsensitiveInOperators": false, + "changeRequestReject": false, "configurableFeatureTypeLifetimes": false, "customRootRolesKillSwitch": false, "demo": false, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 1e51d2c05b..71b7d8ee9e 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -29,6 +29,7 @@ export type IFlagKey = | 'frontendNavigationUpdate' | 'lastSeenByEnvironment' | 'segmentChangeRequests' + | 'changeRequestReject' | 'customRootRolesKillSwitch'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -133,6 +134,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_SEGMENT_CHANGE_REQUESTS, false, ), + changeRequestReject: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_CHANGE_REQUEST_REJECT, + false, + ), customRootRolesKillSwitch: parseEnvVarBoolean( process.env.UNLEASH_EXPERIMENTAL_CUSTOM_ROOT_ROLES_KILL_SWITCH, false,