From 0d1a0a974693bf87949df3fa84c5422191199bf2 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Thu, 4 Jan 2024 11:15:34 +0800 Subject: [PATCH] refactor(console): refactor sie form data parser (#5195) --- .../SignUpDiffSection.tsx | 6 +- .../components/Welcome/GuideModal.tsx | 6 +- .../hooks/use-preview-configs.ts | 4 +- .../src/pages/SignInExperience/index.tsx | 18 ++-- .../src/pages/SignInExperience/types.ts | 8 ++ .../src/pages/SignInExperience/utils/form.ts | 88 +---------------- .../pages/SignInExperience/utils/parser.ts | 97 +++++++++++++++++++ 7 files changed, 122 insertions(+), 105 deletions(-) create mode 100644 packages/console/src/pages/SignInExperience/utils/parser.ts diff --git a/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SignUpDiffSection.tsx b/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SignUpDiffSection.tsx index d3215ec20..1a0953ebb 100644 --- a/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SignUpDiffSection.tsx +++ b/packages/console/src/pages/SignInExperience/components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/SignUpDiffSection.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import DynamicT from '@/ds-components/DynamicT'; import { signUpIdentifierPhrase } from '@/pages/SignInExperience/constants'; import type { SignUpForm } from '@/pages/SignInExperience/types'; -import { signInExperienceParser } from '@/pages/SignInExperience/utils/form'; +import { signUpFormDataParser } from '@/pages/SignInExperience/utils/parser'; import DiffSegment from './DiffSegment'; import * as styles from './index.module.scss'; @@ -19,8 +19,8 @@ type Props = { function SignUpDiffSection({ before, after, isAfter = false }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const parsedBefore = signInExperienceParser.toLocalSignUp(before); - const parsedAfter = signInExperienceParser.toLocalSignUp(after); + const parsedBefore = signUpFormDataParser.fromSignUp(before); + const parsedAfter = signUpFormDataParser.fromSignUp(after); const signUpDiff = isAfter ? diff(parsedBefore, parsedAfter) : diff(parsedAfter, parsedBefore); const signUp = isAfter ? parsedAfter : parsedBefore; const hasChanged = (path: keyof SignUpForm) => getSafe(signUpDiff, path) !== undefined; diff --git a/packages/console/src/pages/SignInExperience/components/Welcome/GuideModal.tsx b/packages/console/src/pages/SignInExperience/components/Welcome/GuideModal.tsx index ed1a521a2..31c7b677d 100644 --- a/packages/console/src/pages/SignInExperience/components/Welcome/GuideModal.tsx +++ b/packages/console/src/pages/SignInExperience/components/Welcome/GuideModal.tsx @@ -22,7 +22,7 @@ import BrandingForm from '../../tabs/Branding/BrandingForm'; import LanguagesForm from '../../tabs/Content/LanguagesForm'; import TermsForm from '../../tabs/Content/TermsForm'; import type { SignInExperienceForm } from '../../types'; -import { signInExperienceParser } from '../../utils/form'; +import { sieFormDataParser } from '../../utils/parser'; import Preview from '../Preview'; import * as styles from './GuideModal.module.scss'; @@ -52,7 +52,7 @@ function GuideModal({ isOpen, onClose }: Props) { useEffect(() => { if (data && !isDirty) { - reset(signInExperienceParser.toLocalForm(data)); + reset(sieFormDataParser.fromSignInExperience(data)); } }, [data, reset, isDirty]); @@ -68,7 +68,7 @@ function GuideModal({ isOpen, onClose }: Props) { await Promise.all([ api.patch('api/sign-in-exp', { - json: signInExperienceParser.toRemoteModel(formData), + json: sieFormDataParser.toSignInExperience(formData), }), updateConfigs({ signInExperienceCustomized: true }), ]); diff --git a/packages/console/src/pages/SignInExperience/hooks/use-preview-configs.ts b/packages/console/src/pages/SignInExperience/hooks/use-preview-configs.ts index d15a25015..a063622f4 100644 --- a/packages/console/src/pages/SignInExperience/hooks/use-preview-configs.ts +++ b/packages/console/src/pages/SignInExperience/hooks/use-preview-configs.ts @@ -4,7 +4,7 @@ import { useEffect, useState, useMemo } from 'react'; import useDebounce from '@/hooks/use-debounce'; import type { SignInExperienceForm } from '../types'; -import { signInExperienceParser } from '../utils/form'; +import { sieFormDataParser } from '../utils/parser'; const usePreviewConfigs = ( formData: SignInExperienceForm, @@ -27,7 +27,7 @@ const usePreviewConfigs = ( return data; } - return signInExperienceParser.toRemoteModel({ + return sieFormDataParser.toSignInExperience({ ...restFormData, customCss: debouncedCustomCss, }); diff --git a/packages/console/src/pages/SignInExperience/index.tsx b/packages/console/src/pages/SignInExperience/index.tsx index 5cdf04d48..2eedd9a43 100644 --- a/packages/console/src/pages/SignInExperience/index.tsx +++ b/packages/console/src/pages/SignInExperience/index.tsx @@ -40,8 +40,8 @@ import { getBrandingErrorCount, getContentErrorCount, getSignUpAndSignInErrorCount, - signInExperienceParser, } from './utils/form'; +import { sieFormDataParser } from './utils/parser'; export enum SignInExperienceTab { Branding = 'branding', @@ -117,7 +117,7 @@ function SignInExperience() { return; } - return signInExperienceParser.toLocalForm(data); + return sieFormDataParser.fromSignInExperience(data); }, [data]); useEffect(() => { @@ -133,16 +133,12 @@ function SignInExperience() { setIsSaving(true); try { - /** - * Note: extract `mfa` since it will not be updated on the SIE config page. - * This is a temporary solution, we will split `SignInExperience` type into multiple types - * when the SIE config page is split into multiple pages. - */ - const { mfa, ...payload } = signInExperienceParser.toRemoteModel(getValues()); const updatedData = await api - .patch('api/sign-in-exp', { json: payload }) + .patch('api/sign-in-exp', { + json: sieFormDataParser.toUpdateSignInExperienceData(getValues()), + }) .json(); - reset(signInExperienceParser.toLocalForm(updatedData)); + reset(sieFormDataParser.fromSignInExperience(updatedData)); void mutate(updatedData); setDataToCompare(undefined); await updateConfigs({ signInExperienceCustomized: true }); @@ -158,7 +154,7 @@ function SignInExperience() { return; } - const formatted = signInExperienceParser.toRemoteModel(formData); + const formatted = sieFormDataParser.toSignInExperience(formData); // Sign-in methods changed, need to show confirm modal first. if (!hasSignUpAndSignInConfigChanged(data, formatted)) { diff --git a/packages/console/src/pages/SignInExperience/types.ts b/packages/console/src/pages/SignInExperience/types.ts index 662237c3f..53e13c6b5 100644 --- a/packages/console/src/pages/SignInExperience/types.ts +++ b/packages/console/src/pages/SignInExperience/types.ts @@ -43,3 +43,11 @@ export type SignInMethodsObject = Record< SignInIdentifier, { password: boolean; verificationCode: boolean } >; + +export type UpdateSignInExperienceData = Omit & { + /** + * `mfa` data will not be updated in the sign-in experience page. + * Hard code it to `undefined` to have a better type checking when constructing the update data. + */ + mfa: undefined; +}; diff --git a/packages/console/src/pages/SignInExperience/utils/form.ts b/packages/console/src/pages/SignInExperience/utils/form.ts index 9cb2f40a5..857016410 100644 --- a/packages/console/src/pages/SignInExperience/utils/form.ts +++ b/packages/console/src/pages/SignInExperience/utils/form.ts @@ -1,7 +1,4 @@ -import { passwordPolicyGuard } from '@logto/core-kit'; -import type { SignInExperience, SignUp } from '@logto/schemas'; -import { SignInMode, SignInIdentifier } from '@logto/schemas'; -import { conditional } from '@silverhand/essentials'; +import type { SignInExperience } from '@logto/schemas'; import type { DeepRequired, FieldErrorsImpl } from 'react-hook-form'; import { @@ -9,89 +6,8 @@ import { hasSignUpSettingsChanged, hasSocialTargetsChanged, } from '../components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/utils'; -import { signUpIdentifiersMapping } from '../constants'; import { SignUpIdentifier } from '../types'; -import type { SignInExperienceForm, SignUpForm } from '../types'; - -import { mapIdentifiersToSignUpIdentifier } from './identifier'; - -export const signInExperienceParser = { - toLocalSignUp: (signUp: SignUp): SignUpForm => { - const { identifiers, ...signUpData } = signUp; - - return { - identifier: mapIdentifiersToSignUpIdentifier(identifiers), - ...signUpData, - }; - }, - toRemoteSignUp: (signUpForm: SignUpForm): SignUp => { - const { identifier, ...signUpFormData } = signUpForm; - - return { - identifiers: signUpIdentifiersMapping[identifier], - ...signUpFormData, - }; - }, - toLocalForm: (signInExperience: SignInExperience): SignInExperienceForm => { - const { signUp, signInMode, customCss, branding, passwordPolicy } = signInExperience; - - return { - ...signInExperience, - signUp: signInExperienceParser.toLocalSignUp(signUp), - createAccountEnabled: signInMode !== SignInMode.SignIn, - customCss: customCss ?? undefined, - branding: { - ...branding, - logoUrl: branding.logoUrl ?? '', - darkLogoUrl: branding.darkLogoUrl ?? '', - favicon: branding.favicon ?? '', - }, - /** Parse password policy with default values. */ - passwordPolicy: { - ...passwordPolicyGuard.parse(passwordPolicy), - customWords: passwordPolicy.rejects?.words?.join('\n') ?? '', - isCustomWordsEnabled: Boolean(passwordPolicy.rejects?.words?.length), - }, - }; - }, - toRemoteModel: (setup: SignInExperienceForm): SignInExperience => { - const { - branding, - createAccountEnabled, - signUp, - customCss, - /** Remove the custom words related properties since they are not used in the remote model. */ - passwordPolicy: { isCustomWordsEnabled, customWords, ...passwordPolicy }, - } = setup; - - return { - ...setup, - branding: { - ...branding, - // Transform empty string to undefined - favicon: conditional(branding.favicon?.length && branding.favicon), - logoUrl: conditional(branding.logoUrl?.length && branding.logoUrl), - darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl), - }, - signUp: signUp - ? signInExperienceParser.toRemoteSignUp(signUp) - : { - identifiers: [SignInIdentifier.Username], - password: true, - verify: false, - }, - signInMode: createAccountEnabled ? SignInMode.SignInAndRegister : SignInMode.SignIn, - customCss: customCss?.length ? customCss : null, - passwordPolicy: { - ...passwordPolicy, - rejects: { - ...passwordPolicy.rejects, - words: isCustomWordsEnabled ? customWords.split('\n').filter(Boolean) : [], - }, - }, - }; - }, -}; +import type { SignInExperienceForm } from '../types'; export const hasSignUpAndSignInConfigChanged = ( before: SignInExperience, diff --git a/packages/console/src/pages/SignInExperience/utils/parser.ts b/packages/console/src/pages/SignInExperience/utils/parser.ts new file mode 100644 index 000000000..30e16e740 --- /dev/null +++ b/packages/console/src/pages/SignInExperience/utils/parser.ts @@ -0,0 +1,97 @@ +import { passwordPolicyGuard } from '@logto/core-kit'; +import { SignInMode, type SignInExperience, type SignUp, SignInIdentifier } from '@logto/schemas'; +import { conditional } from '@silverhand/essentials'; + +import { signUpIdentifiersMapping } from '../constants'; +import { + type UpdateSignInExperienceData, + type SignInExperienceForm, + type SignUpForm, +} from '../types'; + +import { mapIdentifiersToSignUpIdentifier } from './identifier'; + +export const signUpFormDataParser = { + fromSignUp: (data: SignUp): SignUpForm => { + const { identifiers, ...signUpData } = data; + + return { + identifier: mapIdentifiersToSignUpIdentifier(identifiers), + ...signUpData, + }; + }, + toSignUp: (formData: SignUpForm): SignUp => { + const { identifier, ...signUpFormData } = formData; + + return { + identifiers: signUpIdentifiersMapping[identifier], + ...signUpFormData, + }; + }, +}; + +export const sieFormDataParser = { + fromSignInExperience: (data: SignInExperience): SignInExperienceForm => { + const { signUp, signInMode, customCss, branding, passwordPolicy } = data; + + return { + ...data, + signUp: signUpFormDataParser.fromSignUp(signUp), + createAccountEnabled: signInMode !== SignInMode.SignIn, + customCss: customCss ?? undefined, + branding: { + ...branding, + logoUrl: branding.logoUrl ?? '', + darkLogoUrl: branding.darkLogoUrl ?? '', + favicon: branding.favicon ?? '', + }, + /** Parse password policy with default values. */ + passwordPolicy: { + ...passwordPolicyGuard.parse(passwordPolicy), + customWords: passwordPolicy.rejects?.words?.join('\n') ?? '', + isCustomWordsEnabled: Boolean(passwordPolicy.rejects?.words?.length), + }, + }; + }, + toSignInExperience: (formData: SignInExperienceForm): SignInExperience => { + const { + branding, + createAccountEnabled, + signUp, + customCss, + /** Remove the custom words related properties since they are not used in the remote model. */ + passwordPolicy: { isCustomWordsEnabled, customWords, ...passwordPolicy }, + } = formData; + + return { + ...formData, + branding: { + ...branding, + // Transform empty string to undefined + favicon: conditional(branding.favicon?.length && branding.favicon), + logoUrl: conditional(branding.logoUrl?.length && branding.logoUrl), + darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl), + }, + signUp: signUp + ? signUpFormDataParser.toSignUp(signUp) + : { + identifiers: [SignInIdentifier.Username], + password: true, + verify: false, + }, + signInMode: createAccountEnabled ? SignInMode.SignInAndRegister : SignInMode.SignIn, + customCss: customCss?.length ? customCss : null, + passwordPolicy: { + ...passwordPolicy, + rejects: { + ...passwordPolicy.rejects, + words: isCustomWordsEnabled ? customWords.split('\n').filter(Boolean) : [], + }, + }, + }; + }, + toUpdateSignInExperienceData: (formData: SignInExperienceForm): UpdateSignInExperienceData => ({ + ...sieFormDataParser.toSignInExperience(formData), + mfa: undefined, + }), +};