1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: CR title from review page (#3509)

This commit is contained in:
Jaanus Sellin 2023-04-13 12:24:31 +03:00 committed by GitHub
parent a735ad7251
commit 353cb8c05d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 308 additions and 104 deletions

View File

@ -1,9 +1,16 @@
import { FC, useState } from 'react';
import { Box, Button, Divider, Typography, useTheme } from '@mui/material';
import React, { FC, useState } from 'react';
import {
Box,
Button,
Divider,
styled,
Typography,
useTheme,
} from '@mui/material';
import { IChangeRequest } from '../../changeRequest.types';
import { useNavigate } from 'react-router-dom';
import { ChangeRequestStatusBadge } from '../../ChangeRequestStatusBadge/ChangeRequestStatusBadge';
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { changesCount } from '../../changesCount';
import {
Separator,
@ -14,6 +21,7 @@ import {
import { CloudCircle } from '@mui/icons-material';
import { AddCommentField } from '../../ChangeRequestOverview/ChangeRequestComments/AddCommentField';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import { EnvironmentChangeRequestTitle } from './EnvironmentChangeRequestTitle';
const SubmitChangeRequestButton: FC<{ onClick: () => void; count: number }> = ({
onClick,
@ -24,6 +32,26 @@ const SubmitChangeRequestButton: FC<{ onClick: () => void; count: number }> = ({
</Button>
);
const ChangeRequestHeader = styled(Box)(({ theme }) => ({
padding: theme.spacing(3, 3, 1, 3),
border: '2px solid',
borderColor: theme.palette.divider,
borderRadius: `${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px 0 0`,
borderBottom: 'none',
overflow: 'hidden',
backgroundColor: theme.palette.neutral.light,
}));
const ChangeRequestContent = styled(Box)(({ theme }) => ({
padding: theme.spacing(0, 3, 3, 3),
border: '2px solid',
mb: 5,
borderColor: theme.palette.divider,
borderRadius: `0 0 ${theme.shape.borderRadiusLarge}px ${theme.shape.borderRadiusLarge}px`,
borderTop: 'none',
overflow: 'hidden',
}));
export const EnvironmentChangeRequest: FC<{
environmentChangeRequest: IChangeRequest;
onClose: () => void;
@ -36,114 +64,117 @@ export const EnvironmentChangeRequest: FC<{
const { user } = useAuthUser();
return (
<Box
key={environmentChangeRequest.id}
sx={{
padding: 3,
border: '2px solid',
mb: 5,
borderColor: theme => theme.palette.divider,
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
}}
>
<Box sx={{ display: 'flex', alignItems: 'end' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<CloudCircle
sx={theme => ({
color: theme.palette.primary.light,
mr: 0.5,
})}
/>
<Typography component="span" variant="h2">
{environmentChangeRequest.environment}
</Typography>
<Separator />
<UpdateCount
count={environmentChangeRequest.features.length}
/>
<Box key={environmentChangeRequest.id}>
<ChangeRequestHeader>
<Box sx={{ display: 'flex', alignItems: 'end' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<CloudCircle
sx={theme => ({
color: theme.palette.primary.light,
mr: 0.5,
})}
/>
<Typography component="span" variant="h2">
{environmentChangeRequest.environment}
</Typography>
<Separator />
<UpdateCount
count={environmentChangeRequest.features.length}
/>
</Box>
<Box sx={{ ml: 'auto' }}>
<ChangeRequestStatusBadge
state={environmentChangeRequest.state}
/>
</Box>
</Box>
<Box sx={{ ml: 'auto' }}>
<ChangeRequestStatusBadge
state={environmentChangeRequest.state}
/>
</Box>
</Box>
<Divider sx={{ my: 3 }} />
<Typography variant="body2" color="text.secondary">
You request changes for these feature toggles:
</Typography>
{children}
<ConditionallyRender
condition={environmentChangeRequest?.state === 'Draft'}
show={
<AddCommentField
user={user}
commentText={commentText}
onTypeComment={setCommentText}
></AddCommentField>
}
></ConditionallyRender>
<Box sx={{ display: 'flex', mt: 3 }}>
<Divider sx={{ my: 3 }} />
<EnvironmentChangeRequestTitle
environmentChangeRequest={environmentChangeRequest}
/>
</ChangeRequestHeader>
<ChangeRequestContent>
<Typography variant="body2" color="text.secondary">
You request changes for these feature toggles:
</Typography>
{children}
<ConditionallyRender
condition={environmentChangeRequest?.state === 'Draft'}
show={
<>
<SubmitChangeRequestButton
onClick={() =>
onReview(
environmentChangeRequest.id,
commentText
)
}
count={changesCount(environmentChangeRequest)}
/>
<AddCommentField
user={user}
commentText={commentText}
onTypeComment={setCommentText}
></AddCommentField>
}
></ConditionallyRender>
<Box sx={{ display: 'flex', mt: 3 }}>
<ConditionallyRender
condition={environmentChangeRequest?.state === 'Draft'}
show={
<>
<SubmitChangeRequestButton
onClick={() =>
onReview(
environmentChangeRequest.id,
commentText
)
}
count={changesCount(
environmentChangeRequest
)}
/>
<Button
sx={{ ml: 2 }}
variant="outlined"
onClick={() =>
onDiscard(environmentChangeRequest.id)
}
>
Discard changes
</Button>
</>
}
/>
<ConditionallyRender
condition={
environmentChangeRequest.state === 'In review' ||
environmentChangeRequest.state === 'Approved'
}
show={
<>
<StyledFlexAlignCenterBox>
<StyledSuccessIcon />
<Typography color={theme.palette.success.dark}>
Draft successfully sent to review
</Typography>
<Button
sx={{ marginLeft: 2 }}
sx={{ ml: 2 }}
variant="outlined"
onClick={() => {
onClose();
navigate(
`/projects/${environmentChangeRequest.project}/change-requests/${environmentChangeRequest.id}`
);
}}
onClick={() =>
onDiscard(environmentChangeRequest.id)
}
>
View change request page
Discard changes
</Button>
</StyledFlexAlignCenterBox>
</>
}
/>
</Box>
</>
}
/>
<ConditionallyRender
condition={
environmentChangeRequest.state === 'In review' ||
environmentChangeRequest.state === 'Approved'
}
show={
<>
<StyledFlexAlignCenterBox>
<StyledSuccessIcon />
<Typography
color={theme.palette.success.dark}
>
Draft successfully sent to review
</Typography>
<Button
sx={{ marginLeft: 2 }}
variant="outlined"
onClick={() => {
onClose();
navigate(
`/projects/${environmentChangeRequest.project}/change-requests/${environmentChangeRequest.id}`
);
}}
>
View change request page
</Button>
</StyledFlexAlignCenterBox>
</>
}
/>
</Box>
</ChangeRequestContent>
</Box>
);
};

View File

@ -0,0 +1,58 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { EnvironmentChangeRequestTitle } from './EnvironmentChangeRequestTitle';
import { ChangeRequestState } from '../../changeRequest.types';
import userEvent from '@testing-library/user-event';
import { testServerRoute, testServerSetup } from '../../../../utils/testServer';
import { render } from '../../../../utils/testRenderer';
import { UIProviderContainer } from '../../../providers/UIProvider/UIProviderContainer';
const changeRequest = {
id: 3,
state: 'Draft' as ChangeRequestState,
title: 'My title',
project: 'default',
environment: 'default',
minApprovals: 5,
createdBy: { id: 3, username: 'user', imageUrl: 'img' },
createdAt: new Date(),
features: [],
approvals: [],
comments: [],
};
const server = testServerSetup();
testServerRoute(
server,
`/api/admin/projects/${changeRequest.project}/change-requests/${changeRequest.id}/title`,
{},
'put'
);
testServerRoute(server, '/api/admin/ui-config', {});
test('can edit and save title', async () => {
const user = userEvent.setup();
render(
<UIProviderContainer>
<EnvironmentChangeRequestTitle
environmentChangeRequest={changeRequest}
/>
</UIProviderContainer>
);
const editButton = await screen.findByTestId('EditIcon');
await user.click(editButton);
const titleInput = await screen.findByDisplayValue(changeRequest.title);
await user.clear(titleInput);
await user.type(titleInput, 'New title');
const saveButton = await screen.findByText('Save');
await user.click(saveButton);
const newTitle = await screen.findByDisplayValue('New title');
expect(newTitle).toBeInTheDocument();
});

View File

@ -0,0 +1,89 @@
import React, { FC, useState } from 'react';
import { Box, Button, IconButton, styled } from '@mui/material';
import Input from 'component/common/Input/Input';
import { IChangeRequest } from '../../changeRequest.types';
import { Edit } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { formatUnknownError } from 'utils/formatUnknownError';
import useToast from 'hooks/useToast';
const StyledBox = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'row',
width: '100%',
'& > div': { width: '100%' },
justifyContent: 'space-between',
}));
export const EnvironmentChangeRequestTitle: FC<{
environmentChangeRequest: IChangeRequest;
}> = ({ environmentChangeRequest }) => {
const [title, setTitle] = useState(environmentChangeRequest.title);
const [isDisabled, setIsDisabled] = useState(true);
const { updateTitle } = useChangeRequestApi();
const { setToastData, setToastApiError } = useToast();
const toggleEditState = () => {
setIsDisabled(!isDisabled);
};
const saveTitle = async () => {
toggleEditState();
try {
await updateTitle(
environmentChangeRequest.project,
environmentChangeRequest.id,
title
);
setToastData({
type: 'success',
title: 'Change request title updated!',
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
return (
<StyledBox>
<Input
label="Change request title"
id="group-name"
value={title}
fullWidth
onChange={e => setTitle(e.target.value)}
disabled={isDisabled}
/>
<ConditionallyRender
condition={isDisabled}
show={
<IconButton onClick={toggleEditState}>
<Edit />
</IconButton>
}
/>
<ConditionallyRender
condition={!isDisabled}
show={
<>
<Button
variant="contained"
color="primary"
sx={theme => ({ marginLeft: theme.spacing(4) })}
onClick={() => saveTitle()}
>
Save
</Button>
<Button
sx={theme => ({ marginLeft: theme.spacing(1) })}
variant="outlined"
onClick={toggleEditState}
>
Cancel
</Button>{' '}
</>
}
/>
</StyledBox>
);
};

View File

@ -5,6 +5,7 @@ import { IUser } from '../../interfaces/user';
export interface IChangeRequest {
id: number;
state: ChangeRequestState;
title: string;
project: string;
environment: string;
minApprovals: number;

View File

@ -151,6 +151,29 @@ export const useChangeRequestApi = () => {
}
};
const updateTitle = async (
project: string,
changeRequestId: number,
title: string
) => {
trackEvent('change_request', {
props: {
eventType: 'title updated',
},
});
const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/title`;
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify({ title }),
});
try {
await makeRequest(req.caller, req.id);
} catch (e) {
throw e;
}
};
return {
addChange,
changeState,
@ -158,6 +181,7 @@ export const useChangeRequestApi = () => {
updateChangeRequestEnvironmentConfig,
discardDraft,
addComment,
updateTitle,
errors,
loading,
};

View File

@ -14,10 +14,11 @@ export const testServerSetup = (): SetupServerApi => {
export const testServerRoute = (
server: SetupServerApi,
path: string,
json: object
json: object,
method: 'get' | 'post' | 'put' | 'delete' = 'get'
) => {
server.use(
rest.get(path, (req, res, ctx) => {
rest[method](path, (req, res, ctx) => {
return res(ctx.json(json));
})
);