mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
fix(console): reset form state when the submit handler throws (#4029)
This commit is contained in:
parent
9a9d1d2ed4
commit
6726bcaa0f
24 changed files with 412 additions and 329 deletions
|
@ -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<HTMLInputElement>, value: string[]) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<SignInExperienceType>();
|
||||
const updatedData = await api
|
||||
.patch(buildUrl('api/sign-in-exp', { removeUnusedDemoSocialConnector: '1' }), {
|
||||
json: parser.onboardSieConfigToSignInExperience(formData, signInExperience),
|
||||
})
|
||||
.json<SignInExperienceType>();
|
||||
|
||||
void mutate(updatedData);
|
||||
void mutate(updatedData);
|
||||
|
||||
onSuccess();
|
||||
};
|
||||
onSuccess();
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton />;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<Scope>();
|
||||
const createdScope = await api
|
||||
.post(`api/resources/${resourceId}/scopes`, { json: formData })
|
||||
.json<Scope>();
|
||||
|
||||
onClose(createdScope);
|
||||
});
|
||||
onClose(createdScope);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
|
|
|
@ -13,6 +13,7 @@ import TextLink from '@/components/TextLink';
|
|||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import type { ApiResourceDetailsOutletContext } from '../types';
|
||||
|
||||
|
@ -34,24 +35,26 @@ function ApiResourceSettings() {
|
|||
|
||||
const api = useApi();
|
||||
|
||||
const onSubmit = handleSubmit(async ({ isDefault, ...rest }) => {
|
||||
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<Resource>(),
|
||||
api
|
||||
.patch(`api/resources/${resource.id}/is-default`, { json: { isDefault } })
|
||||
.json<Resource>(),
|
||||
]);
|
||||
const [data] = await Promise.all([
|
||||
api.patch(`api/resources/${resource.id}`, { json: rest }).json<Resource>(),
|
||||
api
|
||||
.patch(`api/resources/${resource.id}/is-default`, { json: { isDefault } })
|
||||
.json<Resource>(),
|
||||
]);
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
|
|
|
@ -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<Resource>();
|
||||
onClose?.(createdApiResource);
|
||||
});
|
||||
const createdApiResource = await api.post('api/resources', { json: data }).json<Resource>();
|
||||
onClose?.(createdApiResource);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
|
|
|
@ -27,6 +27,7 @@ import type { RequestError } from '@/hooks/use-api';
|
|||
import useApi from '@/hooks/use-api';
|
||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import Guide from '../Applications/components/Guide';
|
||||
import GuideModal from '../Applications/components/Guide/GuideModal';
|
||||
|
@ -79,34 +80,36 @@ function ApplicationDetails() {
|
|||
reset(data);
|
||||
}, [data, reset]);
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
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<Application>();
|
||||
void mutate();
|
||||
toast.success(t('general.saved'));
|
||||
});
|
||||
})
|
||||
.json<Application>();
|
||||
void mutate();
|
||||
toast.success(t('general.saved'));
|
||||
})
|
||||
);
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!data || isDeleting) {
|
||||
|
|
|
@ -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<Application>();
|
||||
void updateConfigs({
|
||||
applicationCreated: true,
|
||||
...conditional(
|
||||
createdApp.type === ApplicationType.MachineToMachine && { m2mApplicationCreated: true }
|
||||
),
|
||||
});
|
||||
onClose?.(createdApp);
|
||||
});
|
||||
const createdApp = await api.post('api/applications', { json: data }).json<Application>();
|
||||
void updateConfigs({
|
||||
applicationCreated: true,
|
||||
...conditional(
|
||||
createdApp.type === ApplicationType.MachineToMachine && { m2mApplicationCreated: true }
|
||||
),
|
||||
});
|
||||
onClose?.(createdApp);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useConnectorFormConfigParser } from '@/pages/Connectors/components/Conn
|
|||
import { initFormData } from '@/pages/Connectors/components/ConnectorForm/utils';
|
||||
import type { ConnectorFormType } from '@/pages/Connectors/types';
|
||||
import { SyncProfileMode } from '@/pages/Connectors/types';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import SenderTester from './SenderTester';
|
||||
|
||||
|
@ -71,33 +72,35 @@ function ConnectorContent({ isDeleted, connectorData, onConnectorUpdated }: Prop
|
|||
|
||||
const configParser = useConnectorFormConfigParser();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
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<ConnectorResponse>();
|
||||
const updatedConnector = await api
|
||||
.patch(`api/connectors/${id}`, {
|
||||
json: body,
|
||||
})
|
||||
.json<ConnectorResponse>();
|
||||
|
||||
onConnectorUpdated(updatedConnector);
|
||||
toast.success(t('general.saved'));
|
||||
});
|
||||
onConnectorUpdated(updatedConnector);
|
||||
toast.success(t('general.saved'));
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
|
|
|
@ -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 (
|
||||
<div className={className}>
|
||||
|
|
|
@ -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<ConnectorResponse>();
|
||||
|
||||
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<string, string> }>
|
||||
>();
|
||||
|
||||
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<ConnectorResponse>();
|
||||
|
||||
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<string, string> }>
|
||||
>();
|
||||
|
||||
if (metadata.code === targetErrorCode) {
|
||||
setConflictConnectorName(metadata.data.connectorName);
|
||||
setError('target', {}, { shouldFocus: true });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
@ -7,6 +7,7 @@ import Button from '@/components/Button';
|
|||
import TextInput from '@/components/TextInput';
|
||||
import { adminTenantEndpoint, meApi } from '@/consts';
|
||||
import { useStaticApi } from '@/hooks/use-api';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import MainFlowLikeModal from '../../components/MainFlowLikeModal';
|
||||
import { parseLocationState } from '../../utils';
|
||||
|
@ -36,11 +37,13 @@ function LinkEmailModal() {
|
|||
|
||||
const onSubmit = () => {
|
||||
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);
|
||||
|
|
|
@ -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<Role>();
|
||||
reset(updatedRole);
|
||||
onRoleUpdated(updatedRole);
|
||||
toast.success(t('general.saved'));
|
||||
});
|
||||
const updatedRole = await api.patch(`api/roles/${role.id}`, { json: formData }).json<Role>();
|
||||
reset(updatedRole);
|
||||
onRoleUpdated(updatedRole);
|
||||
toast.success(t('general.saved'));
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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<Role>();
|
||||
await updateConfigs({ roleCreated: true });
|
||||
onClose(createdRole);
|
||||
});
|
||||
const createdRole = await api.post('api/roles', { json: payload }).json<Role>();
|
||||
await updateConfigs({ roleCreated: true });
|
||||
onClose(createdRole);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
|
|
|
@ -15,6 +15,7 @@ import useApi from '@/hooks/use-api';
|
|||
import useConfigs from '@/hooks/use-configs';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import usePreviewConfigs from '../../hooks/use-preview-configs';
|
||||
import BrandingForm from '../../tabs/Branding/BrandingForm';
|
||||
|
@ -59,20 +60,22 @@ function GuideModal({ isOpen, onClose }: Props) {
|
|||
await updatePreferences({ experienceNoticeConfirmed: true });
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
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);
|
||||
|
|
|
@ -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 <Skeleton />;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<Domain>();
|
||||
onCustomDomainAdded(createdDomain);
|
||||
});
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async (formData) => {
|
||||
const createdDomain = await api.post('api/domains', { json: formData }).json<Domain>();
|
||||
onCustomDomainAdded(createdDomain);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.addDomain}>
|
||||
|
|
|
@ -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<User> = {
|
||||
...formData,
|
||||
primaryPhone: parsePhoneNumber(primaryPhone),
|
||||
customData: parseResult.data,
|
||||
};
|
||||
if (!parseResult.success) {
|
||||
toast.error(t('user_details.custom_data_invalid'));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: Partial<User> = {
|
||||
...formData,
|
||||
primaryPhone: parsePhoneNumber(primaryPhone),
|
||||
customData: parseResult.data,
|
||||
};
|
||||
|
||||
try {
|
||||
const updatedUser = await api.patch(`api/users/${userId}`, { json: payload }).json<User>();
|
||||
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 (
|
||||
<>
|
||||
|
|
|
@ -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<User>();
|
||||
|
||||
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<User>();
|
||||
|
||||
setCreatedUserInfo({
|
||||
user: createdUser,
|
||||
password,
|
||||
});
|
||||
|
||||
onCreate();
|
||||
} catch {
|
||||
// Do nothing since we only show error toasts, which is handled in the useApi hook
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return createdUserInfo ? (
|
||||
<UserAccountInformation
|
||||
|
|
|
@ -9,6 +9,7 @@ import FormCard from '@/components/FormCard';
|
|||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import BasicWebhookForm from '@/pages/Webhooks/components/BasicWebhookForm';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import { type WebhookDetailsFormType, type WebhookDetailsOutletContext } from '../types';
|
||||
import { webhookDetailsParser } from '../utils';
|
||||
|
@ -32,14 +33,16 @@ function WebhookSettings() {
|
|||
formState: { isSubmitting, isDirty },
|
||||
} = formMethods;
|
||||
|
||||
const onSubmit = handleSubmit(async (formData) => {
|
||||
const updatedHook = await api
|
||||
.patch(`api/hooks/${hook.id}`, { json: webhookDetailsParser.toRemoteModel(formData) })
|
||||
.json<Hook>();
|
||||
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<Hook>();
|
||||
reset(webhookDetailsParser.toLocalForm(updatedHook));
|
||||
onHookUpdated(updatedHook);
|
||||
toast.success(t('general.saved'));
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -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<Hook>();
|
||||
onClose(created);
|
||||
});
|
||||
const created = await api.post('api/hooks', { json: payload }).json<Hook>();
|
||||
onClose(created);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
|
|
21
packages/console/src/utils/form.ts
Normal file
21
packages/console/src/utils/form.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { HTTPError } from 'ky';
|
||||
import { type FieldValues, type SubmitHandler } from 'react-hook-form';
|
||||
|
||||
/**
|
||||
* After upgrading the react-hook-form to v7.42.0, the `isSubmitting` flag does not recover when the submit handler throws.
|
||||
* So we need to catch the error and do nothing if the error is an HTTPError and the status code is not 401 to prevent the `isSubmitting` flag from being stuck.
|
||||
* Reference: https://github.com/orgs/react-hook-form/discussions/10103#discussioncomment-5927542
|
||||
*/
|
||||
export const trySubmitSafe =
|
||||
<T extends FieldValues>(handler: SubmitHandler<T>) =>
|
||||
async (formData: T, event?: React.BaseSyntheticEvent) => {
|
||||
try {
|
||||
await handler(formData, event);
|
||||
} catch (error) {
|
||||
if (error instanceof HTTPError && error.response.status !== 401) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
Loading…
Add table
Reference in a new issue