mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
feat: CR title from review page (#3509)
This commit is contained in:
parent
a735ad7251
commit
353cb8c05d
@ -1,9 +1,16 @@
|
|||||||
import { FC, useState } from 'react';
|
import React, { FC, useState } from 'react';
|
||||||
import { Box, Button, Divider, Typography, useTheme } from '@mui/material';
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
styled,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from '@mui/material';
|
||||||
import { IChangeRequest } from '../../changeRequest.types';
|
import { IChangeRequest } from '../../changeRequest.types';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ChangeRequestStatusBadge } from '../../ChangeRequestStatusBadge/ChangeRequestStatusBadge';
|
import { ChangeRequestStatusBadge } from '../../ChangeRequestStatusBadge/ChangeRequestStatusBadge';
|
||||||
import { ConditionallyRender } from '../../../common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { changesCount } from '../../changesCount';
|
import { changesCount } from '../../changesCount';
|
||||||
import {
|
import {
|
||||||
Separator,
|
Separator,
|
||||||
@ -14,6 +21,7 @@ import {
|
|||||||
import { CloudCircle } from '@mui/icons-material';
|
import { CloudCircle } from '@mui/icons-material';
|
||||||
import { AddCommentField } from '../../ChangeRequestOverview/ChangeRequestComments/AddCommentField';
|
import { AddCommentField } from '../../ChangeRequestOverview/ChangeRequestComments/AddCommentField';
|
||||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
|
import { EnvironmentChangeRequestTitle } from './EnvironmentChangeRequestTitle';
|
||||||
|
|
||||||
const SubmitChangeRequestButton: FC<{ onClick: () => void; count: number }> = ({
|
const SubmitChangeRequestButton: FC<{ onClick: () => void; count: number }> = ({
|
||||||
onClick,
|
onClick,
|
||||||
@ -24,6 +32,26 @@ const SubmitChangeRequestButton: FC<{ onClick: () => void; count: number }> = ({
|
|||||||
</Button>
|
</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<{
|
export const EnvironmentChangeRequest: FC<{
|
||||||
environmentChangeRequest: IChangeRequest;
|
environmentChangeRequest: IChangeRequest;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -36,114 +64,117 @@ export const EnvironmentChangeRequest: FC<{
|
|||||||
const { user } = useAuthUser();
|
const { user } = useAuthUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box key={environmentChangeRequest.id}>
|
||||||
key={environmentChangeRequest.id}
|
<ChangeRequestHeader>
|
||||||
sx={{
|
<Box sx={{ display: 'flex', alignItems: 'end' }}>
|
||||||
padding: 3,
|
<Box
|
||||||
border: '2px solid',
|
sx={{
|
||||||
mb: 5,
|
display: 'flex',
|
||||||
borderColor: theme => theme.palette.divider,
|
alignItems: 'center',
|
||||||
borderRadius: theme => `${theme.shape.borderRadiusLarge}px`,
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<CloudCircle
|
||||||
<Box sx={{ display: 'flex', alignItems: 'end' }}>
|
sx={theme => ({
|
||||||
<Box
|
color: theme.palette.primary.light,
|
||||||
sx={{
|
mr: 0.5,
|
||||||
display: 'flex',
|
})}
|
||||||
alignItems: 'center',
|
/>
|
||||||
}}
|
<Typography component="span" variant="h2">
|
||||||
>
|
{environmentChangeRequest.environment}
|
||||||
<CloudCircle
|
</Typography>
|
||||||
sx={theme => ({
|
<Separator />
|
||||||
color: theme.palette.primary.light,
|
<UpdateCount
|
||||||
mr: 0.5,
|
count={environmentChangeRequest.features.length}
|
||||||
})}
|
/>
|
||||||
/>
|
</Box>
|
||||||
<Typography component="span" variant="h2">
|
<Box sx={{ ml: 'auto' }}>
|
||||||
{environmentChangeRequest.environment}
|
<ChangeRequestStatusBadge
|
||||||
</Typography>
|
state={environmentChangeRequest.state}
|
||||||
<Separator />
|
/>
|
||||||
<UpdateCount
|
</Box>
|
||||||
count={environmentChangeRequest.features.length}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ ml: 'auto' }}>
|
<Divider sx={{ my: 3 }} />
|
||||||
<ChangeRequestStatusBadge
|
<EnvironmentChangeRequestTitle
|
||||||
state={environmentChangeRequest.state}
|
environmentChangeRequest={environmentChangeRequest}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</ChangeRequestHeader>
|
||||||
</Box>
|
<ChangeRequestContent>
|
||||||
<Divider sx={{ my: 3 }} />
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Typography variant="body2" color="text.secondary">
|
You request changes for these feature toggles:
|
||||||
You request changes for these feature toggles:
|
</Typography>
|
||||||
</Typography>
|
{children}
|
||||||
{children}
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={environmentChangeRequest?.state === 'Draft'}
|
|
||||||
show={
|
|
||||||
<AddCommentField
|
|
||||||
user={user}
|
|
||||||
commentText={commentText}
|
|
||||||
onTypeComment={setCommentText}
|
|
||||||
></AddCommentField>
|
|
||||||
}
|
|
||||||
></ConditionallyRender>
|
|
||||||
<Box sx={{ display: 'flex', mt: 3 }}>
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={environmentChangeRequest?.state === 'Draft'}
|
condition={environmentChangeRequest?.state === 'Draft'}
|
||||||
show={
|
show={
|
||||||
<>
|
<AddCommentField
|
||||||
<SubmitChangeRequestButton
|
user={user}
|
||||||
onClick={() =>
|
commentText={commentText}
|
||||||
onReview(
|
onTypeComment={setCommentText}
|
||||||
environmentChangeRequest.id,
|
></AddCommentField>
|
||||||
commentText
|
}
|
||||||
)
|
></ConditionallyRender>
|
||||||
}
|
<Box sx={{ display: 'flex', mt: 3 }}>
|
||||||
count={changesCount(environmentChangeRequest)}
|
<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
|
<Button
|
||||||
sx={{ marginLeft: 2 }}
|
sx={{ ml: 2 }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => {
|
onClick={() =>
|
||||||
onClose();
|
onDiscard(environmentChangeRequest.id)
|
||||||
navigate(
|
}
|
||||||
`/projects/${environmentChangeRequest.project}/change-requests/${environmentChangeRequest.id}`
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
View change request page
|
Discard changes
|
||||||
</Button>
|
</Button>
|
||||||
</StyledFlexAlignCenterBox>
|
</>
|
||||||
</>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<ConditionallyRender
|
||||||
</Box>
|
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>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -5,6 +5,7 @@ import { IUser } from '../../interfaces/user';
|
|||||||
export interface IChangeRequest {
|
export interface IChangeRequest {
|
||||||
id: number;
|
id: number;
|
||||||
state: ChangeRequestState;
|
state: ChangeRequestState;
|
||||||
|
title: string;
|
||||||
project: string;
|
project: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
minApprovals: number;
|
minApprovals: number;
|
||||||
|
@ -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 {
|
return {
|
||||||
addChange,
|
addChange,
|
||||||
changeState,
|
changeState,
|
||||||
@ -158,6 +181,7 @@ export const useChangeRequestApi = () => {
|
|||||||
updateChangeRequestEnvironmentConfig,
|
updateChangeRequestEnvironmentConfig,
|
||||||
discardDraft,
|
discardDraft,
|
||||||
addComment,
|
addComment,
|
||||||
|
updateTitle,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
|
@ -14,10 +14,11 @@ export const testServerSetup = (): SetupServerApi => {
|
|||||||
export const testServerRoute = (
|
export const testServerRoute = (
|
||||||
server: SetupServerApi,
|
server: SetupServerApi,
|
||||||
path: string,
|
path: string,
|
||||||
json: object
|
json: object,
|
||||||
|
method: 'get' | 'post' | 'put' | 'delete' = 'get'
|
||||||
) => {
|
) => {
|
||||||
server.use(
|
server.use(
|
||||||
rest.get(path, (req, res, ctx) => {
|
rest[method](path, (req, res, ctx) => {
|
||||||
return res(ctx.json(json));
|
return res(ctx.json(json));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user