diff --git a/frontend/src/component/changeRequest/ChangeRequestSidebar/DraftChangeRequestActions/DraftChangeRequestActions.tsx b/frontend/src/component/changeRequest/ChangeRequestSidebar/DraftChangeRequestActions/DraftChangeRequestActions.tsx
new file mode 100644
index 0000000000..65c4fad681
--- /dev/null
+++ b/frontend/src/component/changeRequest/ChangeRequestSidebar/DraftChangeRequestActions/DraftChangeRequestActions.tsx
@@ -0,0 +1,164 @@
+import type { FC } from 'react';
+import { styled, Button, Checkbox, TextField, useTheme } from '@mui/material';
+import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
+import CheckBoxIcon from '@mui/icons-material/CheckBox';
+import AutocompleteVirtual from 'component/common/AutocompleteVirtual/AutcompleteVirtual';
+import { caseInsensitiveSearch } from 'utils/search';
+import type { ChangeRequestType } from 'component/changeRequest/changeRequest.types';
+import { changesCount } from '../../changesCount.js';
+import {
+ type AvailableReviewerSchema,
+ useAvailableChangeRequestReviewers,
+} from 'hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.js';
+
+const SubmitChangeRequestButton: FC<{
+ onClick: () => void;
+ count: number;
+ disabled?: boolean;
+}> = ({ onClick, count, disabled = false }) => (
+
+);
+
+const StyledTags = styled('div')(({ theme }) => ({
+ paddingLeft: 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 renderOption = (
+ props: React.HTMLAttributes,
+ option: AvailableReviewerSchema,
+ { selected }: { selected: boolean },
+) => (
+
+ }
+ checkedIcon={}
+ style={{ marginRight: 8 }}
+ checked={selected}
+ />
+
+ {option.name || option.username}
+
+ {option.name && option.username
+ ? option.username
+ : option.email}
+
+
+
+);
+
+const renderTags = (value: AvailableReviewerSchema[]) => (
+
+ {value.length > 1
+ ? `${value.length} users selected`
+ : value[0].name || value[0].username || value[0].email}
+
+);
+
+export const DraftChangeRequestActions: FC<{
+ environmentChangeRequest: ChangeRequestType;
+ reviewers: AvailableReviewerSchema[];
+ setReviewers: React.Dispatch<
+ React.SetStateAction
+ >;
+ onReview: (changeState: (project: string) => Promise) => void;
+ onDiscard: (id: number) => void;
+ sendToReview: (project: string) => Promise;
+ disabled?: boolean;
+ setDisabled: (disabled: boolean) => void;
+}> = ({
+ environmentChangeRequest,
+ reviewers,
+ setReviewers,
+ onReview,
+ onDiscard,
+ sendToReview,
+ disabled,
+ setDisabled,
+}) => {
+ const theme = useTheme();
+ const { reviewers: availableReviewers, loading: isLoading } =
+ useAvailableChangeRequestReviewers(
+ environmentChangeRequest.project,
+ environmentChangeRequest.environment,
+ );
+
+ return (
+ <>
+ {
+ if (
+ event.type === 'keydown' &&
+ (event as React.KeyboardEvent).key === 'Backspace' &&
+ reason === 'removeOption'
+ ) {
+ return;
+ }
+ setReviewers(newValue);
+ }}
+ options={availableReviewers}
+ renderOption={renderOption}
+ filterOptions={(options, { inputValue }) =>
+ options.filter(
+ ({ name, username, email }) =>
+ caseInsensitiveSearch(inputValue, email) ||
+ caseInsensitiveSearch(inputValue, name) ||
+ caseInsensitiveSearch(inputValue, username),
+ )
+ }
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ getOptionLabel={(option: AvailableReviewerSchema) =>
+ option.email || option.name || option.username || ''
+ }
+ renderInput={(params) => (
+
+ )}
+ renderTags={(value) => renderTags(value)}
+ noOptionsText={isLoading ? 'Loading…' : 'No options'}
+ />
+ onReview(sendToReview)}
+ count={changesCount(environmentChangeRequest)}
+ disabled={disabled}
+ />
+
+
+ >
+ );
+};
diff --git a/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx b/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx
index 23aa8d456e..3d48ecae1b 100644
--- a/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequestSidebar/EnvironmentChangeRequest/EnvironmentChangeRequest.tsx
@@ -24,6 +24,9 @@ import Input from 'component/common/Input/Input';
import { ChangeRequestTitle } from './ChangeRequestTitle.tsx';
import { UpdateCount } from 'component/changeRequest/UpdateCount';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
+import { useUiFlag } from 'hooks/useUiFlag';
+import { DraftChangeRequestActions } from '../DraftChangeRequestActions/DraftChangeRequestActions.tsx';
+import type { AvailableReviewerSchema } from 'hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.ts';
const SubmitChangeRequestButton: FC<{
onClick: () => void;
@@ -71,11 +74,22 @@ export const EnvironmentChangeRequest: FC<{
const [commentText, setCommentText] = useState('');
const { user } = useAuthUser();
const [title, setTitle] = useState(environmentChangeRequest.title);
- const { changeState } = useChangeRequestApi();
+ const { changeState, updateRequestedReviewers } = useChangeRequestApi();
+ const [reviewers, setReviewers] = useState([]);
+
const [disabled, setDisabled] = useState(false);
+ const approversEnabled = useUiFlag('changeRequestApproverEmails');
const sendToReview = async (project: string) => {
setDisabled(true);
try {
+ if (reviewers && reviewers.length > 0) {
+ await updateRequestedReviewers(
+ project,
+ environmentChangeRequest.id,
+ reviewers.map((reviewer) => reviewer.id),
+ );
+ }
+
await changeState(project, environmentChangeRequest.id, 'Draft', {
state: 'In review',
comment: commentText,
@@ -153,27 +167,50 @@ export const EnvironmentChangeRequest: FC<{
- onReview(sendToReview)}
- count={changesCount(
- environmentChangeRequest,
- )}
- disabled={disabled}
- />
+
+ }
+ elseShow={
+ <>
+
+ onReview(sendToReview)
+ }
+ count={changesCount(
+ environmentChangeRequest,
+ )}
+ disabled={disabled}
+ />
-
- >
+
+ >
+ }
+ />
}
/>
{
return makeRequest(req.caller, req.id);
};
+ const updateRequestedReviewers = async (
+ project: string,
+ changeRequestId: number,
+ reviewers: string[],
+ ) => {
+ trackEvent('change_request', {
+ props: {
+ eventType: 'reviewers updated',
+ },
+ });
+ const path = `api/admin/projects/${project}/change-requests/${changeRequestId}/reviewers`;
+ const req = createRequest(path, {
+ method: 'PUT',
+ body: JSON.stringify({ reviewers }),
+ });
+ return makeRequest(req.caller, req.id);
+ };
return {
addChange,
@@ -200,6 +217,7 @@ export const useChangeRequestApi = () => {
discardDraft,
addComment,
updateTitle,
+ updateRequestedReviewers,
errors,
loading,
};
diff --git a/frontend/src/hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.ts b/frontend/src/hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.ts
new file mode 100644
index 0000000000..5764c5e1db
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useAvailableChangeRequestReviewers/useAvailableChangeRequestReviewers.ts
@@ -0,0 +1,47 @@
+import useSWR from 'swr';
+import { useMemo } from 'react';
+import { formatApiPath } from 'utils/formatPath';
+import handleErrorResponses from '../httpErrorResponseHandler.js';
+
+export interface AvailableReviewerSchema {
+ id: string;
+ name?: string;
+ email: string;
+ username?: string;
+ imageUrl?: string;
+}
+
+export interface IAvailableReviewersResponse {
+ reviewers: AvailableReviewerSchema[];
+ refetchReviewers: () => void;
+ loading: boolean;
+ error?: Error;
+}
+
+export const useAvailableChangeRequestReviewers = (
+ project: string,
+ environment: string,
+): IAvailableReviewersResponse => {
+ const { data, error, mutate } = useSWR(
+ formatApiPath(
+ `api/admin/projects/${project}/change-requests/available-reviewers/${environment}`,
+ ),
+ fetcher,
+ );
+
+ return useMemo(
+ () => ({
+ reviewers: data?.reviewers || [],
+ loading: !error && !data,
+ refetchReviewers: () => mutate(),
+ error,
+ }),
+ [data, error, mutate],
+ );
+};
+
+const fetcher = (path: string) => {
+ return fetch(path)
+ .then(handleErrorResponses('Available Change Request Reviewers'))
+ .then((res) => res.json());
+};
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 4ddb37bc1c..c743b5a24e 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -90,6 +90,7 @@ export type UiFlags = {
createFlagDialogCache?: boolean;
healthToTechDebt?: boolean;
improvedJsonDiff?: boolean;
+ changeRequestApproverEmails?: boolean;
};
export interface IVersionInfo {