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

Fix typescript errors related to `useAsync()` hook

GitOrigin-RevId: eacf7ec7e9c5da38ad88fde225ffedbfd1c2ff61
This commit is contained in:
Timothée Alby 2022-05-18 15:46:18 +02:00 committed by Copybot
parent 5731463d32
commit 4209828a6a
10 changed files with 77 additions and 25 deletions

View file

@ -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 ? (

View file

@ -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 => {

View file

@ -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>

View file

@ -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)

View file

@ -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

View file

@ -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))
})

View file

@ -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
}
}

View file

@ -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(

View file

@ -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
})
})
})

View file

@ -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(() => {