mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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 { useNavigate } from 'react-router-dom';
 | 
			
		||||
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 }) => ({
 | 
			
		||||
    width: '30%',
 | 
			
		||||
@ -106,6 +108,7 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
        useChangeRequestsEnabled(projectId);
 | 
			
		||||
    const [disabled, setDisabled] = useState(false);
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const approversEnabled = useUiFlag('changeRequestApproverEmails');
 | 
			
		||||
 | 
			
		||||
    if (!changeRequest) {
 | 
			
		||||
        return null;
 | 
			
		||||
@ -288,7 +291,19 @@ export const ChangeRequestOverview: FC = () => {
 | 
			
		||||
            <ChangeRequestBody>
 | 
			
		||||
                <StyledAsideBox>
 | 
			
		||||
                    <ChangeRequestTimeline {...timelineProps} />
 | 
			
		||||
                    <ChangeRequestReviewers changeRequest={changeRequest} />
 | 
			
		||||
                    <ConditionallyRender
 | 
			
		||||
                        condition={approversEnabled}
 | 
			
		||||
                        show={
 | 
			
		||||
                            <ChangeRequestRequestedApprovers
 | 
			
		||||
                                changeRequest={changeRequest}
 | 
			
		||||
                            />
 | 
			
		||||
                        }
 | 
			
		||||
                        elseShow={
 | 
			
		||||
                            <ChangeRequestReviewers
 | 
			
		||||
                                changeRequest={changeRequest}
 | 
			
		||||
                            />
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledAsideBox>
 | 
			
		||||
                <StyledPaper elevation={0}>
 | 
			
		||||
                    <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 CheckCircle from '@mui/icons-material/CheckCircle';
 | 
			
		||||
import Cancel from '@mui/icons-material/Cancel';
 | 
			
		||||
import Pending from '@mui/icons-material/Pending';
 | 
			
		||||
interface IChangeRequestReviewerProps {
 | 
			
		||||
    name?: string;
 | 
			
		||||
    imageUrl?: string;
 | 
			
		||||
@ -26,6 +27,11 @@ export const StyledErrorIcon = styled(Cancel)(({ theme }) => ({
 | 
			
		||||
    marginLeft: 'auto',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const StyledPendingIcon = styled(Pending)(({ theme }) => ({
 | 
			
		||||
    color: theme.palette.neutral.main,
 | 
			
		||||
    marginLeft: 'auto',
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const ReviewerName = styled(Typography)({
 | 
			
		||||
    maxWidth: '170px',
 | 
			
		||||
    textOverflow: 'ellipsis',
 | 
			
		||||
@ -39,7 +45,7 @@ export const ChangeRequestApprover: FC<IChangeRequestReviewerProps> = ({
 | 
			
		||||
    imageUrl,
 | 
			
		||||
}) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledBox>
 | 
			
		||||
        <StyledBox title='Approved'>
 | 
			
		||||
            <StyledAvatar user={{ name, imageUrl }} />
 | 
			
		||||
            <ReviewerName variant='body1'>{name}</ReviewerName>
 | 
			
		||||
            <StyledSuccessIcon />
 | 
			
		||||
@ -52,10 +58,23 @@ export const ChangeRequestRejector: FC<IChangeRequestReviewerProps> = ({
 | 
			
		||||
    imageUrl,
 | 
			
		||||
}) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <StyledBox>
 | 
			
		||||
        <StyledBox title='Rejected'>
 | 
			
		||||
            <StyledAvatar user={{ name, imageUrl }} />
 | 
			
		||||
            <ReviewerName variant='body1'>{name}</ReviewerName>
 | 
			
		||||
            <StyledErrorIcon />
 | 
			
		||||
        </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[]) => (
 | 
			
		||||
    <StyledTags>
 | 
			
		||||
        {value.length > 1
 | 
			
		||||
            ? `${value.length} users selected`
 | 
			
		||||
            ? `${value.length} reviewers`
 | 
			
		||||
            : value[0].name || value[0].username || value[0].email}
 | 
			
		||||
    </StyledTags>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -74,7 +74,7 @@ export const EnvironmentChangeRequest: FC<{
 | 
			
		||||
    const [commentText, setCommentText] = useState('');
 | 
			
		||||
    const { user } = useAuthUser();
 | 
			
		||||
    const [title, setTitle] = useState(environmentChangeRequest.title);
 | 
			
		||||
    const { changeState, updateRequestedReviewers } = useChangeRequestApi();
 | 
			
		||||
    const { changeState, updateRequestedApprovers } = useChangeRequestApi();
 | 
			
		||||
    const [reviewers, setReviewers] = useState<AvailableReviewerSchema[]>([]);
 | 
			
		||||
 | 
			
		||||
    const [disabled, setDisabled] = useState(false);
 | 
			
		||||
@ -83,7 +83,7 @@ export const EnvironmentChangeRequest: FC<{
 | 
			
		||||
        setDisabled(true);
 | 
			
		||||
        try {
 | 
			
		||||
            if (reviewers && reviewers.length > 0) {
 | 
			
		||||
                await updateRequestedReviewers(
 | 
			
		||||
                await updateRequestedApprovers(
 | 
			
		||||
                    project,
 | 
			
		||||
                    environmentChangeRequest.id,
 | 
			
		||||
                    reviewers.map((reviewer) => reviewer.id),
 | 
			
		||||
 | 
			
		||||
@ -190,17 +190,17 @@ export const useChangeRequestApi = () => {
 | 
			
		||||
 | 
			
		||||
        return makeRequest(req.caller, req.id);
 | 
			
		||||
    };
 | 
			
		||||
    const updateRequestedReviewers = async (
 | 
			
		||||
    const updateRequestedApprovers = async (
 | 
			
		||||
        project: string,
 | 
			
		||||
        changeRequestId: number,
 | 
			
		||||
        reviewers: string[],
 | 
			
		||||
        reviewers: number[],
 | 
			
		||||
    ) => {
 | 
			
		||||
        trackEvent('change_request', {
 | 
			
		||||
            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, {
 | 
			
		||||
            method: 'PUT',
 | 
			
		||||
            body: JSON.stringify({ reviewers }),
 | 
			
		||||
@ -217,7 +217,7 @@ export const useChangeRequestApi = () => {
 | 
			
		||||
        discardDraft,
 | 
			
		||||
        addComment,
 | 
			
		||||
        updateTitle,
 | 
			
		||||
        updateRequestedReviewers,
 | 
			
		||||
        updateRequestedApprovers,
 | 
			
		||||
        errors,
 | 
			
		||||
        loading,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import handleErrorResponses from '../httpErrorResponseHandler.js';
 | 
			
		||||
 | 
			
		||||
// TODO: These will likely be created by Orval next time it is run
 | 
			
		||||
export interface AvailableReviewerSchema {
 | 
			
		||||
    id: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    name?: string;
 | 
			
		||||
    email: 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