-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor credentials from error handling #94682
Changes from 16 commits
9cf625b
27b3ca7
2597b9d
f754897
f916cb6
d1a7d52
7f9e9d6
b241fb4
def978d
f844202
2d709d8
203221a
d62b7a1
c458551
8292361
6795cbb
677df7c
bdd0b24
86f6013
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { useTranslate } from 'i18n-calypso'; | ||
import { useCallback, useMemo } from 'react'; | ||
import { FieldErrors } from 'react-hook-form'; | ||
import { ApiError, CredentialsFormData } from '../types'; | ||
|
||
// This function is used to map the error message to the correct field in the form. | ||
// Backend is returning the errors related to backup files using 'from_url' key | ||
// but we need to use 'backupFileLocation' to identify the field in the form. | ||
const getFieldName = ( key: string, migrationType: string ) => { | ||
return 'backup' === migrationType && key === 'from_url' ? 'backupFileLocation' : key; | ||
}; | ||
|
||
// ** This hook is used to map the error messages to the form fields errors. | ||
export const useFormErrorMapping = ( | ||
error?: ApiError | null, | ||
variables?: CredentialsFormData | null | ||
): FieldErrors< CredentialsFormData > | undefined => { | ||
const translate = useTranslate(); | ||
|
||
const fieldMapping: Record< string, { type: string; message: string } | null > = useMemo( | ||
() => ( { | ||
from_url: { type: 'manual', message: translate( 'Enter a valid URL.' ) }, | ||
username: { type: 'manual', message: translate( 'Enter a valid username.' ) }, | ||
password: { type: 'manual', message: translate( 'Enter a valid password.' ) }, | ||
backupFileLocation: { type: 'manual', message: translate( 'Enter a valid URL.' ) }, | ||
} ), | ||
[ translate ] | ||
); | ||
|
||
const getTranslatedMessage = useCallback( | ||
( key: string ) => { | ||
return ( | ||
fieldMapping[ key ] ?? { | ||
type: 'manual', | ||
message: translate( 'Invalid input, please check again' ), | ||
} | ||
); | ||
}, | ||
[ fieldMapping, translate ] | ||
); | ||
|
||
const handleServerError = useCallback( | ||
( error: ApiError, { migrationType }: CredentialsFormData ) => { | ||
const { code, message, data } = error; | ||
|
||
if ( code === 'rest_missing_callback_param' || ! code ) { | ||
return { | ||
root: { | ||
type: 'manual', | ||
message: translate( 'An error occurred while saving credentials.' ), | ||
}, | ||
}; | ||
} | ||
|
||
if ( code !== 'rest_invalid_param' || ! data?.params ) { | ||
return { root: { type: 'manual', message } }; | ||
} | ||
|
||
const invalidFields = Object.keys( data.params ); | ||
|
||
return invalidFields.reduce( | ||
( errors, key ) => { | ||
const fieldName = getFieldName( key, migrationType ); | ||
const message = getTranslatedMessage( key ); | ||
|
||
errors[ fieldName ] = message; | ||
return errors; | ||
}, | ||
{} as Record< string, { type: string; message: string } > | ||
); | ||
}, | ||
[ getTranslatedMessage, translate ] | ||
); | ||
|
||
return useMemo( () => { | ||
if ( error && variables ) { | ||
return handleServerError( error, variables ) as FieldErrors< CredentialsFormData >; | ||
} | ||
return undefined; | ||
}, [ error, handleServerError, variables ] ); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,124 +1,66 @@ | ||
import { useTranslate } from 'i18n-calypso'; | ||
import { useEffect } from 'react'; | ||
import { useForm } from 'react-hook-form'; | ||
import { useQuery } from 'calypso/landing/stepper/hooks/use-query'; | ||
import { recordTracksEvent } from 'calypso/lib/analytics/tracks'; | ||
import { MigrationError, CredentialsFormData } from './types'; | ||
import { useFormErrorMapping } from './hooks/use-form-error-mapping'; | ||
import { CredentialsFormData } from './types'; | ||
import { useSiteMigrationCredentialsMutation } from './use-site-migration-credentials-mutation'; | ||
|
||
const mapApiError = ( error: any ) => { | ||
return { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed this response transformation to reduce complexity |
||
body: { | ||
code: error.code, | ||
message: error.message, | ||
data: error.data, | ||
}, | ||
status: error.status, | ||
}; | ||
}; | ||
|
||
export const useCredentialsForm = ( onSubmit: () => void ) => { | ||
const translate = useTranslate(); | ||
const importSiteQueryParam = useQuery().get( 'from' ) || ''; | ||
const { | ||
isPending, | ||
mutate: requestAutomatedMigration, | ||
error, | ||
isSuccess, | ||
variables, | ||
} = useSiteMigrationCredentialsMutation(); | ||
|
||
const fieldMapping = { | ||
from_url: { | ||
fieldName: 'siteAddress', | ||
errorMessage: translate( 'Enter a valid URL.' ), | ||
}, | ||
username: { | ||
fieldName: 'username', | ||
errorMessage: translate( 'Enter a valid username.' ), | ||
}, | ||
password: { | ||
fieldName: 'password', | ||
errorMessage: translate( 'Enter a valid password.' ), | ||
}, | ||
migration_type: { | ||
fieldName: 'howToAccessSite', | ||
errorMessage: null, | ||
}, | ||
notes: { | ||
fieldName: 'notes', | ||
errorMessage: null, | ||
}, | ||
}; | ||
|
||
const setGlobalError = ( message?: string | null | undefined ) => { | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
setError( 'root', { | ||
type: 'manual', | ||
message: message ?? translate( 'An error occurred while saving credentials.' ), | ||
} ); | ||
}; | ||
|
||
const handleMigrationError = ( err: MigrationError ) => { | ||
let hasUnmappedFieldError = false; | ||
|
||
if ( err.body?.code === 'rest_invalid_param' && err.body?.data?.params ) { | ||
Object.entries( err.body.data.params ).forEach( ( [ key ] ) => { | ||
const field = fieldMapping[ key as keyof typeof fieldMapping ]; | ||
const keyName = | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
'backup' === accessMethod && field?.fieldName === 'siteAddress' | ||
? 'backupFileLocation' | ||
: field?.fieldName; | ||
|
||
if ( keyName ) { | ||
const message = field?.errorMessage ?? translate( 'Invalid input, please check again' ); | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
setError( keyName as keyof CredentialsFormData, { type: 'manual', message } ); | ||
} else if ( ! hasUnmappedFieldError ) { | ||
hasUnmappedFieldError = true; | ||
setGlobalError(); | ||
} | ||
} ); | ||
} else { | ||
setGlobalError( err.body?.message ); | ||
} | ||
}; | ||
|
||
const { isPending, requestAutomatedMigration } = useSiteMigrationCredentialsMutation( { | ||
onSuccess: () => { | ||
recordTracksEvent( 'calypso_site_migration_automated_request_success' ); | ||
onSubmit(); | ||
}, | ||
onError: ( error: any ) => { | ||
handleMigrationError( mapApiError( error ) ); | ||
recordTracksEvent( 'calypso_site_migration_automated_request_error' ); | ||
}, | ||
} ); | ||
const serverSideError = useFormErrorMapping( error, variables ); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All error logic is now parsed by this hook and returned on the serverSideError variable |
||
const { | ||
formState: { errors }, | ||
control, | ||
handleSubmit, | ||
watch, | ||
setError, | ||
clearErrors, | ||
} = useForm< CredentialsFormData >( { | ||
mode: 'onSubmit', | ||
reValidateMode: 'onSubmit', | ||
disabled: isPending, | ||
defaultValues: { | ||
siteAddress: importSiteQueryParam, | ||
from_url: importSiteQueryParam, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The backend is waiting for a |
||
username: '', | ||
password: '', | ||
backupFileLocation: '', | ||
notes: '', | ||
howToAccessSite: 'credentials', | ||
migrationType: 'credentials', | ||
}, | ||
errors: serverSideError, | ||
} ); | ||
|
||
const accessMethod = watch( 'migrationType' ); | ||
|
||
useEffect( () => { | ||
if ( isSuccess ) { | ||
recordTracksEvent( 'calypso_site_migration_automated_request_success' ); | ||
onSubmit(); | ||
} | ||
}, [ isSuccess, onSubmit ] ); | ||
|
||
useEffect( () => { | ||
if ( error ) { | ||
recordTracksEvent( 'calypso_site_migration_automated_request_error' ); | ||
} | ||
}, [ error ] ); | ||
|
||
useEffect( () => { | ||
const { unsubscribe } = watch( () => { | ||
clearErrors( 'root' ); | ||
} ); | ||
return () => unsubscribe(); | ||
}, [ watch, clearErrors ] ); | ||
|
||
const accessMethod = watch( 'howToAccessSite' ); | ||
|
||
const submitHandler = ( data: CredentialsFormData ) => { | ||
requestAutomatedMigration( data ); | ||
}; | ||
|
@@ -131,7 +73,6 @@ export const useCredentialsForm = ( onSubmit: () => void ) => { | |
accessMethod, | ||
isPending, | ||
submitHandler, | ||
setError, | ||
importSiteQueryParam, | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are switching to snake case here, but
backupFileLocation
is in camel case. Having two different cases for the form fields in the same form looks a bit confusing to me. WDYT about sticking to one of the cases for all form fields instead?