Browse Source

Merge pull request #7883 from overleaf/ii-institution-autocomplete

Institution autocomplete

GitOrigin-RevId: f0a42794ce9071ec7c0c5c2c4d499e8a027811f8
Timothée Alby 4 years ago
parent
commit
920a5921c7

+ 19 - 13
services/web/frontend/js/features/settings/components/emails/add-email.tsx

@@ -50,22 +50,27 @@ function AddEmail() {
     setNewEmailMatchedInstitution(institution || null)
   }
 
-  const handleAddNewEmail = () => {
-    const selectedKnownUniversity = countryCode
-      ? universities[countryCode]?.find(({ name }) => name === universityName)
-      : undefined
+  const getSelectedKnownUniversityId = (): number | undefined => {
+    if (countryCode) {
+      return universities[countryCode]?.find(
+        ({ name }) => name === universityName
+      )?.id
+    }
 
-    const knownUniversityData = universityName &&
-      selectedKnownUniversity && {
-        university: {
-          id: selectedKnownUniversity.id,
-        },
-        role,
-        department,
-      }
+    return newEmailMatchedInstitution?.university.id
+  }
 
+  const handleAddNewEmail = () => {
+    const selectedKnownUniversityId = getSelectedKnownUniversityId()
+    const knownUniversityData = selectedKnownUniversityId && {
+      university: {
+        id: selectedKnownUniversityId,
+      },
+      role,
+      department,
+    }
     const unknownUniversityData = universityName &&
-      !selectedKnownUniversity && {
+      !selectedKnownUniversityId && {
         university: {
           name: universityName,
           country_code: countryCode,
@@ -136,6 +141,7 @@ function AddEmail() {
                   setRole={setRole}
                   department={department}
                   setDepartment={setDepartment}
+                  newEmailMatchedInstitution={newEmailMatchedInstitution}
                 />
               </Cell>
             </Col>

+ 1 - 1
services/web/frontend/js/features/settings/components/emails/add-email/country-input.tsx

@@ -52,7 +52,7 @@ function Downshift({ setValue, inputRef }: CountryInputProps) {
         }
       )}
     >
-      <div {...getComboboxProps()} className="form-group mb-2 ui-select-toggle">
+      <div {...getComboboxProps()} className="ui-select-toggle">
         {/* eslint-disable-next-line jsx-a11y/label-has-for */}
         <label {...getLabelProps()} className="sr-only">
           {t('country')}

+ 61 - 18
services/web/frontend/js/features/settings/components/emails/add-email/institution-fields.tsx

@@ -7,8 +7,10 @@ import defaultRoles from '../../../data/roles'
 import defaultDepartments from '../../../data/departments'
 import { CountryCode } from '../../../data/countries-list'
 import { University } from '../../../../../../../types/university'
+import { InstitutionInfo } from './input'
 import { getJSON } from '../../../../../infrastructure/fetch-json'
 import useAsync from '../../../../../shared/hooks/use-async'
+import UniversityName from './university-name'
 
 type InstitutionFieldsProps = {
   countryCode: CountryCode | null
@@ -23,6 +25,7 @@ type InstitutionFieldsProps = {
   setRole: React.Dispatch<React.SetStateAction<string>>
   department: string
   setDepartment: React.Dispatch<React.SetStateAction<string>>
+  newEmailMatchedInstitution: InstitutionInfo | null
 }
 
 function InstitutionFields({
@@ -36,6 +39,7 @@ function InstitutionFields({
   setRole,
   department,
   setDepartment,
+  newEmailMatchedInstitution,
 }: InstitutionFieldsProps) {
   const { t } = useTranslation()
   const countryRef = useRef<HTMLInputElement | null>(null)
@@ -59,6 +63,16 @@ function InstitutionFields({
     }
   }, [setIsUniversityDirty, universityName])
 
+  // If the institution selected by autocompletion has changed
+  // hide the fields visibility and reset values
+  useEffect(() => {
+    if (!newEmailMatchedInstitution) {
+      setIsInstitutionFieldsVisible(false)
+      setRole('')
+      setDepartment('')
+    }
+  }, [newEmailMatchedInstitution, setRole, setDepartment])
+
   useEffect(() => {
     const selectedKnownUniversity = countryCode
       ? universities[countryCode]?.find(({ name }) => name === universityName)
@@ -100,7 +114,21 @@ function InstitutionFields({
     setIsInstitutionFieldsVisible(true)
   }
 
-  if (!isInstitutionFieldsVisible) {
+  const handleSelectUniversityManually = () => {
+    setRole('')
+    setDepartment('')
+    handleShowInstitutionFields()
+  }
+
+  const isLetUsKnowVisible =
+    !newEmailMatchedInstitution && !isInstitutionFieldsVisible
+  const isAutocompletedInstitutionVisible =
+    newEmailMatchedInstitution && !isInstitutionFieldsVisible
+  const isRoleAndDepartmentVisible =
+    isAutocompletedInstitutionVisible || isUniversityDirty
+
+  // Is the email affiliated with an institution?
+  if (isLetUsKnowVisible) {
     return (
       <EmailAffiliatedWithInstitution onClick={handleShowInstitutionFields} />
     )
@@ -108,24 +136,39 @@ function InstitutionFields({
 
   return (
     <>
-      <div className="form-group mb-2">
-        <CountryInput
-          id="new-email-country-input"
-          setValue={setCountryCode}
-          ref={countryRef}
+      {isAutocompletedInstitutionVisible ? (
+        // Display the institution name after autocompletion
+        <UniversityName
+          name={newEmailMatchedInstitution.university.name}
+          onClick={handleSelectUniversityManually}
         />
-      </div>
-      <div className="form-group mb-2">
-        <DownshiftInput
-          items={getUniversityItems()}
-          inputValue={universityName}
-          placeholder={t('university')}
-          label={t('university')}
-          setValue={setUniversityName}
-          disabled={!countryCode}
-        />
-      </div>
-      {isUniversityDirty && (
+      ) : (
+        // Display the country and university fields
+        <>
+          <div className="form-group mb-2">
+            <CountryInput
+              id="new-email-country-input"
+              setValue={setCountryCode}
+              ref={countryRef}
+            />
+          </div>
+          <div
+            className={`form-group ${
+              isRoleAndDepartmentVisible ? 'mb-2' : 'mb-0'
+            }`}
+          >
+            <DownshiftInput
+              items={getUniversityItems()}
+              inputValue={universityName}
+              placeholder={t('university')}
+              label={t('university')}
+              setValue={setUniversityName}
+              disabled={!countryCode}
+            />
+          </div>
+        </>
+      )}
+      {isRoleAndDepartmentVisible && (
         <>
           <div className="form-group mb-2">
             <DownshiftInput

+ 25 - 0
services/web/frontend/js/features/settings/components/emails/add-email/university-name.tsx

@@ -0,0 +1,25 @@
+import { useTranslation } from 'react-i18next'
+import { Button } from 'react-bootstrap'
+
+type UniversityNameProps = {
+  name: string
+  onClick: () => void
+}
+
+function UniversityName({ name, onClick }: UniversityNameProps) {
+  const { t } = useTranslation()
+
+  return (
+    <p className="pt-1">
+      {name}
+      <span className="small">
+        {' '}
+        <Button className="btn-inline-link" onClick={onClick}>
+          {t('change')}
+        </Button>
+      </span>
+    </p>
+  )
+}
+
+export default UniversityName

+ 1 - 1
services/web/frontend/js/features/settings/components/emails/downshift-input.tsx

@@ -66,7 +66,7 @@ function Downshift({
         }
       )}
     >
-      <div {...getComboboxProps()} className="form-group mb-2">
+      <div {...getComboboxProps()}>
         {/* eslint-disable-next-line jsx-a11y/label-has-for */}
         <label {...getLabelProps()} className="sr-only">
           {label}

+ 19 - 15
services/web/frontend/js/features/settings/components/emails/institution-and-role.tsx

@@ -118,21 +118,25 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
       ) : (
         <div className="affiliation-change-container small">
           <form onSubmit={handleSubmit}>
-            <DownshiftInput
-              items={[...defaultRoles]}
-              inputValue={role}
-              placeholder={t('role')}
-              label={t('role')}
-              setValue={setRole}
-              ref={roleRef}
-            />
-            <DownshiftInput
-              items={departments}
-              inputValue={department}
-              placeholder={t('department')}
-              label={t('department')}
-              setValue={setDepartment}
-            />
+            <div className="form-group mb-2">
+              <DownshiftInput
+                items={[...defaultRoles]}
+                inputValue={role}
+                placeholder={t('role')}
+                label={t('role')}
+                setValue={setRole}
+                ref={roleRef}
+              />
+            </div>
+            <div className="form-group mb-2">
+              <DownshiftInput
+                items={departments}
+                inputValue={department}
+                placeholder={t('department')}
+                label={t('department')}
+                setValue={setDepartment}
+              />
+            </div>
             <Button
               bsSize="small"
               bsStyle="success"

+ 15 - 2
services/web/frontend/stories/settings/helpers/emails.js

@@ -62,7 +62,7 @@ const bazFakeInstitution = {
   team_id: null,
 }
 
-const fakeInstitutionDomain = [
+const fakeInstitutionDomain1 = [
   {
     university: {
       id: 1234,
@@ -74,6 +74,18 @@ const fakeInstitutionDomain = [
   },
 ]
 
+const fakeInstitutionDomain2 = [
+  {
+    university: {
+      id: 5678,
+      ssoEnabled: false,
+      name: 'Fake Auto Complete University',
+    },
+    hostname: 'fake-autocomplete.edu',
+    confirmed: true,
+  },
+]
+
 export function defaultSetupMocks(fetchMock) {
   fetchMock
     .get(/\/user\/emails/, fakeUsersData, { delay: MOCK_DELAY })
@@ -82,7 +94,8 @@ export function defaultSetupMocks(fetchMock) {
     .get(/\/institutions\/list\?country_code=.*/, fakeInstitutions, {
       delay: MOCK_DELAY,
     })
-    .get(/\/institutions\/domains/, fakeInstitutionDomain)
+    .get(/\/institutions\/domains\?hostname=a/, fakeInstitutionDomain1)
+    .get(/\/institutions\/domains\?hostname=f/, fakeInstitutionDomain2)
     .post(/\/user\/emails\/*/, 200, {
       delay: MOCK_DELAY,
     })

+ 151 - 1
services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx

@@ -46,7 +46,7 @@ const institutionDomainData = [
     hostname: 'autocomplete.edu',
     confirmed: true,
   },
-]
+] as const
 
 function resetFetchMock() {
   fetchMock.reset()
@@ -391,4 +391,154 @@ describe('<EmailsSection />', function () {
     screen.getByText(userEmailData.affiliation.role, { exact: false })
     screen.getByText(userEmailData.affiliation.department, { exact: false })
   })
+
+  it('shows country, university, role and department fields based on whether `change` was clicked or not', async function () {
+    const institutionDomainDataCopy = [
+      {
+        ...institutionDomainData[0],
+        university: {
+          ...institutionDomainData[0].university,
+          ssoEnabled: false,
+        },
+      },
+    ]
+    const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
+    fetchMock.get('/user/emails?ensureAffiliation=true', [])
+    render(<EmailsSection />)
+
+    await fetchMock.flush(true)
+    fetchMock.reset()
+    fetchMock.get(
+      `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
+      institutionDomainDataCopy
+    )
+
+    await userEvent.click(
+      screen.getByRole('button', {
+        name: /add another email/i,
+      })
+    )
+
+    await userEvent.type(
+      screen.getByLabelText(/email/i),
+      `user@${hostnameFirstChar}`
+    )
+
+    await userEvent.keyboard('{Tab}')
+    await fetchMock.flush(true)
+    fetchMock.reset()
+
+    expect(
+      screen.queryByRole('textbox', {
+        name: /country/i,
+      })
+    ).to.be.null
+    expect(
+      screen.queryByRole('textbox', {
+        name: /university/i,
+      })
+    ).to.be.null
+    screen.getByRole('textbox', {
+      name: /role/i,
+    })
+    screen.getByRole('textbox', {
+      name: /department/i,
+    })
+
+    await userEvent.click(screen.getByRole('button', { name: /change/i }))
+
+    screen.getByRole('textbox', {
+      name: /country/i,
+    })
+    screen.getByRole('textbox', {
+      name: /university/i,
+    })
+    expect(
+      screen.queryByRole('textbox', {
+        name: /role/i,
+      })
+    ).to.be.null
+    expect(
+      screen.queryByRole('textbox', {
+        name: /department/i,
+      })
+    ).to.be.null
+  })
+
+  it('displays institution name with change button when autocompleted and adds new record', async function () {
+    const institutionDomainDataCopy = [
+      {
+        ...institutionDomainData[0],
+        university: {
+          ...institutionDomainData[0].university,
+          ssoEnabled: false,
+        },
+      },
+    ]
+    const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
+    fetchMock.get('/user/emails?ensureAffiliation=true', [])
+    render(<EmailsSection />)
+
+    await fetchMock.flush(true)
+    fetchMock.reset()
+    fetchMock.get(
+      `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
+      institutionDomainDataCopy
+    )
+
+    await userEvent.click(
+      screen.getByRole('button', {
+        name: /add another email/i,
+      })
+    )
+
+    await userEvent.type(
+      screen.getByLabelText(/email/i),
+      `user@${hostnameFirstChar}`
+    )
+
+    await userEvent.keyboard('{Tab}')
+    await fetchMock.flush(true)
+    fetchMock.reset()
+
+    screen.getByText(institutionDomainDataCopy[0].university.name)
+
+    const userEmailDataCopy = {
+      ...userEmailData,
+      affiliation: {
+        ...userEmailData.affiliation,
+        institution: {
+          ...userEmailData.affiliation.institution,
+          name: institutionDomainDataCopy[0].university.name,
+        },
+      },
+    }
+
+    fetchMock
+      .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
+      .post('/user/emails', 200)
+
+    await userEvent.type(
+      screen.getByRole('textbox', { name: /role/i }),
+      userEmailData.affiliation.role
+    )
+    await userEvent.type(
+      screen.getByRole('textbox', { name: /department/i }),
+      userEmailData.affiliation.department
+    )
+    await userEvent.click(
+      screen.getByRole('button', {
+        name: /add new email/i,
+      })
+    )
+
+    await fetchMock.flush(true)
+    fetchMock.reset()
+
+    screen.getByText(userEmailDataCopy.affiliation.institution.name, {
+      exact: false,
+    })
+    screen.getByText(userEmailDataCopy.affiliation.role, { exact: false })
+    screen.getByText(userEmailDataCopy.affiliation.department, { exact: false })
+  })
 })