diff --git a/packages/console/src/components/UnsavedChangesAlertModal/index.tsx b/packages/console/src/components/UnsavedChangesAlertModal/index.tsx index 68130e99e..42486418a 100644 --- a/packages/console/src/components/UnsavedChangesAlertModal/index.tsx +++ b/packages/console/src/components/UnsavedChangesAlertModal/index.tsx @@ -20,11 +20,19 @@ type BlockerNavigator = Navigator & { type Props = { hasUnsavedChanges: boolean; + parentPath?: string; }; -const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => { +const UnsavedChangesAlertModal = ({ hasUnsavedChanges, parentPath }: Props) => { const { navigator } = useContext(UNSAFE_NavigationContext); + /** + * Props `block` and `location` are removed from `Navigator` type in react-router, for the same reason as above. + * So we have to define our own type `BlockerNavigator` to acquire these props that actually exist in `navigator` object. + */ + // eslint-disable-next-line no-restricted-syntax + const { block, location } = navigator as BlockerNavigator; + const [displayAlert, setDisplayAlert] = useState(false); const [transition, setTransition] = useState(); @@ -35,12 +43,6 @@ const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => { return; } - /** - * Props `block` and `location` are removed from `Navigator` type in react-router, for the same reason as above. - * So we have to define our own type `BlockerNavigator` to acquire these props that actually exist in `navigator` object. - */ - // eslint-disable-next-line no-restricted-syntax - const { block, location } = navigator as BlockerNavigator; const { pathname } = location; const unblock = block((transition) => { @@ -53,6 +55,13 @@ const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => { return; } + if (parentPath && targetPathname.startsWith(parentPath)) { + unblock(); + transition.retry(); + + return; + } + setDisplayAlert(true); setTransition({ @@ -65,7 +74,7 @@ const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => { }); return unblock; - }, [navigator, hasUnsavedChanges]); + }, [navigator, hasUnsavedChanges, location, block, parentPath]); const leavePage = useCallback(() => { transition?.retry(); diff --git a/packages/console/src/pages/SignInExperience/components/TabWrapper/index.module.scss b/packages/console/src/pages/SignInExperience/components/TabWrapper/index.module.scss new file mode 100644 index 000000000..aaa3d95c7 --- /dev/null +++ b/packages/console/src/pages/SignInExperience/components/TabWrapper/index.module.scss @@ -0,0 +1,3 @@ +.hide { + display: none; +} diff --git a/packages/console/src/pages/SignInExperience/components/TabWrapper/index.tsx b/packages/console/src/pages/SignInExperience/components/TabWrapper/index.tsx new file mode 100644 index 000000000..b9bf8b60e --- /dev/null +++ b/packages/console/src/pages/SignInExperience/components/TabWrapper/index.tsx @@ -0,0 +1,16 @@ +import classNames from 'classnames'; +import type { ReactNode } from 'react'; + +import * as styles from './index.module.scss'; + +type Props = { + isActive: boolean; + className?: string; + children: ReactNode; +}; + +const TabWrapper = ({ isActive, className, children }: Props) => { + return
{children}
; +}; + +export default TabWrapper; diff --git a/packages/console/src/pages/SignInExperience/index.tsx b/packages/console/src/pages/SignInExperience/index.tsx index 0d436f073..47b9052e7 100644 --- a/packages/console/src/pages/SignInExperience/index.tsx +++ b/packages/console/src/pages/SignInExperience/index.tsx @@ -1,7 +1,6 @@ import type { SignInExperience as SignInExperienceType } from '@logto/schemas'; import classNames from 'classnames'; -import { nanoid } from 'nanoid'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; @@ -28,7 +27,13 @@ import Branding from './tabs/Branding'; import Others from './tabs/Others'; import SignUpAndSignIn from './tabs/SignUpAndSignIn'; import type { SignInExperienceForm } from './types'; -import { compareSignUpAndSignInConfigs, signInExperienceParser } from './utilities'; +import { + compareSignUpAndSignInConfigs, + getBrandingErrorCount, + getOthersErrorCount, + getSignUpAndSignInErrorCount, + signInExperienceParser, +} from './utilities'; const SignInExperience = () => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); @@ -38,14 +43,13 @@ const SignInExperience = () => { const { error: languageError, isLoading: isLoadingLanguages } = useUiLanguages(); const [dataToCompare, setDataToCompare] = useState(); - const { current: formId } = useRef(nanoid()); const methods = useForm(); const { reset, handleSubmit, getValues, watch, - formState: { isSubmitting, isDirty }, + formState: { isSubmitting, isDirty, errors }, } = methods; const api = useApi(); const formData = watch(); @@ -64,7 +68,7 @@ const SignInExperience = () => { if (defaultFormData) { reset(defaultFormData); } - }, [reset, defaultFormData, tab]); + }, [reset, defaultFormData]); const saveData = async () => { const updatedData = await api @@ -126,26 +130,29 @@ const SignInExperience = () => { className={styles.cardTitle} /> - + {t('sign_in_exp.tabs.branding')} - + {t('sign_in_exp.tabs.sign_up_and_sign_in')} - {t('sign_in_exp.tabs.others')} + + {t('sign_in_exp.tabs.others')} + {!data && error &&
{`error occurred: ${error.body?.message ?? error.message}`}
} {data && defaultFormData && (
-
- {tab === 'branding' && } - {tab === 'sign-up-and-sign-in' && } - {tab === 'others' && } + + {/* Todo: LOG-4766 Add Constants To Guard Router Path */} + + +
{formData.id && ( @@ -173,7 +180,10 @@ const SignInExperience = () => { {dataToCompare && } )} - +
); }; diff --git a/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx b/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx index fbe580e8a..550f89db2 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Branding/index.tsx @@ -1,11 +1,17 @@ +import TabWrapper from '../../components/TabWrapper'; +import * as styles from '../index.module.scss'; import BrandingForm from './BrandingForm'; import ColorForm from './ColorForm'; -const Branding = () => ( - <> +type Props = { + isActive: boolean; +}; + +const Branding = ({ isActive }: Props) => ( + - + ); export default Branding; diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/index.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/index.tsx index 22d8ad52b..c4cc546ca 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/index.tsx @@ -1,13 +1,19 @@ +import TabWrapper from '../../components/TabWrapper'; +import * as styles from '../index.module.scss'; import AuthenticationForm from './AuthenticationForm'; import LanguagesForm from './LanguagesForm'; import TermsForm from './TermsForm'; -const Others = () => ( - <> +type Props = { + isActive: boolean; +}; + +const Others = ({ isActive }: Props) => ( + - + ); export default Others; diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/index.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/index.tsx index fc3c7c31e..63c285ac7 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/components/SignInMethodEditBox/index.tsx @@ -129,6 +129,7 @@ const SignInMethodEditBox = () => { }} onDelete={() => { remove(index); + revalidate(); }} /> )} @@ -147,6 +148,7 @@ const SignInMethodEditBox = () => { verificationCode: getSignInMethodVerificationCodeCheckState(identifier), isPasswordPrimary: true, }); + revalidate(); }} />
diff --git a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/index.tsx b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/index.tsx index 8627c0a68..78e496bed 100644 --- a/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/index.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/SignUpAndSignIn/index.tsx @@ -1,13 +1,19 @@ +import TabWrapper from '../../components/TabWrapper'; +import * as styles from '../index.module.scss'; import SignInForm from './SignInForm'; import SignUpForm from './SignUpForm'; import SocialSignInForm from './SocialSignInForm'; -const SignUpAndSignIn = () => ( - <> +type Props = { + isActive: boolean; +}; + +const SignUpAndSignIn = ({ isActive }: Props) => ( + - + ); export default SignUpAndSignIn; diff --git a/packages/console/src/pages/SignInExperience/tabs/index.module.scss b/packages/console/src/pages/SignInExperience/tabs/index.module.scss index 810924c2d..80bf86e98 100644 --- a/packages/console/src/pages/SignInExperience/tabs/index.module.scss +++ b/packages/console/src/pages/SignInExperience/tabs/index.module.scss @@ -1,5 +1,11 @@ @use '@/scss/underscore' as _; +.tabContent { + > :not(:first-child) { + margin-top: _.unit(3); + } +} + .title { @include _.subhead-cap; color: var(--color-neutral-variant-60); diff --git a/packages/console/src/pages/SignInExperience/utilities.ts b/packages/console/src/pages/SignInExperience/utilities.ts index a40e09767..c1466f52b 100644 --- a/packages/console/src/pages/SignInExperience/utilities.ts +++ b/packages/console/src/pages/SignInExperience/utilities.ts @@ -2,6 +2,7 @@ import en from '@logto/phrases-ui/lib/locales/en'; import type { SignInExperience, Translation } from '@logto/schemas'; import { SignUpIdentifier, SignInMode } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; +import type { DeepRequired, FieldErrorsImpl } from 'react-hook-form'; import { isSignInMethodsDifferent, @@ -84,3 +85,50 @@ const emptyTranslation = (translation: Translation): Translation => }, {}); export const createEmptyUiTranslation = () => emptyTranslation(en.translation); + +export const getBrandingErrorCount = ( + errors: FieldErrorsImpl> +) => { + const { color, branding } = errors; + const colorFormErrorCount = color ? Object.keys(color).length : 0; + const brandingFormErrorCount = branding ? Object.keys(branding).length : 0; + + return colorFormErrorCount + brandingFormErrorCount; +}; + +export const getSignUpAndSignInErrorCount = ( + errors: FieldErrorsImpl>, + formData: SignInExperienceForm +) => { + const signUpIdentifier = formData.signUp?.identifier; + /** + * Note: we treat the `emailOrSms` sign-up identifier as 2 errors when it's invalid. + */ + const signUpIdentifierRelatedErrorCount = signUpIdentifier + ? signUpIdentifier === SignUpIdentifier.EmailOrSms + ? 2 + : 1 + : 0; + + const { signUp, signIn } = errors; + + const signUpErrorCount = signUp?.identifier ? signUpIdentifierRelatedErrorCount : 0; + + const signInMethodErrors = signIn?.methods; + + const signInMethodErrorCount = Array.isArray(signInMethodErrors) + ? signInMethodErrors.filter(Boolean).length + : 0; + + return signUpErrorCount + signInMethodErrorCount; +}; + +export const getOthersErrorCount = ( + errors: FieldErrorsImpl> +) => { + const { termsOfUse } = errors; + + const termsOfUseErrorCount = termsOfUse ? Object.keys(termsOfUse).length : 0; + + return termsOfUseErrorCount; +};