From 6726bcaa0f39078be3d19d2f481d38c1c756aa81 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 13 Jun 2023 11:28:41 +0800 Subject: [PATCH] fix(console): reset form state when the submit handler throws (#4029) --- .../mdx-components/UriInputField/index.tsx | 5 +- .../src/onboarding/pages/About/index.tsx | 9 +- .../pages/SignInExperience/index.tsx | 26 ++-- .../src/onboarding/pages/Welcome/index.tsx | 9 +- .../CreatePermissionModal/index.tsx | 21 +-- .../ApiResourceSettings/index.tsx | 35 ++--- .../components/CreateForm/index.tsx | 17 ++- .../src/pages/ApplicationDetails/index.tsx | 55 ++++---- .../components/CreateForm/index.tsx | 29 ++-- .../components/ConnectorContent.tsx | 51 +++---- .../components/SenderTester/index.tsx | 21 +-- .../Connectors/components/Guide/index.tsx | 127 +++++++++--------- .../containers/LinkEmailModal/index.tsx | 13 +- .../pages/RoleDetails/RoleSettings/index.tsx | 21 +-- .../Roles/components/CreateRoleForm/index.tsx | 29 ++-- .../components/Welcome/GuideModal.tsx | 27 ++-- .../src/pages/SignInExperience/index.tsx | 27 ++-- .../LanguageEditor/LanguageDetails.tsx | 13 +- .../AddDomainForm/index.tsx | 11 +- .../pages/UserDetails/UserSettings/index.tsx | 59 ++++---- .../Users/components/CreateForm/index.tsx | 69 +++++----- .../WebhookDetails/WebhookSettings/index.tsx | 19 +-- .../components/CreateFormModal/CreateForm.tsx | 27 ++-- packages/console/src/utils/form.ts | 21 +++ 24 files changed, 412 insertions(+), 329 deletions(-) create mode 100644 packages/console/src/utils/form.ts diff --git a/packages/console/src/mdx-components/UriInputField/index.tsx b/packages/console/src/mdx-components/UriInputField/index.tsx index f43c9e148..feb17448a 100644 --- a/packages/console/src/mdx-components/UriInputField/index.tsx +++ b/packages/console/src/mdx-components/UriInputField/index.tsx @@ -15,6 +15,7 @@ import TextInput from '@/components/TextInput'; import type { RequestError } from '@/hooks/use-api'; import useApi from '@/hooks/use-api'; import type { GuideForm } from '@/types/guide'; +import { trySubmitSafe } from '@/utils/form'; import { uriValidator } from '@/utils/validator'; import * as styles from './index.module.scss'; @@ -42,7 +43,7 @@ function UriInputField({ appId, name, title, isSingle = false }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const api = useApi(); - const onSubmit = async (value: string[]) => { + const onSubmit = trySubmitSafe(async (value: string[]) => { const updatedApp = await api .patch(`api/applications/${appId}`, { json: { @@ -57,7 +58,7 @@ function UriInputField({ appId, name, title, isSingle = false }: Props) { // Reset form to set 'isDirty' to false reset(getValues()); - }; + }); const onKeyPress = (event: KeyboardEvent, value: string[]) => { if (event.key === 'Enter') { diff --git a/packages/console/src/onboarding/pages/About/index.tsx b/packages/console/src/onboarding/pages/About/index.tsx index de65b9108..49ad6108e 100644 --- a/packages/console/src/onboarding/pages/About/index.tsx +++ b/packages/console/src/onboarding/pages/About/index.tsx @@ -13,6 +13,7 @@ import PageMeta from '@/components/PageMeta'; import TextInput from '@/components/TextInput'; import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data'; import * as pageLayout from '@/onboarding/scss/layout.module.scss'; +import { trySubmitSafe } from '@/utils/form'; import { CardSelector, MultiCardSelector } from '../../../components/CardSelector'; import ActionBar from '../../components/ActionBar'; @@ -41,9 +42,11 @@ function About() { reset(questionnaire); }, [questionnaire, reset]); - const onSubmit = handleSubmit(async (formData) => { - await update({ questionnaire: formData }); - }); + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + await update({ questionnaire: formData }); + }) + ); const onNext = async () => { await onSubmit(); diff --git a/packages/console/src/onboarding/pages/SignInExperience/index.tsx b/packages/console/src/onboarding/pages/SignInExperience/index.tsx index 9b67f0276..8c90b83a4 100644 --- a/packages/console/src/onboarding/pages/SignInExperience/index.tsx +++ b/packages/console/src/onboarding/pages/SignInExperience/index.tsx @@ -25,6 +25,7 @@ import * as pageLayout from '@/onboarding/scss/layout.module.scss'; import type { OnboardingSieConfig } from '@/onboarding/types'; import { Authentication, OnboardingPage } from '@/onboarding/types'; import { getOnboardingPage } from '@/onboarding/utils'; +import { trySubmitSafe } from '@/utils/form'; import { buildUrl } from '@/utils/url'; import { uriValidator } from '@/utils/validator'; @@ -85,21 +86,22 @@ function SignInExperience() { } }, [onboardingSieConfig, signInExperience]); - const submit = (onSuccess: () => void) => async (formData: OnboardingSieConfig) => { - if (!signInExperience) { - return; - } + const submit = (onSuccess: () => void) => + trySubmitSafe(async (formData: OnboardingSieConfig) => { + if (!signInExperience) { + return; + } - const updatedData = await api - .patch(buildUrl('api/sign-in-exp', { removeUnusedDemoSocialConnector: '1' }), { - json: parser.onboardSieConfigToSignInExperience(formData, signInExperience), - }) - .json(); + const updatedData = await api + .patch(buildUrl('api/sign-in-exp', { removeUnusedDemoSocialConnector: '1' }), { + json: parser.onboardSieConfigToSignInExperience(formData, signInExperience), + }) + .json(); - void mutate(updatedData); + void mutate(updatedData); - onSuccess(); - }; + onSuccess(); + }); if (isLoading) { return ; diff --git a/packages/console/src/onboarding/pages/Welcome/index.tsx b/packages/console/src/onboarding/pages/Welcome/index.tsx index ddc0d3e0f..0428d39a1 100644 --- a/packages/console/src/onboarding/pages/Welcome/index.tsx +++ b/packages/console/src/onboarding/pages/Welcome/index.tsx @@ -14,6 +14,7 @@ import PageMeta from '@/components/PageMeta'; import ActionBar from '@/onboarding/components/ActionBar'; import useUserOnboardingData from '@/onboarding/hooks/use-user-onboarding-data'; import * as pageLayout from '@/onboarding/scss/layout.module.scss'; +import { trySubmitSafe } from '@/utils/form'; import type { Questionnaire } from '../../types'; import { OnboardingPage } from '../../types'; @@ -42,9 +43,11 @@ function Welcome() { reset(questionnaire); }, [questionnaire, reset]); - const onSubmit = handleSubmit(async (formData) => { - await update({ questionnaire: formData }); - }); + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + await update({ questionnaire: formData }); + }) + ); const onNext = async () => { await onSubmit(); diff --git a/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/components/CreatePermissionModal/index.tsx b/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/components/CreatePermissionModal/index.tsx index d0e9a4977..e7683e17f 100644 --- a/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/components/CreatePermissionModal/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/ApiResourcePermissions/components/CreatePermissionModal/index.tsx @@ -10,6 +10,7 @@ import ModalLayout from '@/components/ModalLayout'; import TextInput from '@/components/TextInput'; import useApi from '@/hooks/use-api'; import * as modalStyles from '@/scss/modal.module.scss'; +import { trySubmitSafe } from '@/utils/form'; type Props = { resourceId: string; @@ -28,17 +29,19 @@ function CreatePermissionModal({ resourceId, onClose }: Props) { const api = useApi(); - const onSubmit = handleSubmit(async (formData) => { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + if (isSubmitting) { + return; + } - const createdScope = await api - .post(`api/resources/${resourceId}/scopes`, { json: formData }) - .json(); + const createdScope = await api + .post(`api/resources/${resourceId}/scopes`, { json: formData }) + .json(); - onClose(createdScope); - }); + onClose(createdScope); + }) + ); return ( { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async ({ isDefault, ...rest }) => { + if (isSubmitting) { + return; + } - const [data] = await Promise.all([ - api.patch(`api/resources/${resource.id}`, { json: rest }).json(), - api - .patch(`api/resources/${resource.id}/is-default`, { json: { isDefault } }) - .json(), - ]); + const [data] = await Promise.all([ + api.patch(`api/resources/${resource.id}`, { json: rest }).json(), + api + .patch(`api/resources/${resource.id}/is-default`, { json: { isDefault } }) + .json(), + ]); - // We cannot ensure the order of API requests, manually combine the results - const updatedApiResource = { ...data, isDefault }; - reset(updatedApiResource); - onResourceUpdated(updatedApiResource); - toast.success(t('general.saved')); - }); + // We cannot ensure the order of API requests, manually combine the results + const updatedApiResource = { ...data, isDefault }; + reset(updatedApiResource); + onResourceUpdated(updatedApiResource); + toast.success(t('general.saved')); + }) + ); return ( <> diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx index e05dd13ff..efe9a20e2 100644 --- a/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx +++ b/packages/console/src/pages/ApiResources/components/CreateForm/index.tsx @@ -8,6 +8,7 @@ import ModalLayout from '@/components/ModalLayout'; import TextInput from '@/components/TextInput'; import TextLink from '@/components/TextLink'; import useApi from '@/hooks/use-api'; +import { trySubmitSafe } from '@/utils/form'; type FormData = { name: string; @@ -28,14 +29,16 @@ function CreateForm({ onClose }: Props) { const api = useApi(); - const onSubmit = handleSubmit(async (data) => { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (data) => { + if (isSubmitting) { + return; + } - const createdApiResource = await api.post('api/resources', { json: data }).json(); - onClose?.(createdApiResource); - }); + const createdApiResource = await api.post('api/resources', { json: data }).json(); + onClose?.(createdApiResource); + }) + ); return ( { - if (!data || isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + if (!data || isSubmitting) { + return; + } - await api - .patch(`api/applications/${data.id}`, { - json: { - ...formData, - oidcClientMetadata: { - ...formData.oidcClientMetadata, - redirectUris: mapToUriFormatArrays(formData.oidcClientMetadata.redirectUris), - postLogoutRedirectUris: mapToUriFormatArrays( - formData.oidcClientMetadata.postLogoutRedirectUris - ), + await api + .patch(`api/applications/${data.id}`, { + json: { + ...formData, + oidcClientMetadata: { + ...formData.oidcClientMetadata, + redirectUris: mapToUriFormatArrays(formData.oidcClientMetadata.redirectUris), + postLogoutRedirectUris: mapToUriFormatArrays( + formData.oidcClientMetadata.postLogoutRedirectUris + ), + }, + customClientMetadata: { + ...formData.customClientMetadata, + corsAllowedOrigins: mapToUriOriginFormatArrays( + formData.customClientMetadata.corsAllowedOrigins + ), + }, }, - customClientMetadata: { - ...formData.customClientMetadata, - corsAllowedOrigins: mapToUriOriginFormatArrays( - formData.customClientMetadata.corsAllowedOrigins - ), - }, - }, - }) - .json(); - void mutate(); - toast.success(t('general.saved')); - }); + }) + .json(); + void mutate(); + toast.success(t('general.saved')); + }) + ); const onDelete = async () => { if (!data || isDeleting) { diff --git a/packages/console/src/pages/Applications/components/CreateForm/index.tsx b/packages/console/src/pages/Applications/components/CreateForm/index.tsx index e822b5dce..876997cf7 100644 --- a/packages/console/src/pages/Applications/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Applications/components/CreateForm/index.tsx @@ -14,6 +14,7 @@ import useApi from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; import * as modalStyles from '@/scss/modal.module.scss'; import { applicationTypeI18nKey } from '@/types/applications'; +import { trySubmitSafe } from '@/utils/form'; import TypeDescription from '../TypeDescription'; @@ -48,20 +49,22 @@ function CreateForm({ isOpen, onClose }: Props) { return null; } - const onSubmit = handleSubmit(async (data) => { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (data) => { + if (isSubmitting) { + return; + } - const createdApp = await api.post('api/applications', { json: data }).json(); - void updateConfigs({ - applicationCreated: true, - ...conditional( - createdApp.type === ApplicationType.MachineToMachine && { m2mApplicationCreated: true } - ), - }); - onClose?.(createdApp); - }); + const createdApp = await api.post('api/applications', { json: data }).json(); + void updateConfigs({ + applicationCreated: true, + ...conditional( + createdApp.type === ApplicationType.MachineToMachine && { m2mApplicationCreated: true } + ), + }); + onClose?.(createdApp); + }) + ); return ( { - const { formItems, isStandard, id } = connectorData; - const config = configParser(data, formItems); - const { syncProfile, name, logo, logoDark, target } = data; + const onSubmit = handleSubmit( + trySubmitSafe(async (data) => { + const { formItems, isStandard, id } = connectorData; + const config = configParser(data, formItems); + const { syncProfile, name, logo, logoDark, target } = data; - const payload = isSocialConnector - ? { - config, - syncProfile: syncProfile === SyncProfileMode.EachSignIn, - } - : { config }; - const standardConnectorPayload = { - ...payload, - metadata: { name: { en: name }, logo, logoDark, target }, - }; - // Should not update `target` for neither passwordless connectors nor non-standard social connectors. - const body = isStandard ? standardConnectorPayload : { ...payload, target: undefined }; + const payload = isSocialConnector + ? { + config, + syncProfile: syncProfile === SyncProfileMode.EachSignIn, + } + : { config }; + const standardConnectorPayload = { + ...payload, + metadata: { name: { en: name }, logo, logoDark, target }, + }; + // Should not update `target` for neither passwordless connectors nor non-standard social connectors. + const body = isStandard ? standardConnectorPayload : { ...payload, target: undefined }; - const updatedConnector = await api - .patch(`api/connectors/${id}`, { - json: body, - }) - .json(); + const updatedConnector = await api + .patch(`api/connectors/${id}`, { + json: body, + }) + .json(); - onConnectorUpdated(updatedConnector); - toast.success(t('general.saved')); - }); + onConnectorUpdated(updatedConnector); + toast.success(t('general.saved')); + }) + ); return ( diff --git a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx index 204e20f1c..a74d31c94 100644 --- a/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/components/SenderTester/index.tsx @@ -11,6 +11,7 @@ import TextInput from '@/components/TextInput'; import { Tooltip } from '@/components/Tip'; import useApi from '@/hooks/use-api'; import { onKeyDownHandler } from '@/utils/a11y'; +import { trySubmitSafe } from '@/utils/form'; import { parsePhoneNumber } from '@/utils/phone'; import * as styles from './index.module.scss'; @@ -54,17 +55,19 @@ function SenderTester({ connectorFactoryId, connectorType, className, parse }: P }; }, [showTooltip]); - const onSubmit = handleSubmit(async (formData) => { - const { sendTo } = formData; + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + const { sendTo } = formData; - const data = { - config: parse(), - ...(isSms ? { phone: parsePhoneNumber(sendTo) } : { email: sendTo }), - }; + const data = { + config: parse(), + ...(isSms ? { phone: parsePhoneNumber(sendTo) } : { email: sendTo }), + }; - await api.post(`api/connectors/${connectorFactoryId}/test`, { json: data }).json(); - setShowTooltip(true); - }); + await api.post(`api/connectors/${connectorFactoryId}/test`, { json: data }).json(); + setShowTooltip(true); + }) + ); return (
diff --git a/packages/console/src/pages/Connectors/components/Guide/index.tsx b/packages/console/src/pages/Connectors/components/Guide/index.tsx index 5d11c80a4..9927f2be2 100644 --- a/packages/console/src/pages/Connectors/components/Guide/index.tsx +++ b/packages/console/src/pages/Connectors/components/Guide/index.tsx @@ -24,6 +24,7 @@ import useApi from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; import SenderTester from '@/pages/ConnectorDetails/components/SenderTester'; import * as modalStyles from '@/scss/modal.module.scss'; +import { trySubmitSafe } from '@/utils/form'; import type { ConnectorFormType } from '../../types'; import { SyncProfileMode } from '../../types'; @@ -88,70 +89,72 @@ function Guide({ connector, onClose }: Props) { const { title, content } = splitMarkdownByTitle(readme); const connectorName = conditional(isLanguageTag(language) && name[language]) ?? name.en; - const onSubmit = handleSubmit(async (data) => { - if (isSubmitting) { - return; - } - - // Recover error state - setConflictConnectorName(undefined); - - const config = configParser(data, formItems); - - const { syncProfile, name, logo, logoDark, target } = data; - - const basePayload = { - config, - connectorId, - id: conditional(connectorType === ConnectorType.Social && callbackConnectorId.current), - metadata: conditional( - isStandard && { - logo, - logoDark, - target, - name: { en: name }, - } - ), - }; - - const payload = isSocialConnector - ? { ...basePayload, syncProfile: syncProfile === SyncProfileMode.EachSignIn } - : basePayload; - - try { - const createdConnector = await api - .post('api/connectors', { - json: payload, - }) - .json(); - - await updateConfigs({ passwordlessConfigured: true }); - - onClose(); - toast.success(t('general.saved')); - navigate( - `/connectors/${isSocialConnector ? ConnectorsTabs.Social : ConnectorsTabs.Passwordless}/${ - createdConnector.id - }` - ); - } catch (error: unknown) { - if (error instanceof HTTPError) { - const { response } = error; - const metadata = await response.json< - RequestErrorBody<{ connectorName: Record }> - >(); - - if (metadata.code === targetErrorCode) { - setConflictConnectorName(metadata.data.connectorName); - setError('target', {}, { shouldFocus: true }); - - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (data) => { + if (isSubmitting) { + return; } - throw error; - } - }); + // Recover error state + setConflictConnectorName(undefined); + + const config = configParser(data, formItems); + + const { syncProfile, name, logo, logoDark, target } = data; + + const basePayload = { + config, + connectorId, + id: conditional(connectorType === ConnectorType.Social && callbackConnectorId.current), + metadata: conditional( + isStandard && { + logo, + logoDark, + target, + name: { en: name }, + } + ), + }; + + const payload = isSocialConnector + ? { ...basePayload, syncProfile: syncProfile === SyncProfileMode.EachSignIn } + : basePayload; + + try { + const createdConnector = await api + .post('api/connectors', { + json: payload, + }) + .json(); + + await updateConfigs({ passwordlessConfigured: true }); + + onClose(); + toast.success(t('general.saved')); + navigate( + `/connectors/${isSocialConnector ? ConnectorsTabs.Social : ConnectorsTabs.Passwordless}/${ + createdConnector.id + }` + ); + } catch (error: unknown) { + if (error instanceof HTTPError) { + const { response } = error; + const metadata = await response.json< + RequestErrorBody<{ connectorName: Record }> + >(); + + if (metadata.code === targetErrorCode) { + setConflictConnectorName(metadata.data.connectorName); + setError('target', {}, { shouldFocus: true }); + + return; + } + } + + throw error; + } + }) + ); return ( { clearErrors(); - void handleSubmit(async ({ email }) => { - await api.post(`me/verification-codes`, { json: { email } }); - reset(); - navigate('../verification-code', { state: { email, action: 'changeEmail' } }); - })(); + void handleSubmit( + trySubmitSafe(async ({ email }) => { + await api.post(`me/verification-codes`, { json: { email } }); + reset(); + navigate('../verification-code', { state: { email, action: 'changeEmail' } }); + }) + )(); }; const { email: currentEmail } = parseLocationState(state); diff --git a/packages/console/src/pages/RoleDetails/RoleSettings/index.tsx b/packages/console/src/pages/RoleDetails/RoleSettings/index.tsx index c83e57c95..2bc636523 100644 --- a/packages/console/src/pages/RoleDetails/RoleSettings/index.tsx +++ b/packages/console/src/pages/RoleDetails/RoleSettings/index.tsx @@ -10,6 +10,7 @@ import FormField from '@/components/FormField'; import TextInput from '@/components/TextInput'; import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import useApi from '@/hooks/use-api'; +import { trySubmitSafe } from '@/utils/form'; import type { RoleDetailsOutletContext } from '../types'; @@ -27,16 +28,18 @@ function RoleSettings() { const api = useApi(); - const onSubmit = handleSubmit(async (formData) => { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + if (isSubmitting) { + return; + } - const updatedRole = await api.patch(`api/roles/${role.id}`, { json: formData }).json(); - reset(updatedRole); - onRoleUpdated(updatedRole); - toast.success(t('general.saved')); - }); + const updatedRole = await api.patch(`api/roles/${role.id}`, { json: formData }).json(); + reset(updatedRole); + onRoleUpdated(updatedRole); + toast.success(t('general.saved')); + }) + ); return ( <> diff --git a/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx b/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx index f83b132f3..a1dc5f95e 100644 --- a/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx +++ b/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx @@ -11,6 +11,7 @@ import RoleScopesTransfer from '@/components/RoleScopesTransfer'; import TextInput from '@/components/TextInput'; import useApi from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; +import { trySubmitSafe } from '@/utils/form'; export type Props = { onClose: (createdRole?: Role) => void; @@ -36,21 +37,23 @@ function CreateRoleForm({ onClose }: Props) { const api = useApi(); const { updateConfigs } = useConfigs(); - const onSubmit = handleSubmit(async ({ name, description, scopes }) => { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async ({ name, description, scopes }) => { + if (isSubmitting) { + return; + } - const payload: CreateRolePayload = { - name, - description, - scopeIds: conditional(scopes.length > 0 && scopes.map(({ id }) => id)), - }; + const payload: CreateRolePayload = { + name, + description, + scopeIds: conditional(scopes.length > 0 && scopes.map(({ id }) => id)), + }; - const createdRole = await api.post('api/roles', { json: payload }).json(); - await updateConfigs({ roleCreated: true }); - onClose(createdRole); - }); + const createdRole = await api.post('api/roles', { json: payload }).json(); + await updateConfigs({ roleCreated: true }); + onClose(createdRole); + }) + ); return ( { - if (!data || isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + if (!data || isSubmitting) { + return; + } - await Promise.all([ - api.patch('api/sign-in-exp', { - json: signInExperienceParser.toRemoteModel(formData), - }), - updateConfigs({ signInExperienceCustomized: true }), - ]); + await Promise.all([ + api.patch('api/sign-in-exp', { + json: signInExperienceParser.toRemoteModel(formData), + }), + updateConfigs({ signInExperienceCustomized: true }), + ]); - onClose(); - }); + onClose(); + }) + ); const onSkip = async () => { setIsLoading(true); diff --git a/packages/console/src/pages/SignInExperience/index.tsx b/packages/console/src/pages/SignInExperience/index.tsx index adea8d554..427b27c46 100644 --- a/packages/console/src/pages/SignInExperience/index.tsx +++ b/packages/console/src/pages/SignInExperience/index.tsx @@ -23,6 +23,7 @@ import useApi from '@/hooks/use-api'; import useConfigs from '@/hooks/use-configs'; import useUiLanguages from '@/hooks/use-ui-languages'; import useUserAssetsService from '@/hooks/use-user-assets-service'; +import { trySubmitSafe } from '@/utils/form'; import Preview from './components/Preview'; import SignUpAndSignInChangePreview from './components/SignUpAndSignInChangePreview'; @@ -133,22 +134,24 @@ function SignInExperience() { } }; - const onSubmit = handleSubmit(async (formData: SignInExperienceForm) => { - if (!data || isSaving) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (formData: SignInExperienceForm) => { + if (!data || isSaving) { + return; + } - const formatted = signInExperienceParser.toRemoteModel(formData); + const formatted = signInExperienceParser.toRemoteModel(formData); - // Sign-in methods changed, need to show confirm modal first. - if (!hasSignUpAndSignInConfigChanged(data, formatted)) { - setDataToCompare(formatted); + // Sign-in methods changed, need to show confirm modal first. + if (!hasSignUpAndSignInConfigChanged(data, formatted)) { + setDataToCompare(formatted); - return; - } + return; + } - await saveData(); - }); + await saveData(); + }) + ); if (isLoading) { return ; diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx index 03b37d05a..a93cc29dc 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx @@ -28,6 +28,7 @@ import { flattenTranslation, } from '@/pages/SignInExperience/utils/language'; import type { CustomPhraseResponse } from '@/types/custom-phrase'; +import { trySubmitSafe } from '@/utils/form'; import * as styles from './LanguageDetails.module.scss'; import { LanguageEditorContext } from './use-language-editor-context'; @@ -134,11 +135,13 @@ function LanguageDetails() { setSelectedLanguage(languages.find((languageTag) => languageTag !== selectedLanguage) ?? 'en'); }, [api, globalMutate, isDefaultLanguage, languages, selectedLanguage, setSelectedLanguage]); - const onSubmit = handleSubmit(async (formData: Translation) => { - const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData); - void mutate(updatedCustomPhrase); - toast.success(t('general.saved')); - }); + const onSubmit = handleSubmit( + trySubmitSafe(async (formData: Translation) => { + const updatedCustomPhrase = await upsertCustomPhrase(selectedLanguage, formData); + void mutate(updatedCustomPhrase); + toast.success(t('general.saved')); + }) + ); useEffect(() => { reset(defaultFormValues); diff --git a/packages/console/src/pages/TenantSettings/TenantDomainSettings/AddDomainForm/index.tsx b/packages/console/src/pages/TenantSettings/TenantDomainSettings/AddDomainForm/index.tsx index 98f4a48b3..7494c6248 100644 --- a/packages/console/src/pages/TenantSettings/TenantDomainSettings/AddDomainForm/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantDomainSettings/AddDomainForm/index.tsx @@ -6,6 +6,7 @@ import Button from '@/components/Button'; import TextInput from '@/components/TextInput'; import useApi from '@/hooks/use-api'; import { onKeyDownHandler } from '@/utils/a11y'; +import { trySubmitSafe } from '@/utils/form'; import * as styles from './index.module.scss'; @@ -36,10 +37,12 @@ function AddDomainForm({ onCustomDomainAdded }: Props) { const api = useApi(); - const onSubmit = handleSubmit(async (formData) => { - const createdDomain = await api.post('api/domains', { json: formData }).json(); - onCustomDomainAdded(createdDomain); - }); + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + const createdDomain = await api.post('api/domains', { json: formData }).json(); + onCustomDomainAdded(createdDomain); + }) + ); return (
diff --git a/packages/console/src/pages/UserDetails/UserSettings/index.tsx b/packages/console/src/pages/UserDetails/UserSettings/index.tsx index 28d5d1204..0a138fa2f 100644 --- a/packages/console/src/pages/UserDetails/UserSettings/index.tsx +++ b/packages/console/src/pages/UserDetails/UserSettings/index.tsx @@ -16,6 +16,7 @@ import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal'; import useApi from '@/hooks/use-api'; import { useConfirmModal } from '@/hooks/use-confirm-modal'; import useDocumentationUrl from '@/hooks/use-documentation-url'; +import { trySubmitSafe } from '@/utils/form'; import { safeParseJsonObject } from '@/utils/json'; import { parsePhoneNumber } from '@/utils/phone'; import { uriValidator } from '@/utils/validator'; @@ -47,47 +48,45 @@ function UserSettings() { const api = useApi(); - const onSubmit = handleSubmit(async (formData) => { - if (isSubmitting) { - return; - } - const { identities, id: userId } = user; - const { customData: inputCustomData, username, primaryEmail, primaryPhone } = formData; - - if (!username && !primaryEmail && !primaryPhone && Object.keys(identities).length === 0) { - const [result] = await show({ - ModalContent: t('user_details.warning_no_sign_in_identifier'), - type: 'confirm', - }); - - if (!result) { + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + if (isSubmitting) { return; } - } + const { identities, id: userId } = user; + const { customData: inputCustomData, username, primaryEmail, primaryPhone } = formData; - const parseResult = safeParseJsonObject(inputCustomData); + if (!username && !primaryEmail && !primaryPhone && Object.keys(identities).length === 0) { + const [result] = await show({ + ModalContent: t('user_details.warning_no_sign_in_identifier'), + type: 'confirm', + }); - if (!parseResult.success) { - toast.error(t('user_details.custom_data_invalid')); + if (!result) { + return; + } + } - return; - } + const parseResult = safeParseJsonObject(inputCustomData); - const payload: Partial = { - ...formData, - primaryPhone: parsePhoneNumber(primaryPhone), - customData: parseResult.data, - }; + if (!parseResult.success) { + toast.error(t('user_details.custom_data_invalid')); + + return; + } + + const payload: Partial = { + ...formData, + primaryPhone: parsePhoneNumber(primaryPhone), + customData: parseResult.data, + }; - try { const updatedUser = await api.patch(`api/users/${userId}`, { json: payload }).json(); reset(userDetailsParser.toLocalForm(updatedUser)); onUserUpdated(updatedUser); toast.success(t('general.saved')); - } catch { - // Do nothing since we only show error toasts, which is handled in the useApi hook - } - }); + }) + ); return ( <> diff --git a/packages/console/src/pages/Users/components/CreateForm/index.tsx b/packages/console/src/pages/Users/components/CreateForm/index.tsx index 866745d37..bc974f1ba 100644 --- a/packages/console/src/pages/Users/components/CreateForm/index.tsx +++ b/packages/console/src/pages/Users/components/CreateForm/index.tsx @@ -14,6 +14,7 @@ import TextInput from '@/components/TextInput'; import UserAccountInformation from '@/components/UserAccountInformation'; import useApi from '@/hooks/use-api'; import * as modalStyles from '@/scss/modal.module.scss'; +import { trySubmitSafe } from '@/utils/form'; import { generateRandomPassword } from '@/utils/password'; import { parsePhoneNumber } from '@/utils/phone'; @@ -62,46 +63,48 @@ function CreateForm({ onClose, onCreate }: Props) { } }; - const onSubmit = handleSubmit(async (data) => { - if (isSubmitting) { - return; - } + const onSubmit = handleSubmit( + trySubmitSafe(async (data) => { + if (isSubmitting) { + return; + } - setMissingIdentifierError(undefined); + setMissingIdentifierError(undefined); - if (!hasIdentifier()) { - setMissingIdentifierError(t('users.error_missing_identifier')); - return; - } + if (!hasIdentifier()) { + setMissingIdentifierError(t('users.error_missing_identifier')); + return; + } - const password = generateRandomPassword(); + const password = generateRandomPassword(); - const { primaryPhone } = data; + const { primaryPhone } = data; - const userData = { - ...data, - password, - ...conditional(primaryPhone && { primaryPhone: parsePhoneNumber(primaryPhone) }), - }; - - // Filter out empty values - const payload = Object.fromEntries( - Object.entries(userData).filter(([, value]) => Boolean(value)) - ); - - try { - const createdUser = await api.post('api/users', { json: payload }).json(); - - setCreatedUserInfo({ - user: createdUser, + const userData = { + ...data, password, - }); + ...conditional(primaryPhone && { primaryPhone: parsePhoneNumber(primaryPhone) }), + }; - onCreate(); - } catch { - // Do nothing since we only show error toasts, which is handled in the useApi hook - } - }); + // Filter out empty values + const payload = Object.fromEntries( + Object.entries(userData).filter(([, value]) => Boolean(value)) + ); + + try { + const createdUser = await api.post('api/users', { json: payload }).json(); + + setCreatedUserInfo({ + user: createdUser, + password, + }); + + onCreate(); + } catch { + // Do nothing since we only show error toasts, which is handled in the useApi hook + } + }) + ); return createdUserInfo ? ( { - const updatedHook = await api - .patch(`api/hooks/${hook.id}`, { json: webhookDetailsParser.toRemoteModel(formData) }) - .json(); - reset(webhookDetailsParser.toLocalForm(updatedHook)); - onHookUpdated(updatedHook); - toast.success(t('general.saved')); - }); + const onSubmit = handleSubmit( + trySubmitSafe(async (formData) => { + const updatedHook = await api + .patch(`api/hooks/${hook.id}`, { json: webhookDetailsParser.toRemoteModel(formData) }) + .json(); + reset(webhookDetailsParser.toLocalForm(updatedHook)); + onHookUpdated(updatedHook); + toast.success(t('general.saved')); + }) + ); return ( <> diff --git a/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx b/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx index 9126acc54..e6f2cf40a 100644 --- a/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx +++ b/packages/console/src/pages/Webhooks/components/CreateFormModal/CreateForm.tsx @@ -4,6 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form'; import Button from '@/components/Button'; import ModalLayout from '@/components/ModalLayout'; import useApi from '@/hooks/use-api'; +import { trySubmitSafe } from '@/utils/form'; import { type BasicWebhookFormType } from '../../types'; import BasicWebhookForm from '../BasicWebhookForm'; @@ -28,19 +29,21 @@ function CreateForm({ onClose }: Props) { const api = useApi(); - const onSubmit = handleSubmit(async (data) => { - const { name, events, url } = data; - const payload: CreateHookPayload = { - name, - events, - config: { - url, - }, - }; + const onSubmit = handleSubmit( + trySubmitSafe(async (data) => { + const { name, events, url } = data; + const payload: CreateHookPayload = { + name, + events, + config: { + url, + }, + }; - const created = await api.post('api/hooks', { json: payload }).json(); - onClose(created); - }); + const created = await api.post('api/hooks', { json: payload }).json(); + onClose(created); + }) + ); return ( (handler: SubmitHandler) => + async (formData: T, event?: React.BaseSyntheticEvent) => { + try { + await handler(formData, event); + } catch (error) { + if (error instanceof HTTPError && error.response.status !== 401) { + return; + } + + throw error; + } + };