Skip to content

Commit

Permalink
Save site migration credentials for auto migration (#93668)
Browse files Browse the repository at this point in the history
* Add mutation function to save site creds

* Add hook to handle form saving

* Move types to a separate file

* Import saving hook and add generic error field

* Remove handler file for mutation

* Update mutation to handle requests differently

* Use mutation directly, handle error, disable form, use separate errors

* Test default states without input

* Fix form issues

* Add more tests for validations

* Add tests for server side error handling

* Update API endpoint
  • Loading branch information
Imran92 authored Aug 21, 2024
1 parent d7566fb commit 4984b3a
Show file tree
Hide file tree
Showing 4 changed files with 532 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FormLabel } from '@automattic/components';
import Card from '@automattic/components/src/card';
import { NextButton, StepContainer } from '@automattic/onboarding';
import { useTranslate } from 'i18n-calypso';
import { type FC } from 'react';
import { useEffect, type FC } from 'react';
import { Controller, useForm } from 'react-hook-form';
import getValidationMessage from 'calypso/blocks/import/capture/url-validation-message-helper';
import { CAPTURE_URL_RGX } from 'calypso/blocks/import/util';
Expand All @@ -11,25 +11,29 @@ import FormattedHeader from 'calypso/components/formatted-header';
import FormRadio from 'calypso/components/forms/form-radio';
import FormTextInput from 'calypso/components/forms/form-text-input';
import FormTextArea from 'calypso/components/forms/form-textarea';
import { useQuery } from 'calypso/landing/stepper/hooks/use-query';
import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
import { isValidUrl } from 'calypso/lib/importer/url-validation';
import { CredentialsFormData, MigrationError } from './types';
import { useSiteMigrationCredentialsMutation } from './use-site-migration-credentials-mutation';
import type { Step } from '../../types';

import './style.scss';

interface CredentialsFormProps {
onSubmit: ( data: CredentialsFormData ) => void;
onSubmit: () => void;
onSkip: () => void;
}

interface CredentialsFormData {
siteAddress: string;
username: string;
password: string;
backupFileLocation: string;
notes: string;
howToAccessSite: 'credentials' | 'backup';
}
const mapApiError = ( error: any ) => {
return {
body: {
code: error.code,
message: error.message,
data: error.data,
},
status: error.status,
};
};

export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip } ) => {
const translate = useTranslate();
Expand All @@ -42,20 +46,93 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
}
};

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 isBackupFileLocationValid = ( fileLocation: string ) => {
return ! isValidUrl( fileLocation ) ? translate( 'Please enter a valid URL.' ) : undefined;
};

const importSiteQueryParam = useQuery().get( 'from' ) || '';

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 ) => {
handleMigrationError( mapApiError( error ) );
recordTracksEvent( 'calypso_site_migration_automated_request_error' );
},
} );

const {
formState: { errors },
control,
handleSubmit,
watch,
setError,
clearErrors,
} = useForm< CredentialsFormData >( {
mode: 'onSubmit',
reValidateMode: 'onSubmit',
disabled: isPending,
defaultValues: {
siteAddress: '',
siteAddress: importSiteQueryParam,
username: '',
password: '',
backupFileLocation: '',
Expand All @@ -64,11 +141,19 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
},
} );

// Subscribe only this field to the access method value
// Clear any root errors when the user changes any field.
useEffect( () => {
const { unsubscribe } = watch( () => {
clearErrors( 'root' );
} );
return () => unsubscribe();
}, [ watch, clearErrors ] );

// Subscribe only this field to the access method value.
const accessMethod = watch( 'howToAccessSite' );

const submitHandler = ( data: CredentialsFormData ) => {
onSubmit( data );
requestAutomatedMigration( data );
};

return (
Expand Down Expand Up @@ -162,6 +247,14 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
isError={ !! errors.username }
placeholder={ translate( 'Username' ) }
{ ...field }
onChange={ ( e: any ) => {
const trimmedValue = e.target.value.trim();
field.onChange( trimmedValue );
} }
onBlur={ ( e: any ) => {
field.onBlur();
e.target.value = e.target.value.trim();
} }
/>
) }
/>
Expand Down Expand Up @@ -209,6 +302,7 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
} }
render={ ( { field } ) => (
<FormTextInput
id="backup-file"
type="text"
isError={ !! errors.backupFileLocation }
placeholder={ translate( 'Enter your backup file location' ) }
Expand All @@ -232,13 +326,15 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
) }

<div className="site-migration-credentials__form-field">
<FormLabel htmlFor="site-address">{ translate( 'Notes (optional)' ) }</FormLabel>
<FormLabel htmlFor="notes">{ translate( 'Notes (optional)' ) }</FormLabel>
<Controller
control={ control }
name="notes"
render={ ( { field } ) => (
<FormTextArea
id="notes"
type="text"
maxLength={ 1000 }
placeholder={ translate(
'Share any other details that will help us access your site for the migration.'
) }
Expand All @@ -248,13 +344,22 @@ export const CredentialsForm: FC< CredentialsFormProps > = ( { onSubmit, onSkip
) }
/>
</div>
{ errors?.notes && (
<div className="site-migration-credentials__form-error">{ errors.notes.message }</div>
) }
{ errors?.root && (
<div className="site-migration-credentials__form-error">{ errors.root.message }</div>
) }
<div>
<NextButton type="submit">{ translate( 'Continue' ) }</NextButton>
<NextButton disabled={ isPending } type="submit">
{ translate( 'Continue' ) }
</NextButton>
</div>
</Card>
<div className="site-migration-credentials__skip">
<button
className="button navigation-link step-container__navigation-link has-underline is-borderless"
disabled={ isPending }
onClick={ onSkip }
>
{ translate( 'Skip, I need help providing access' ) }
Expand Down
Loading

0 comments on commit 4984b3a

Please sign in to comment.