Преглед изворни кода

Merge pull request #7941 from overleaf/msm-use-async-typing-fix

Fix typescript errors related to `useAsync()` hook

GitOrigin-RevId: eacf7ec7e9c5da38ad88fde225ffedbfd1c2ff61
Timothée Alby пре 4 година
родитељ
комит
4209828a6a

+ 5 - 2
services/web/frontend/js/features/settings/components/account-info-section.tsx

@@ -7,7 +7,10 @@ import {
   FormGroup,
 } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
-import { postJSON } from '../../../infrastructure/fetch-json'
+import {
+  getUserFacingMessage,
+  postJSON,
+} from '../../../infrastructure/fetch-json'
 import getMeta from '../../../utils/meta'
 import { ExposedSettings } from '../../../../../types/exposed-settings'
 import useAsync from '../../../shared/hooks/use-async'
@@ -109,7 +112,7 @@ function AccountInfoSection() {
         ) : null}
         {isError ? (
           <FormGroup>
-            <Alert bsStyle="danger">{error.getUserFacingMessage()}</Alert>
+            <Alert bsStyle="danger">{getUserFacingMessage(error)}</Alert>
           </FormGroup>
         ) : null}
         {canUpdateEmail || canUpdateNames ? (

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

@@ -49,7 +49,7 @@ function InstitutionFields({
   const [isInstitutionFieldsVisible, setIsInstitutionFieldsVisible] =
     useState(false)
   const [isUniversityDirty, setIsUniversityDirty] = useState(false)
-  const { runAsync: institutionRunAsync } = useAsync()
+  const { runAsync: institutionRunAsync } = useAsync<University[]>()
 
   useEffect(() => {
     if (isInstitutionFieldsVisible && countryRef.current) {
@@ -93,7 +93,7 @@ function InstitutionFields({
       return
     }
 
-    institutionRunAsync<University[]>(
+    institutionRunAsync(
       getJSON(`/institutions/list?country_code=${countryCode}`)
     )
       .then(data => {

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

@@ -1,6 +1,7 @@
 import { Row, Alert } from 'react-bootstrap'
 import Icon from '../../../../../shared/components/icon'
 import { UseAsyncReturnType } from '../../../../../shared/hooks/use-async'
+import { getUserFacingMessage } from '../../../../../infrastructure/fetch-json'
 
 type LayoutProps = {
   children: React.ReactNode
@@ -14,7 +15,7 @@ function Layout({ isError, error, children }: LayoutProps) {
       <Row>{children}</Row>
       {isError && (
         <Alert bsStyle="danger" className="text-center">
-          <Icon type="exclamation-triangle" fw /> {error.getUserFacingMessage()}
+          <Icon type="exclamation-triangle" fw /> {getUserFacingMessage(error)}
         </Alert>
       )}
     </div>

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

@@ -18,7 +18,7 @@ type InstitutionAndRoleProps = {
 function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
   const { t } = useTranslation()
   const { isLoading, isError, runAsync } = useAsync()
-  const changeAffiliationAsync = useAsync()
+  const changeAffiliationAsync = useAsync<University>()
   const { affiliation } = userEmailData
   const {
     state,
@@ -55,9 +55,7 @@ function InstitutionAndRole({ userEmailData }: InstitutionAndRoleProps) {
     }
 
     changeAffiliationAsync
-      .runAsync<University>(
-        getJSON(`/institutions/list/${affiliation.institution.id}`)
-      )
+      .runAsync(getJSON(`/institutions/list/${affiliation.institution.id}`))
       .then(data => {
         if (data.departments.length) {
           setDepartments(data.departments)

+ 13 - 3
services/web/frontend/js/features/settings/components/password-section.tsx

@@ -7,12 +7,21 @@ import {
   FormGroup,
 } from 'react-bootstrap'
 import { useTranslation } from 'react-i18next'
-import { postJSON } from '../../../infrastructure/fetch-json'
+import {
+  getUserFacingMessage,
+  postJSON,
+} from '../../../infrastructure/fetch-json'
 import getMeta from '../../../utils/meta'
 import { ExposedSettings } from '../../../../../types/exposed-settings'
 import { PasswordStrengthOptions } from '../../../../../types/password-strength-options'
 import useAsync from '../../../shared/hooks/use-async'
 
+type PasswordUpdateResult = {
+  message?: {
+    text: string
+  }
+}
+
 function PasswordSection() {
   const { t } = useTranslation()
 
@@ -58,7 +67,8 @@ function PasswordForm() {
   const [currentPassword, setCurrentPassword] = useState('')
   const [newPassword1, setNewPassword1] = useState('')
   const [newPassword2, setNewPassword2] = useState('')
-  const { isLoading, isSuccess, isError, data, error, runAsync } = useAsync()
+  const { isLoading, isSuccess, isError, data, error, runAsync } =
+    useAsync<PasswordUpdateResult>()
   const [isNewPasswordValid, setIsNewPasswordValid] = useState(false)
   const [isFormValid, setIsFormValid] = useState(false)
 
@@ -128,7 +138,7 @@ function PasswordForm() {
       ) : null}
       {isError ? (
         <FormGroup>
-          <Alert bsStyle="danger">{error.getUserFacingMessage()}</Alert>
+          <Alert bsStyle="danger">{getUserFacingMessage(error)}</Alert>
         </FormGroup>
       ) : null}
       <Button

+ 3 - 2
services/web/frontend/js/features/settings/context/user-email-context.tsx

@@ -203,10 +203,11 @@ function useUserEmails() {
   )
   const [state, unsafeDispatch] = useReducer(reducer, initialState)
   const dispatch = useSafeDispatch(unsafeDispatch)
-  const { data, isLoading, isError, isSuccess, runAsync } = useAsync()
+  const { data, isLoading, isError, isSuccess, runAsync } =
+    useAsync<UserEmailData[]>()
 
   const getEmails = useCallback(() => {
-    runAsync(getJSON<UserEmailData[]>('/user/emails?ensureAffiliation=true'))
+    runAsync(getJSON('/user/emails?ensureAffiliation=true'))
       .then(data => {
         dispatch(ActionCreators.setData(data))
       })

+ 9 - 0
services/web/frontend/js/infrastructure/fetch-json.ts

@@ -207,3 +207,12 @@ async function parseResponseBody(response: Response) {
   // responses) or unsupported
   return {}
 }
+
+export function getUserFacingMessage(error: Error): string {
+  if (error instanceof FetchError) {
+    return error.getUserFacingMessage()
+  } else {
+    // checking existence of `error` to prevent errors when called from Javascript
+    return error?.message
+  }
+}

+ 14 - 10
services/web/frontend/js/shared/hooks/use-async.ts

@@ -2,34 +2,38 @@ import * as React from 'react'
 import useSafeDispatch from './use-safe-dispatch'
 import { Nullable } from '../../../../types/utils'
 
-type State = {
+type State<T> = {
   status: 'idle' | 'pending' | 'resolved' | 'rejected'
-  data: Nullable<unknown>
-  error: Nullable<Record<string, unknown>>
+  data: Nullable<T>
+  error: Nullable<Error>
 }
-type Action = Partial<State>
+type Action<T> = Partial<State<T>>
 
-const defaultInitialState: State = { status: 'idle', data: null, error: null }
+const defaultInitialState: State<null> = {
+  status: 'idle',
+  data: null,
+  error: null,
+}
 
-function useAsync(initialState?: Partial<State>) {
+function useAsync<T = any>(initialState?: Partial<State<T>>) {
   const initialStateRef = React.useRef({
     ...defaultInitialState,
     ...initialState,
   })
   const [{ status, data, error }, setState] = React.useReducer(
-    (state: State, action: Action) => ({ ...state, ...action }),
+    (state: State<T>, action: Action<T>) => ({ ...state, ...action }),
     initialStateRef.current
   )
 
   const safeSetState = useSafeDispatch(setState)
 
   const setData = React.useCallback(
-    data => safeSetState({ data, status: 'resolved' }),
+    (data: Nullable<T>) => safeSetState({ data, status: 'resolved' }),
     [safeSetState]
   )
 
   const setError = React.useCallback(
-    error => safeSetState({ error, status: 'rejected' }),
+    (error: Nullable<Error>) => safeSetState({ error, status: 'rejected' }),
     [safeSetState]
   )
 
@@ -39,7 +43,7 @@ function useAsync(initialState?: Partial<State>) {
   )
 
   const runAsync = React.useCallback(
-    <T>(promise: Promise<T>) => {
+    (promise: Promise<T>) => {
       safeSetState({ status: 'pending' })
 
       return promise.then(

+ 26 - 0
services/web/test/frontend/infrastructure/fetch-json.test.js

@@ -4,6 +4,7 @@ import { Response } from 'node-fetch'
 import {
   deleteJSON,
   FetchError,
+  getUserFacingMessage,
   getJSON,
   postJSON,
   putJSON,
@@ -190,4 +191,29 @@ describe('fetchJSON', function () {
 
     return expect(deleteJSON('/test')).to.eventually.deep.equal({})
   })
+
+  describe('getUserFacingMessage()', function () {
+    it('returns the error facing message for FetchError instances', function () {
+      const error = new FetchError(
+        '403 error',
+        'http:/example.com',
+        {},
+        { status: 403 }
+      )
+      expect(getUserFacingMessage(error)).to.equal(
+        'Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies.'
+      )
+    })
+
+    it('returns `message` for Error instances different than FetchError', function () {
+      const error = new Error('403 error')
+      expect(getUserFacingMessage(error)).to.equal('403 error')
+    })
+
+    it('returns `undefined` for non-Error instances', function () {
+      expect(getUserFacingMessage(undefined)).to.be.undefined
+      expect(getUserFacingMessage(null)).to.be.undefined
+      expect(getUserFacingMessage('error')).to.be.undefined
+    })
+  })
 })

+ 1 - 1
services/web/test/frontend/shared/hooks/use-async.test.ts

@@ -150,7 +150,7 @@ describe('useAsync', function () {
   })
 
   it('can set the error', function () {
-    const mockError = Symbol('rejected value')
+    const mockError = new Error('rejected value')
     const { result } = renderHook(() => useAsync())
 
     act(() => {