diff --git a/src/apps/copilots/src/copilots.routes.tsx b/src/apps/copilots/src/copilots.routes.tsx index 9d5921211..1a3f9a9e4 100644 --- a/src/apps/copilots/src/copilots.routes.tsx +++ b/src/apps/copilots/src/copilots.routes.tsx @@ -32,6 +32,7 @@ export const childRoutes = [ authRequired: true, element: , id: 'CopilotRequestForm', + rolesRequired: [UserRole.administrator, UserRole.projectManager] as UserRole[], route: '/requests/new', }, { diff --git a/src/apps/copilots/src/models/CopilotOpportunity.ts b/src/apps/copilots/src/models/CopilotOpportunity.ts index 5c47a4baf..891dfffbb 100644 --- a/src/apps/copilots/src/models/CopilotOpportunity.ts +++ b/src/apps/copilots/src/models/CopilotOpportunity.ts @@ -22,4 +22,5 @@ export interface CopilotOpportunity { startDate: Date, tzRestrictions: 'yes' | 'no', createdAt: Date, + members: Array, } diff --git a/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx b/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx index d961895e5..7b1dbacf3 100644 --- a/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx +++ b/src/apps/copilots/src/pages/copilot-opportunity-details/index.tsx @@ -125,6 +125,7 @@ const CopilotOpportunityDetails: FC<{}> = () => { } const application = copilotApplications && copilotApplications[0] + const isAlreadyMemberOfTheProject = profile && opportunity?.members?.includes(profile.userId) return ( = () => { isCopilot && copilotApplications && copilotApplications.length === 0 - && opportunity?.status === 'active' ? applyCopilotOpportunityButton : undefined + && opportunity?.status === 'active' + && !isAlreadyMemberOfTheProject ? applyCopilotOpportunityButton : undefined } infoComponent={(isCopilot && !(copilotApplications && copilotApplications.length === 0 diff --git a/src/apps/copilots/src/pages/copilot-request-form/index.tsx b/src/apps/copilots/src/pages/copilot-request-form/index.tsx index 5b58e950f..79aa395ee 100644 --- a/src/apps/copilots/src/pages/copilot-request-form/index.tsx +++ b/src/apps/copilots/src/pages/copilot-request-form/index.tsx @@ -1,14 +1,14 @@ import { FC, useContext, useMemo, useState } from 'react' -import { bind, isEmpty } from 'lodash' +import { bind, debounce, isEmpty } from 'lodash' import { toast } from 'react-toastify' import classNames from 'classnames' import { profileContext, ProfileContextData } from '~/libs/core' import { Button, IconSolid, InputDatePicker, InputMultiselectOption, - InputRadio, InputSelect, InputSelectOption, InputSelectReact, InputText, InputTextarea } from '~/libs/ui' + InputRadio, InputSelect, InputSelectReact, InputText, InputTextarea } from '~/libs/ui' import { InputSkillSelector } from '~/libs/shared' -import { ProjectsResponse, useProjects } from '../../services/projects' +import { getProjects } from '../../services/projects' import { ProjectTypes, ProjectTypeValues } from '../../constants' import { saveCopilotRequest } from '../../services/copilot-requests' @@ -20,20 +20,9 @@ const CopilotRequestForm: FC<{}> = () => { const [formValues, setFormValues] = useState({}) const [isFormChanged, setIsFormChanged] = useState(false) const [formErrors, setFormErrors] = useState({}) - const [searchTerm, setSearchTerm] = useState('') - const { data: projectsData }: ProjectsResponse = useProjects(searchTerm) const [existingCopilot, setExistingCopilot] = useState('') const [paymentType, setPaymentType] = useState('') - const projects = useMemo( - () => ( - projectsData - ? projectsData.map(project => ({ label: project.name, value: project.id })) - : [] - ), - [projectsData], - ) - const projectTypes = ProjectTypes ? ProjectTypes.map(project => ({ label: project, value: ProjectTypeValues[project], @@ -63,18 +52,12 @@ const CopilotRequestForm: FC<{}> = () => { setPaymentType(t) } - function filterProjects(option: InputSelectOption, value: string): boolean { - setSearchTerm(value) - return ( - option.label - ?.toString() - .toLowerCase() - .includes(value.toLowerCase()) ?? false - ) - } - - function handleProjectSearch(inputValue: string): void { - setSearchTerm(inputValue) + async function handleProjectSearch(inputValue: string): Promise> { + const response = await getProjects(inputValue) + return response.map(project => ({ label: project.name, value: project.id })) } function handleProjectSelect(option: React.ChangeEvent): void { @@ -268,6 +251,11 @@ const CopilotRequestForm: FC<{}> = () => { setFormErrors(updatedFormErrors) } + const debouncedProjectSearch = useMemo(() => debounce((inputValue: string, callback: (options: any[]) => void) => { + handleProjectSearch(inputValue) + .then(callback) + }, 300), []) + return (
@@ -290,15 +278,14 @@ const CopilotRequestForm: FC<{}> = () => {

Select the project you want the copilot for

diff --git a/src/apps/copilots/src/services/projects.ts b/src/apps/copilots/src/services/projects.ts index 9bdca01b0..45b26131e 100644 --- a/src/apps/copilots/src/services/projects.ts +++ b/src/apps/copilots/src/services/projects.ts @@ -42,3 +42,9 @@ export const useProjects = (search?: string, config?: {isPaused?: () => boolean, revalidateOnFocus: false, }) } + +export const getProjects = (search?: string, filter?: any): Promise => { + const params = { name: `"${search}"`, ...filter } + const url = buildUrl(baseUrl, params) + return xhrGetAsync(url) +} diff --git a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx index 48546fa7f..224f73928 100644 --- a/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx +++ b/src/libs/ui/lib/components/form/form-groups/form-input/input-select-react/InputSelectReact.tsx @@ -8,6 +8,8 @@ import { useRef, } from 'react' import { find } from 'lodash' +import AsyncCreatable from 'react-select/async-creatable' +import AsyncSelect from 'react-select/async' import CreatableSelect from 'react-select/creatable' import ReactSelect, { GroupBase, OptionsOrGroups } from 'react-select' import classNames from 'classnames' @@ -33,7 +35,7 @@ interface InputSelectReactProps { readonly name: string readonly onChange: (event: ChangeEvent) => void readonly onInputChange?: (newValue: string) => void - readonly options: OptionsOrGroups> + readonly options?: OptionsOrGroups> readonly placeholder?: string readonly tabIndex?: number readonly value?: string @@ -43,6 +45,8 @@ interface InputSelectReactProps { readonly onBlur?: (event: FocusEvent) => void readonly openMenuOnClick?: boolean readonly openMenuOnFocus?: boolean + readonly async?: boolean + readonly loadOptions?: (inputValue: string, callback: (option: any) => void) => void readonly filterOption?: (option: InputSelectOption, value: string) => boolean } @@ -120,9 +124,13 @@ const InputSelectReact: FC = props => { } as FocusEvent) } - const Input = useMemo(() => ( - props.creatable ? CreatableSelect : ReactSelect - ), [props.creatable]) + const Input = useMemo(() => { + if (props.async) { + return props.creatable ? AsyncCreatable : AsyncSelect + } + + return props.creatable ? CreatableSelect : ReactSelect + }, [props.creatable, props.async]) return ( = props => { styles.select, ) } + loadOptions={props.loadOptions} onChange={handleSelect} onInputChange={props.onInputChange} menuPortalTarget={menuPortalTarget}