mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(console): unsaved changes alert (#1409)
This commit is contained in:
parent
98995cabcf
commit
098367e1a3
14 changed files with 370 additions and 316 deletions
|
@ -48,6 +48,7 @@
|
||||||
"dayjs": "^1.10.5",
|
"dayjs": "^1.10.5",
|
||||||
"dnd-core": "^16.0.0",
|
"dnd-core": "^16.0.0",
|
||||||
"eslint": "^8.10.0",
|
"eslint": "^8.10.0",
|
||||||
|
"history": "^5.3.0",
|
||||||
"i18next": "^21.6.12",
|
"i18next": "^21.6.12",
|
||||||
"i18next-browser-languagedetector": "^6.1.3",
|
"i18next-browser-languagedetector": "^6.1.3",
|
||||||
"ky": "^0.31.0",
|
"ky": "^0.31.0",
|
||||||
|
|
|
@ -19,6 +19,7 @@ export type ConfirmModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConfirmModal = ({
|
const ConfirmModal = ({
|
||||||
|
@ -31,6 +32,7 @@ const ConfirmModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onCancel,
|
onCancel,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
onClose = onCancel,
|
||||||
}: ConfirmModalProps) => {
|
}: ConfirmModalProps) => {
|
||||||
return (
|
return (
|
||||||
<ReactModal
|
<ReactModal
|
||||||
|
@ -47,7 +49,7 @@ const ConfirmModal = ({
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
className={classNames(styles.content, className)}
|
className={classNames(styles.content, className)}
|
||||||
onClose={onCancel}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ModalLayout>
|
</ModalLayout>
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import type { Blocker, Transition } from 'history';
|
||||||
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { UNSAFE_NavigationContext, Navigator } from 'react-router-dom';
|
||||||
|
|
||||||
|
import ConfirmModal from '../ConfirmModal';
|
||||||
|
|
||||||
|
type BlockerNavigator = Navigator & {
|
||||||
|
location: Location;
|
||||||
|
block(blocker: Blocker): () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
hasUnsavedChanges: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => {
|
||||||
|
const { navigator } = useContext(UNSAFE_NavigationContext);
|
||||||
|
|
||||||
|
const [displayAlert, setDisplayAlert] = useState(false);
|
||||||
|
const [transition, setTransition] = useState<Transition>();
|
||||||
|
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasUnsavedChanges) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
block,
|
||||||
|
location: { pathname },
|
||||||
|
} = navigator as BlockerNavigator;
|
||||||
|
|
||||||
|
const unblock = block((transition) => {
|
||||||
|
const {
|
||||||
|
location: { pathname: targetPathname },
|
||||||
|
} = transition;
|
||||||
|
|
||||||
|
// Note: We don't want to show the alert if the user is navigating to the same page.
|
||||||
|
if (targetPathname === pathname) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDisplayAlert(true);
|
||||||
|
|
||||||
|
setTransition({
|
||||||
|
...transition,
|
||||||
|
retry() {
|
||||||
|
unblock();
|
||||||
|
transition.retry();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return unblock;
|
||||||
|
}, [navigator, hasUnsavedChanges]);
|
||||||
|
|
||||||
|
const leavePage = useCallback(() => {
|
||||||
|
transition?.retry();
|
||||||
|
setDisplayAlert(false);
|
||||||
|
}, [transition]);
|
||||||
|
|
||||||
|
const stayOnPage = useCallback(() => {
|
||||||
|
setDisplayAlert(false);
|
||||||
|
}, [setDisplayAlert]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={displayAlert}
|
||||||
|
confirmButtonType="primary"
|
||||||
|
confirmButtonText="admin_console.general.stay_on_page"
|
||||||
|
cancelButtonText="admin_console.general.leave_page"
|
||||||
|
onCancel={leavePage}
|
||||||
|
onConfirm={stayOnPage}
|
||||||
|
onClose={stayOnPage}
|
||||||
|
>
|
||||||
|
{t('general.unsaved_changes_warning')}
|
||||||
|
</ConfirmModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnsavedChangesAlertModal;
|
|
@ -20,6 +20,7 @@ import FormField from '@/components/FormField';
|
||||||
import LinkButton from '@/components/LinkButton';
|
import LinkButton from '@/components/LinkButton';
|
||||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||||
import TextInput from '@/components/TextInput';
|
import TextInput from '@/components/TextInput';
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
import useApi, { RequestError } from '@/hooks/use-api';
|
import useApi, { RequestError } from '@/hooks/use-api';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import Back from '@/icons/Back';
|
import Back from '@/icons/Back';
|
||||||
|
@ -52,7 +53,7 @@ const ApiResourceDetails = () => {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting, errors },
|
formState: { isDirty, isSubmitting, errors },
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
defaultValues: data,
|
defaultValues: data,
|
||||||
});
|
});
|
||||||
|
@ -179,6 +180,7 @@ const ApiResourceDetails = () => {
|
||||||
</Card>
|
</Card>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,24 +1,43 @@
|
||||||
import { SnakeCaseOidcConfig } from '@logto/schemas';
|
import { Application, SnakeCaseOidcConfig } from '@logto/schemas';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
import FormField from '@/components/FormField';
|
import FormField from '@/components/FormField';
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
|
|
||||||
import * as styles from '../index.module.scss';
|
import * as styles from '../index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
oidcConfig: SnakeCaseOidcConfig;
|
oidcConfig: SnakeCaseOidcConfig;
|
||||||
|
defaultData: Application;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AdvancedSettings = ({ oidcConfig }: Props) => {
|
const AdvancedSettings = ({ oidcConfig, defaultData }: Props) => {
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
formState: { isDirty },
|
||||||
|
} = useFormContext<Application>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(defaultData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset(defaultData);
|
||||||
|
};
|
||||||
|
}, [reset, defaultData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormField title="admin_console.application_details.token_endpoint">
|
<>
|
||||||
<CopyToClipboard
|
<FormField title="admin_console.application_details.token_endpoint">
|
||||||
className={styles.textField}
|
<CopyToClipboard
|
||||||
value={oidcConfig.token_endpoint}
|
className={styles.textField}
|
||||||
variant="border"
|
value={oidcConfig.token_endpoint}
|
||||||
/>
|
variant="border"
|
||||||
</FormField>
|
/>
|
||||||
|
</FormField>
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Application, ApplicationType, SnakeCaseOidcConfig } from '@logto/schemas';
|
import { Application, ApplicationType, SnakeCaseOidcConfig } from '@logto/schemas';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
import { Controller, useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import MultiTextInput from '@/components/MultiTextInput';
|
||||||
import { MultiTextInputRule } from '@/components/MultiTextInput/types';
|
import { MultiTextInputRule } from '@/components/MultiTextInput/types';
|
||||||
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
|
import { createValidatorForRhf, convertRhfErrorMessage } from '@/components/MultiTextInput/utils';
|
||||||
import TextInput from '@/components/TextInput';
|
import TextInput from '@/components/TextInput';
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
import { uriOriginValidator, uriValidator } from '@/utilities/validator';
|
import { uriOriginValidator, uriValidator } from '@/utilities/validator';
|
||||||
|
|
||||||
import * as styles from '../index.module.scss';
|
import * as styles from '../index.module.scss';
|
||||||
|
@ -16,15 +17,25 @@ import * as styles from '../index.module.scss';
|
||||||
type Props = {
|
type Props = {
|
||||||
applicationType: ApplicationType;
|
applicationType: ApplicationType;
|
||||||
oidcConfig: SnakeCaseOidcConfig;
|
oidcConfig: SnakeCaseOidcConfig;
|
||||||
|
defaultData: Application;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Settings = ({ applicationType, oidcConfig }: Props) => {
|
const Settings = ({ applicationType, oidcConfig, defaultData }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
reset,
|
||||||
|
formState: { errors, isDirty },
|
||||||
} = useFormContext<Application>();
|
} = useFormContext<Application>();
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(defaultData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset(defaultData);
|
||||||
|
};
|
||||||
|
}, [reset, defaultData]);
|
||||||
|
|
||||||
const uriPatternRules: MultiTextInputRule = {
|
const uriPatternRules: MultiTextInputRule = {
|
||||||
pattern: {
|
pattern: {
|
||||||
|
@ -141,6 +152,7 @@ const Settings = ({ applicationType, oidcConfig }: Props) => {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -39,7 +39,7 @@ const mapToUriOriginFormatArrays = (value?: string[]) =>
|
||||||
|
|
||||||
const ApplicationDetails = () => {
|
const ApplicationDetails = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const location = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { data, error, mutate } = useSWR<Application, RequestError>(
|
const { data, error, mutate } = useSWR<Application, RequestError>(
|
||||||
id && `/api/applications/${id}`
|
id && `/api/applications/${id}`
|
||||||
|
@ -101,7 +101,7 @@ const ApplicationDetails = () => {
|
||||||
setIsReadmeOpen(false);
|
setIsReadmeOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isAdvancedSettings = location.pathname.includes('advanced-settings');
|
const isAdvancedSettings = pathname.includes('advanced-settings');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={detailsStyles.container}>
|
<div className={detailsStyles.container}>
|
||||||
|
@ -177,10 +177,15 @@ const ApplicationDetails = () => {
|
||||||
<FormProvider {...formMethods}>
|
<FormProvider {...formMethods}>
|
||||||
<form className={classNames(styles.form, detailsStyles.body)} onSubmit={onSubmit}>
|
<form className={classNames(styles.form, detailsStyles.body)} onSubmit={onSubmit}>
|
||||||
<div className={styles.fields}>
|
<div className={styles.fields}>
|
||||||
{isAdvancedSettings ? (
|
{isAdvancedSettings && (
|
||||||
<AdvancedSettings oidcConfig={oidcConfig} />
|
<AdvancedSettings oidcConfig={oidcConfig} defaultData={data} />
|
||||||
) : (
|
)}
|
||||||
<Settings applicationType={data.type} oidcConfig={oidcConfig} />
|
{!isAdvancedSettings && (
|
||||||
|
<Settings
|
||||||
|
applicationType={data.type}
|
||||||
|
oidcConfig={oidcConfig}
|
||||||
|
defaultData={data}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={detailsStyles.footer}>
|
<div className={detailsStyles.footer}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SignInExperience as SignInExperienceType } from '@logto/schemas';
|
import { SignInExperience as SignInExperienceType } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -16,17 +16,15 @@ import useApi, { RequestError } from '@/hooks/use-api';
|
||||||
import useSettings from '@/hooks/use-settings';
|
import useSettings from '@/hooks/use-settings';
|
||||||
import * as detailsStyles from '@/scss/details.module.scss';
|
import * as detailsStyles from '@/scss/details.module.scss';
|
||||||
|
|
||||||
import BrandingForm from './components/BrandingForm';
|
|
||||||
import ColorForm from './components/ColorForm';
|
|
||||||
import LanguagesForm from './components/LanguagesForm';
|
|
||||||
import Preview from './components/Preview';
|
import Preview from './components/Preview';
|
||||||
import SignInMethodsChangePreview from './components/SignInMethodsChangePreview';
|
import SignInMethodsChangePreview from './components/SignInMethodsChangePreview';
|
||||||
import SignInMethodsForm from './components/SignInMethodsForm';
|
|
||||||
import Skeleton from './components/Skeleton';
|
import Skeleton from './components/Skeleton';
|
||||||
import TermsForm from './components/TermsForm';
|
|
||||||
import Welcome from './components/Welcome';
|
import Welcome from './components/Welcome';
|
||||||
import usePreviewConfigs from './hooks';
|
import usePreviewConfigs from './hooks';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
import BrandingTab from './tabs/BrandingTab';
|
||||||
|
import OthersTab from './tabs/OthersTab';
|
||||||
|
import SignInMethodsTab from './tabs/SignInMethodsTab';
|
||||||
import { SignInExperienceForm } from './types';
|
import { SignInExperienceForm } from './types';
|
||||||
import { compareSignInMethods, signInExperienceParser } from './utilities';
|
import { compareSignInMethods, signInExperienceParser } from './utilities';
|
||||||
|
|
||||||
|
@ -50,11 +48,19 @@ const SignInExperience = () => {
|
||||||
|
|
||||||
const previewConfigs = usePreviewConfigs(formData, isDirty, data);
|
const previewConfigs = usePreviewConfigs(formData, isDirty, data);
|
||||||
|
|
||||||
useEffect(() => {
|
const defaultFormData = useMemo(() => {
|
||||||
if (data && !isDirty) {
|
if (!data) {
|
||||||
reset(signInExperienceParser.toLocalForm(data));
|
return;
|
||||||
}
|
}
|
||||||
}, [data, reset, isDirty]);
|
|
||||||
|
return signInExperienceParser.toLocalForm(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultFormData && !isDirty) {
|
||||||
|
reset(defaultFormData);
|
||||||
|
}
|
||||||
|
}, [reset, isDirty, defaultFormData]);
|
||||||
|
|
||||||
const saveData = async () => {
|
const saveData = async () => {
|
||||||
const updatedData = await api
|
const updatedData = await api
|
||||||
|
@ -113,22 +119,18 @@ const SignInExperience = () => {
|
||||||
</TabNavItem>
|
</TabNavItem>
|
||||||
</TabNav>
|
</TabNav>
|
||||||
{!data && error && <div>{`error occurred: ${error.body?.message ?? error.message}`}</div>}
|
{!data && error && <div>{`error occurred: ${error.body?.message ?? error.message}`}</div>}
|
||||||
{data && (
|
{data && defaultFormData && (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className={styles.formWrapper} onSubmit={onSubmit}>
|
<form className={styles.formWrapper} onSubmit={onSubmit}>
|
||||||
<div className={classNames(detailsStyles.body, styles.form)}>
|
<div className={classNames(detailsStyles.body, styles.form)}>
|
||||||
{tab === 'branding' && (
|
{tab === 'branding' && (
|
||||||
<>
|
<BrandingTab defaultData={defaultFormData} isDataDirty={isDirty} />
|
||||||
<ColorForm />
|
)}
|
||||||
<BrandingForm />
|
{tab === 'methods' && (
|
||||||
</>
|
<SignInMethodsTab defaultData={defaultFormData} isDataDirty={isDirty} />
|
||||||
)}
|
)}
|
||||||
{tab === 'methods' && <SignInMethodsForm />}
|
|
||||||
{tab === 'others' && (
|
{tab === 'others' && (
|
||||||
<>
|
<OthersTab defaultData={defaultFormData} isDataDirty={isDirty} />
|
||||||
<TermsForm />
|
|
||||||
<LanguagesForm />
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={detailsStyles.footer}>
|
<div className={detailsStyles.footer}>
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
|
|
||||||
|
import BrandingForm from '../components/BrandingForm';
|
||||||
|
import ColorForm from '../components/ColorForm';
|
||||||
|
import { SignInExperienceForm } from '../types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultData: SignInExperienceForm;
|
||||||
|
isDataDirty: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BrandingTab = ({ defaultData, isDataDirty }: Props) => {
|
||||||
|
const { reset } = useFormContext<SignInExperienceForm>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(defaultData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset(defaultData);
|
||||||
|
};
|
||||||
|
}, [reset, defaultData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ColorForm />
|
||||||
|
<BrandingForm />
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDataDirty} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BrandingTab;
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
|
|
||||||
|
import LanguagesForm from '../components/LanguagesForm';
|
||||||
|
import TermsForm from '../components/TermsForm';
|
||||||
|
import { SignInExperienceForm } from '../types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultData: SignInExperienceForm;
|
||||||
|
isDataDirty: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OthersTab = ({ defaultData, isDataDirty }: Props) => {
|
||||||
|
const { reset } = useFormContext<SignInExperienceForm>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(defaultData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset(defaultData);
|
||||||
|
};
|
||||||
|
}, [reset, defaultData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TermsForm />
|
||||||
|
<LanguagesForm />
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDataDirty} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OthersTab;
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||||
|
|
||||||
|
import SignInMethodsForm from '../components/SignInMethodsForm';
|
||||||
|
import { SignInExperienceForm } from '../types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultData: SignInExperienceForm;
|
||||||
|
isDataDirty: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SignInMethodsTab = ({ defaultData, isDataDirty }: Props) => {
|
||||||
|
const { reset } = useFormContext<SignInExperienceForm>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(defaultData);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
reset(defaultData);
|
||||||
|
};
|
||||||
|
}, [reset, defaultData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SignInMethodsForm />
|
||||||
|
<UnsavedChangesAlertModal hasUnsavedChanges={isDataDirty} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignInMethodsTab;
|
|
@ -39,6 +39,10 @@ const translation = {
|
||||||
add_another: '+ Add Another',
|
add_another: '+ Add Another',
|
||||||
deletion_confirmation: 'Are you sure you want to delete this {{title}}?',
|
deletion_confirmation: 'Are you sure you want to delete this {{title}}?',
|
||||||
settings_nav: 'Settings',
|
settings_nav: 'Settings',
|
||||||
|
unsaved_changes_warning:
|
||||||
|
'You have made some changes. Are you sure you want to leave this page?',
|
||||||
|
leave_page: 'Leave Page',
|
||||||
|
stay_on_page: 'Stay on Page',
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
something_went_wrong: 'Oops! Something went wrong.',
|
something_went_wrong: 'Oops! Something went wrong.',
|
||||||
|
|
|
@ -41,6 +41,9 @@ const translation = {
|
||||||
add_another: '+ 新增',
|
add_another: '+ 新增',
|
||||||
deletion_confirmation: '你确定要删除这个 {{title}} 吗?',
|
deletion_confirmation: '你确定要删除这个 {{title}} 吗?',
|
||||||
settings_nav: '设置',
|
settings_nav: '设置',
|
||||||
|
unsaved_changes_warning: '还有未保存的变更, 确定要离开吗?',
|
||||||
|
leave_page: '离开此页',
|
||||||
|
stay_on_page: '留在此页',
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
something_went_wrong: '哎呀,出错了!',
|
something_went_wrong: '哎呀,出错了!',
|
||||||
|
|
366
pnpm-lock.yaml
366
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue