0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(console): refactor sie form data parser (#5195)

This commit is contained in:
Xiao Yijun 2024-01-04 11:15:34 +08:00 committed by GitHub
parent 8edbc0b4a8
commit 0d1a0a9746
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 105 deletions

View file

@ -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;

View file

@ -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 }),
]);

View file

@ -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,
});

View file

@ -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<SignInExperienceType>();
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)) {

View file

@ -43,3 +43,11 @@ export type SignInMethodsObject = Record<
SignInIdentifier,
{ password: boolean; verificationCode: boolean }
>;
export type UpdateSignInExperienceData = Omit<SignInExperience, 'mfa'> & {
/**
* `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;
};

View file

@ -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,

View file

@ -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,
}),
};