mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
feat(changerequests): add requested approvers to overview (#10232)
This commit is contained in:
parent
f2766b6b3b
commit
28caa82ad1
@ -34,6 +34,8 @@ import { ScheduleChangeRequestDialog } from './ChangeRequestScheduledDialogs/Sch
|
|||||||
import type { PlausibleChangeRequestState } from '../changeRequest.types';
|
import type { PlausibleChangeRequestState } from '../changeRequest.types';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useActionableChangeRequests } from 'hooks/api/getters/useActionableChangeRequests/useActionableChangeRequests';
|
import { useActionableChangeRequests } from 'hooks/api/getters/useActionableChangeRequests/useActionableChangeRequests';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag.ts';
|
||||||
|
import { ChangeRequestRequestedApprovers } from './ChangeRequestRequestedApprovers/ChangeRequestRequestedApprovers.tsx';
|
||||||
|
|
||||||
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
const StyledAsideBox = styled(Box)(({ theme }) => ({
|
||||||
width: '30%',
|
width: '30%',
|
||||||
@ -106,6 +108,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
useChangeRequestsEnabled(projectId);
|
useChangeRequestsEnabled(projectId);
|
||||||
const [disabled, setDisabled] = useState(false);
|
const [disabled, setDisabled] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const approversEnabled = useUiFlag('changeRequestApproverEmails');
|
||||||
|
|
||||||
if (!changeRequest) {
|
if (!changeRequest) {
|
||||||
return null;
|
return null;
|
||||||
@ -288,7 +291,19 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
<ChangeRequestBody>
|
<ChangeRequestBody>
|
||||||
<StyledAsideBox>
|
<StyledAsideBox>
|
||||||
<ChangeRequestTimeline {...timelineProps} />
|
<ChangeRequestTimeline {...timelineProps} />
|
||||||
<ChangeRequestReviewers changeRequest={changeRequest} />
|
<ConditionallyRender
|
||||||
|
condition={approversEnabled}
|
||||||
|
show={
|
||||||
|
<ChangeRequestRequestedApprovers
|
||||||
|
changeRequest={changeRequest}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<ChangeRequestReviewers
|
||||||
|
changeRequest={changeRequest}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</StyledAsideBox>
|
</StyledAsideBox>
|
||||||
<StyledPaper elevation={0}>
|
<StyledPaper elevation={0}>
|
||||||
<StyledInnerContainer>
|
<StyledInnerContainer>
|
||||||
|
@ -0,0 +1,372 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Paper,
|
||||||
|
styled,
|
||||||
|
IconButton,
|
||||||
|
useTheme,
|
||||||
|
type AutocompleteChangeReason,
|
||||||
|
type FilterOptionsState,
|
||||||
|
Checkbox,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
type ReviewerSchema,
|
||||||
|
useRequestedApprovers,
|
||||||
|
} from 'hooks/api/getters/useRequestedApprovers/useRequestedApprovers';
|
||||||
|
import { useState, type FC } from 'react';
|
||||||
|
import type { ChangeRequestType } from '../../changeRequest.types';
|
||||||
|
import {
|
||||||
|
ChangeRequestApprover,
|
||||||
|
ChangeRequestPending,
|
||||||
|
ChangeRequestRejector,
|
||||||
|
} from '../ChangeRequestReviewers/ChangeRequestReviewer.js';
|
||||||
|
import Add from '@mui/icons-material/Add';
|
||||||
|
import KeyboardArrowUp from '@mui/icons-material/KeyboardArrowUp';
|
||||||
|
import AutocompleteVirtual from 'component/common/AutocompleteVirtual/AutcompleteVirtual.js';
|
||||||
|
import {
|
||||||
|
type AvailableReviewerSchema,
|
||||||
|
useAvailableChangeRequestReviewers,
|
||||||
|
} from 'hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.js';
|
||||||
|
import { caseInsensitiveSearch } from 'utils/search.js';
|
||||||
|
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||||
|
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||||
|
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi.js';
|
||||||
|
|
||||||
|
export const StyledSpan = styled('span')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
fontSize: theme.fontSizes.bodySize,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
width: '100%',
|
||||||
|
'& > div': { width: '100%' },
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
marginRight: theme.spacing(-2),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledIconButton = styled(IconButton)(({ theme }) => ({
|
||||||
|
marginRight: theme.spacing(-1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StrechedLi = styled('li')({ width: '100%' });
|
||||||
|
|
||||||
|
const StyledOption = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
'& > span:first-of-type': {
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledTags = styled('div')(({ theme }) => ({
|
||||||
|
paddingLeft: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderOption = (
|
||||||
|
props: React.HTMLAttributes<HTMLLIElement>,
|
||||||
|
option: AvailableReviewerSchema,
|
||||||
|
{ selected }: { selected: boolean },
|
||||||
|
) => (
|
||||||
|
<StrechedLi {...props} key={option.id}>
|
||||||
|
<Checkbox
|
||||||
|
icon={<CheckBoxOutlineBlankIcon fontSize='small' />}
|
||||||
|
checkedIcon={<CheckBoxIcon fontSize='small' />}
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
checked={selected}
|
||||||
|
/>
|
||||||
|
<StyledOption>
|
||||||
|
<span>{option.name || option.username}</span>
|
||||||
|
<span>
|
||||||
|
{option.name && option.username
|
||||||
|
? option.username
|
||||||
|
: option.email}
|
||||||
|
</span>
|
||||||
|
</StyledOption>
|
||||||
|
</StrechedLi>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTags = (value: AvailableReviewerSchema[]) => (
|
||||||
|
<StyledTags>
|
||||||
|
{value.length > 1
|
||||||
|
? `${value.length} reviewers`
|
||||||
|
: value[0].name || value[0].username || value[0].email}
|
||||||
|
</StyledTags>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const ChangeRequestReviewersHeader: FC<{
|
||||||
|
canShowAddReviewers: boolean;
|
||||||
|
showAddReviewers: boolean;
|
||||||
|
actualApprovals: number;
|
||||||
|
minApprovals: number;
|
||||||
|
setShowAddReviewers: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
}> = ({
|
||||||
|
canShowAddReviewers,
|
||||||
|
showAddReviewers,
|
||||||
|
actualApprovals,
|
||||||
|
minApprovals,
|
||||||
|
setShowAddReviewers,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledBox sx={{ mb: 1 }}>
|
||||||
|
<StyledSpan>
|
||||||
|
Reviewers
|
||||||
|
<Typography
|
||||||
|
component='span'
|
||||||
|
color='text.secondary'
|
||||||
|
sx={{ ml: 1 }}
|
||||||
|
>
|
||||||
|
({actualApprovals}/{minApprovals} required)
|
||||||
|
</Typography>
|
||||||
|
</StyledSpan>
|
||||||
|
{canShowAddReviewers &&
|
||||||
|
(showAddReviewers ? (
|
||||||
|
<StyledIconButton
|
||||||
|
title='Close'
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddReviewers(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<KeyboardArrowUp />
|
||||||
|
</StyledIconButton>
|
||||||
|
) : (
|
||||||
|
<StyledIconButton
|
||||||
|
title='Request approvals'
|
||||||
|
onClick={() => {
|
||||||
|
setShowAddReviewers(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
</StyledIconButton>
|
||||||
|
))}
|
||||||
|
</StyledBox>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeRequestAddRequestedApprovers: FC<{
|
||||||
|
changeRequest: Pick<ChangeRequestType, 'id' | 'project' | 'environment'>;
|
||||||
|
saveClicked: (reviewers: AvailableReviewerSchema[]) => void;
|
||||||
|
existingReviewers: Pick<ReviewerSchema, 'id'>[];
|
||||||
|
}> = ({ changeRequest, saveClicked, existingReviewers }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [reviewers, setReviewers] = useState<AvailableReviewerSchema[]>([]);
|
||||||
|
const { reviewers: fetchedReviewers, loading: isLoading } =
|
||||||
|
useAvailableChangeRequestReviewers(
|
||||||
|
changeRequest.project,
|
||||||
|
changeRequest.environment,
|
||||||
|
);
|
||||||
|
const availableReviewers = fetchedReviewers.filter(
|
||||||
|
(reviewer) =>
|
||||||
|
!existingReviewers.some((existing) => existing.id === reviewer.id),
|
||||||
|
);
|
||||||
|
const autoCompleteChange = (
|
||||||
|
event: React.SyntheticEvent,
|
||||||
|
newValue: AvailableReviewerSchema[],
|
||||||
|
reason: AutocompleteChangeReason,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
event.type === 'keydown' &&
|
||||||
|
(event as React.KeyboardEvent).key === 'Backspace' &&
|
||||||
|
reason === 'removeOption'
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setReviewers(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterOptions = (
|
||||||
|
options: AvailableReviewerSchema[],
|
||||||
|
{ inputValue }: FilterOptionsState<AvailableReviewerSchema>,
|
||||||
|
) =>
|
||||||
|
options.filter(
|
||||||
|
({ name, username, email }) =>
|
||||||
|
caseInsensitiveSearch(inputValue, email) ||
|
||||||
|
caseInsensitiveSearch(inputValue, name) ||
|
||||||
|
caseInsensitiveSearch(inputValue, username),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledBox sx={{ mb: 4 }}>
|
||||||
|
<AutocompleteVirtual
|
||||||
|
sx={{ ml: 'auto', width: theme.spacing(40) }}
|
||||||
|
size='small'
|
||||||
|
limitTags={3}
|
||||||
|
openOnFocus
|
||||||
|
multiple
|
||||||
|
disableCloseOnSelect
|
||||||
|
value={reviewers as AvailableReviewerSchema[]}
|
||||||
|
onChange={autoCompleteChange}
|
||||||
|
options={availableReviewers}
|
||||||
|
renderOption={renderOption}
|
||||||
|
filterOptions={filterOptions}
|
||||||
|
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||||
|
getOptionLabel={(option: AvailableReviewerSchema) =>
|
||||||
|
option.email || option.name || option.username || ''
|
||||||
|
}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label={`Reviewers (${reviewers.length})`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderTags={(value) => renderTags(value)}
|
||||||
|
noOptionsText={isLoading ? 'Loading…' : 'No options'}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 2 }}
|
||||||
|
variant='contained'
|
||||||
|
color='primary'
|
||||||
|
disabled={false}
|
||||||
|
onClick={() => saveClicked(reviewers)}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChangeRequestRequestedApprovers: FC<{
|
||||||
|
changeRequest: Pick<
|
||||||
|
ChangeRequestType,
|
||||||
|
| 'id'
|
||||||
|
| 'project'
|
||||||
|
| 'approvals'
|
||||||
|
| 'rejections'
|
||||||
|
| 'state'
|
||||||
|
| 'minApprovals'
|
||||||
|
| 'environment'
|
||||||
|
>;
|
||||||
|
}> = ({ changeRequest }) => {
|
||||||
|
const [showAddReviewers, setShowAddReviewers] = useState(false);
|
||||||
|
const { reviewers: requestedReviewers, refetchReviewers } =
|
||||||
|
useRequestedApprovers(changeRequest.project, changeRequest.id);
|
||||||
|
const { updateRequestedApprovers } = useChangeRequestApi();
|
||||||
|
const canShowAddReviewers =
|
||||||
|
(changeRequest.state === 'Draft' ||
|
||||||
|
changeRequest.state === 'In review') &&
|
||||||
|
changeRequest.minApprovals > 0;
|
||||||
|
|
||||||
|
let reviewers = requestedReviewers.map((reviewer) => {
|
||||||
|
const approver = changeRequest.approvals.find(
|
||||||
|
(approval) => approval.createdBy.id === reviewer.id,
|
||||||
|
);
|
||||||
|
const rejector = changeRequest.rejections.find(
|
||||||
|
(rejection) => rejection.createdBy.id === reviewer.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: reviewer.id,
|
||||||
|
name: reviewer.username || reviewer.name || 'Unknown user',
|
||||||
|
imageUrl: reviewer.imageUrl,
|
||||||
|
status: approver ? 'approved' : rejector ? 'rejected' : 'pending',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
reviewers = [
|
||||||
|
...reviewers,
|
||||||
|
...changeRequest.approvals
|
||||||
|
.filter(
|
||||||
|
(approval) =>
|
||||||
|
!reviewers.find(
|
||||||
|
(r) => r.name === approval.createdBy.username,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((approval) => ({
|
||||||
|
id: approval.createdBy.id,
|
||||||
|
name: approval.createdBy.username || 'Unknown user',
|
||||||
|
imageUrl: approval.createdBy.imageUrl,
|
||||||
|
status: 'approved',
|
||||||
|
})),
|
||||||
|
...changeRequest.rejections
|
||||||
|
.filter(
|
||||||
|
(rejection) =>
|
||||||
|
!reviewers.find(
|
||||||
|
(r) => r.name === rejection.createdBy.username,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((rejection) => ({
|
||||||
|
id: rejection.createdBy.id,
|
||||||
|
name: rejection.createdBy.username || 'Unknown user',
|
||||||
|
imageUrl: rejection.createdBy.imageUrl,
|
||||||
|
status: 'rejected',
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const saveClicked = async (
|
||||||
|
selectedReviewers: AvailableReviewerSchema[],
|
||||||
|
) => {
|
||||||
|
if (selectedReviewers.length > 0) {
|
||||||
|
const tosend = [
|
||||||
|
...reviewers.map((reviewer) => reviewer.id),
|
||||||
|
...selectedReviewers.map((reviewer) => reviewer.id),
|
||||||
|
];
|
||||||
|
await updateRequestedApprovers(
|
||||||
|
changeRequest.project,
|
||||||
|
changeRequest.id,
|
||||||
|
tosend,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
refetchReviewers();
|
||||||
|
setShowAddReviewers(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginTop: theme.spacing(2),
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
paddingTop: theme.spacing(2),
|
||||||
|
borderRadius: (theme) => `${theme.shape.borderRadiusLarge}px`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ChangeRequestReviewersHeader
|
||||||
|
canShowAddReviewers={canShowAddReviewers}
|
||||||
|
showAddReviewers={showAddReviewers}
|
||||||
|
minApprovals={changeRequest.minApprovals}
|
||||||
|
actualApprovals={changeRequest.approvals.length}
|
||||||
|
setShowAddReviewers={setShowAddReviewers}
|
||||||
|
/>
|
||||||
|
{canShowAddReviewers && showAddReviewers && (
|
||||||
|
<ChangeRequestAddRequestedApprovers
|
||||||
|
changeRequest={changeRequest}
|
||||||
|
existingReviewers={reviewers}
|
||||||
|
saveClicked={saveClicked}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reviewers.map((reviewer) => (
|
||||||
|
<>
|
||||||
|
{reviewer.status === 'approved' && (
|
||||||
|
<ChangeRequestApprover
|
||||||
|
key={reviewer.name}
|
||||||
|
name={reviewer.name || 'Unknown user'}
|
||||||
|
imageUrl={reviewer.imageUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reviewer.status === 'rejected' && (
|
||||||
|
<ChangeRequestRejector
|
||||||
|
key={reviewer.name}
|
||||||
|
name={reviewer.name || 'Unknown user'}
|
||||||
|
imageUrl={reviewer.imageUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reviewer.status === 'pending' && (
|
||||||
|
<ChangeRequestPending
|
||||||
|
key={reviewer.name}
|
||||||
|
name={reviewer.name || 'Unknown user'}
|
||||||
|
imageUrl={reviewer.imageUrl}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
@ -3,6 +3,7 @@ import type { FC } from 'react';
|
|||||||
import { StyledAvatar } from '../ChangeRequestHeader/ChangeRequestHeader.styles';
|
import { StyledAvatar } from '../ChangeRequestHeader/ChangeRequestHeader.styles';
|
||||||
import CheckCircle from '@mui/icons-material/CheckCircle';
|
import CheckCircle from '@mui/icons-material/CheckCircle';
|
||||||
import Cancel from '@mui/icons-material/Cancel';
|
import Cancel from '@mui/icons-material/Cancel';
|
||||||
|
import Pending from '@mui/icons-material/Pending';
|
||||||
interface IChangeRequestReviewerProps {
|
interface IChangeRequestReviewerProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
@ -26,6 +27,11 @@ export const StyledErrorIcon = styled(Cancel)(({ theme }) => ({
|
|||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const StyledPendingIcon = styled(Pending)(({ theme }) => ({
|
||||||
|
color: theme.palette.neutral.main,
|
||||||
|
marginLeft: 'auto',
|
||||||
|
}));
|
||||||
|
|
||||||
export const ReviewerName = styled(Typography)({
|
export const ReviewerName = styled(Typography)({
|
||||||
maxWidth: '170px',
|
maxWidth: '170px',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
@ -39,7 +45,7 @@ export const ChangeRequestApprover: FC<IChangeRequestReviewerProps> = ({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox title='Approved'>
|
||||||
<StyledAvatar user={{ name, imageUrl }} />
|
<StyledAvatar user={{ name, imageUrl }} />
|
||||||
<ReviewerName variant='body1'>{name}</ReviewerName>
|
<ReviewerName variant='body1'>{name}</ReviewerName>
|
||||||
<StyledSuccessIcon />
|
<StyledSuccessIcon />
|
||||||
@ -52,10 +58,23 @@ export const ChangeRequestRejector: FC<IChangeRequestReviewerProps> = ({
|
|||||||
imageUrl,
|
imageUrl,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledBox>
|
<StyledBox title='Rejected'>
|
||||||
<StyledAvatar user={{ name, imageUrl }} />
|
<StyledAvatar user={{ name, imageUrl }} />
|
||||||
<ReviewerName variant='body1'>{name}</ReviewerName>
|
<ReviewerName variant='body1'>{name}</ReviewerName>
|
||||||
<StyledErrorIcon />
|
<StyledErrorIcon />
|
||||||
</StyledBox>
|
</StyledBox>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ChangeRequestPending: FC<IChangeRequestReviewerProps> = ({
|
||||||
|
name,
|
||||||
|
imageUrl,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledBox title='Pending'>
|
||||||
|
<StyledAvatar user={{ name, imageUrl }} />
|
||||||
|
<ReviewerName variant='body1'>{name}</ReviewerName>
|
||||||
|
<StyledPendingIcon />
|
||||||
|
</StyledBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -74,7 +74,7 @@ const renderOption = (
|
|||||||
const renderTags = (value: AvailableReviewerSchema[]) => (
|
const renderTags = (value: AvailableReviewerSchema[]) => (
|
||||||
<StyledTags>
|
<StyledTags>
|
||||||
{value.length > 1
|
{value.length > 1
|
||||||
? `${value.length} users selected`
|
? `${value.length} reviewers`
|
||||||
: value[0].name || value[0].username || value[0].email}
|
: value[0].name || value[0].username || value[0].email}
|
||||||
</StyledTags>
|
</StyledTags>
|
||||||
);
|
);
|
||||||
|
@ -74,7 +74,7 @@ export const EnvironmentChangeRequest: FC<{
|
|||||||
const [commentText, setCommentText] = useState('');
|
const [commentText, setCommentText] = useState('');
|
||||||
const { user } = useAuthUser();
|
const { user } = useAuthUser();
|
||||||
const [title, setTitle] = useState(environmentChangeRequest.title);
|
const [title, setTitle] = useState(environmentChangeRequest.title);
|
||||||
const { changeState, updateRequestedReviewers } = useChangeRequestApi();
|
const { changeState, updateRequestedApprovers } = useChangeRequestApi();
|
||||||
const [reviewers, setReviewers] = useState<AvailableReviewerSchema[]>([]);
|
const [reviewers, setReviewers] = useState<AvailableReviewerSchema[]>([]);
|
||||||
|
|
||||||
const [disabled, setDisabled] = useState(false);
|
const [disabled, setDisabled] = useState(false);
|
||||||
@ -83,7 +83,7 @@ export const EnvironmentChangeRequest: FC<{
|
|||||||
setDisabled(true);
|
setDisabled(true);
|
||||||
try {
|
try {
|
||||||
if (reviewers && reviewers.length > 0) {
|
if (reviewers && reviewers.length > 0) {
|
||||||
await updateRequestedReviewers(
|
await updateRequestedApprovers(
|
||||||
project,
|
project,
|
||||||
environmentChangeRequest.id,
|
environmentChangeRequest.id,
|
||||||
reviewers.map((reviewer) => reviewer.id),
|
reviewers.map((reviewer) => reviewer.id),
|
||||||
|
@ -190,17 +190,17 @@ export const useChangeRequestApi = () => {
|
|||||||
|
|
||||||
return makeRequest(req.caller, req.id);
|
return makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
const updateRequestedReviewers = async (
|
const updateRequestedApprovers = async (
|
||||||
project: string,
|
project: string,
|
||||||
changeRequestId: number,
|
changeRequestId: number,
|
||||||
reviewers: string[],
|
reviewers: number[],
|
||||||
) => {
|
) => {
|
||||||
trackEvent('change_request', {
|
trackEvent('change_request', {
|
||||||
props: {
|
props: {
|
||||||
eventType: 'reviewers updated',
|
eventType: 'approvers updated',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/reviewers`;
|
const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/approvers`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ reviewers }),
|
body: JSON.stringify({ reviewers }),
|
||||||
@ -217,7 +217,7 @@ export const useChangeRequestApi = () => {
|
|||||||
discardDraft,
|
discardDraft,
|
||||||
addComment,
|
addComment,
|
||||||
updateTitle,
|
updateTitle,
|
||||||
updateRequestedReviewers,
|
updateRequestedApprovers,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import handleErrorResponses from '../httpErrorResponseHandler.js';
|
|||||||
|
|
||||||
// TODO: These will likely be created by Orval next time it is run
|
// TODO: These will likely be created by Orval next time it is run
|
||||||
export interface AvailableReviewerSchema {
|
export interface AvailableReviewerSchema {
|
||||||
id: string;
|
id: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
email: string;
|
email: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler.js';
|
||||||
|
|
||||||
|
// TODO: These will likely be created by Orval next time it is run
|
||||||
|
export interface ReviewerSchema {
|
||||||
|
id: number;
|
||||||
|
name?: string;
|
||||||
|
email: string;
|
||||||
|
username?: string;
|
||||||
|
imageUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IReviewersResponse {
|
||||||
|
reviewers: ReviewerSchema[];
|
||||||
|
refetchReviewers: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRequestedApprovers = (
|
||||||
|
project: string,
|
||||||
|
changeRequestId: number,
|
||||||
|
): IReviewersResponse => {
|
||||||
|
const { data, error, mutate } = useSWR(
|
||||||
|
formatApiPath(
|
||||||
|
`api/admin/projects/${project}/change-requests/${changeRequestId}/approvers`,
|
||||||
|
),
|
||||||
|
fetcher,
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
reviewers: data?.reviewers || [],
|
||||||
|
loading: !error && !data,
|
||||||
|
refetchReviewers: () => mutate(),
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[data, error, mutate],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('Requested Approvers'))
|
||||||
|
.then((res) => res.json());
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user