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

feat(console): sign in methods change alert ()

This commit is contained in:
Wang Sijie 2022-05-07 16:32:35 +08:00 committed by GitHub
parent d815d96f1f
commit a1ceea0685
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 37 deletions
packages
console/src/pages/SignInExperience
phrases/src/locales

View file

@ -0,0 +1,28 @@
@use '@/scss/underscore' as _;
.description {
font: var(--font-body-medium);
}
.content {
margin-top: _.unit(6);
border-radius: 8px;
padding: _.unit(5);
background: var(--color-layer-2);
font: var(--font-body-medium);
.section {
&:not(:first-child) {
margin-top: _.unit(3);
}
.title {
font: var(--font-subhead-2);
margin: _.unit(1) 0;
}
.connector {
margin-left: _.unit(1);
}
}
}

View file

@ -0,0 +1,54 @@
import { SignInExperience } from '@logto/schemas';
import React from 'react';
import { useTranslation } from 'react-i18next';
import Button from '@/components/Button';
import ModalLayout from '@/components/ModalLayout';
import * as styles from './SaveAlert.module.scss';
import SignInMethodsPreview from './SignInMethodsPreview';
type Props = {
before: SignInExperience;
after: SignInExperience;
onClose: () => void;
onConfirm: () => void;
};
const SaveAlert = ({ before, after, onClose, onConfirm }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<ModalLayout
title="sign_in_exp.save_alert.title"
footer={
<>
<Button type="outline" title="general.cancel" onClick={onClose} />
<Button
type="danger"
title="general.confirm"
onClick={() => {
onClose();
onConfirm();
}}
/>
</>
}
onClose={onClose}
>
<div className={styles.description}>{t('sign_in_exp.save_alert.description')}</div>
<div className={styles.content}>
<div className={styles.section}>
<div className={styles.title}>{t('sign_in_exp.save_alert.before')}</div>
<SignInMethodsPreview data={before} />
</div>
<div className={styles.section}>
<div className={styles.title}>{t('sign_in_exp.save_alert.after')}</div>
<SignInMethodsPreview data={after} />
</div>
</div>
</ModalLayout>
);
};
export default SaveAlert;

View file

@ -0,0 +1,59 @@
import { ConnectorDTO, SignInExperience, SignInMethodKey, SignInMethodState } from '@logto/schemas';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import UnnamedTrans from '@/components/UnnamedTrans';
import { RequestError } from '@/hooks/use-api';
import * as styles from './SaveAlert.module.scss';
type Props = {
data: SignInExperience;
};
const SignInMethodsPreview = ({ data }: Props) => {
const { data: connectors, error } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');
const { signInMethods, socialSignInConnectorIds } = data;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const connectorNames = useMemo(() => {
if (!connectors) {
return null;
}
return socialSignInConnectorIds.map((connectorId) => {
const connector = connectors.find(({ id }) => id === connectorId);
if (!connector) {
return null;
}
return (
<UnnamedTrans
key={connectorId}
className={styles.connector}
resource={connector.metadata.name}
/>
);
});
}, [connectors, socialSignInConnectorIds]);
return (
<div>
{!connectors && !error && <div>loading</div>}
{error && <div>{error.body.message}</div>}
{connectors &&
Object.values(SignInMethodKey)
.filter((key) => signInMethods[key] !== SignInMethodState.Disabled)
.map((key) => (
<div key={key}>
{t('sign_in_exp.sign_in_methods.methods', { context: key })}
{key === SignInMethodKey.Social && <span>: {connectorNames}</span>}
</div>
))}
</div>
);
};
export default SignInMethodsPreview;

View file

@ -1,9 +1,10 @@
import { Setting, SignInExperience as SignInExperienceType } from '@logto/schemas'; import { Setting, SignInExperience as SignInExperienceType } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useEffect } from 'react'; import React, { useEffect, 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';
import ReactModal from 'react-modal';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import useSWR from 'swr'; import useSWR from 'swr';
@ -13,25 +14,29 @@ import CardTitle from '@/components/CardTitle';
import TabNav, { TabNavLink } from '@/components/TabNav'; import TabNav, { TabNavLink } from '@/components/TabNav';
import useApi, { RequestError } from '@/hooks/use-api'; import useApi, { RequestError } from '@/hooks/use-api';
import * as detailsStyles from '@/scss/details.module.scss'; import * as detailsStyles from '@/scss/details.module.scss';
import * as modalStyles from '@/scss/modal.module.scss';
import BrandingForm from './components/BrandingForm'; import BrandingForm from './components/BrandingForm';
import LanguagesForm from './components/LanguagesForm'; import LanguagesForm from './components/LanguagesForm';
import SaveAlert from './components/SaveAlert';
import SignInMethodsForm from './components/SignInMethodsForm'; import SignInMethodsForm from './components/SignInMethodsForm';
import TermsForm from './components/TermsForm'; import TermsForm from './components/TermsForm';
import Welcome from './components/Welcome'; import Welcome from './components/Welcome';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
import { SignInExperienceForm } from './types'; import { SignInExperienceForm } from './types';
import { signInExperienceParser } from './utilities'; import { compareSignInMethods, signInExperienceParser } from './utilities';
const SignInExperience = () => { const SignInExperience = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { tab } = useParams(); const { tab } = useParams();
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp'); const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
const { data: settings, error: settingsError } = useSWR<Setting, RequestError>('/api/settings'); const { data: settings, error: settingsError } = useSWR<Setting, RequestError>('/api/settings');
const [dataToCompare, setDataToCompare] = useState<SignInExperienceType>();
const methods = useForm<SignInExperienceForm>(); const methods = useForm<SignInExperienceForm>();
const { const {
reset, reset,
handleSubmit, handleSubmit,
getValues,
formState: { isSubmitting }, formState: { isSubmitting },
} = methods; } = methods;
const api = useApi(); const api = useApi();
@ -42,18 +47,31 @@ const SignInExperience = () => {
} }
}, [data, reset]); }, [data, reset]);
const saveData = async () => {
const updatedData = await api
.patch('/api/sign-in-exp', {
json: signInExperienceParser.toRemoteModel(getValues()),
})
.json<SignInExperienceType>();
void mutate(updatedData);
toast.success(t('application_details.save_success'));
};
const onSubmit = handleSubmit(async (formData) => { const onSubmit = handleSubmit(async (formData) => {
if (!data || isSubmitting) { if (!data || isSubmitting) {
return; return;
} }
const updatedData = await api const formatted = signInExperienceParser.toRemoteModel(formData);
.patch('/api/sign-in-exp', {
json: signInExperienceParser.toRemoteModel(formData), // Sign in methods changed, need to show confirm modal first.
}) if (!compareSignInMethods(data, formatted)) {
.json<SignInExperienceType>(); setDataToCompare(formatted);
void mutate(updatedData);
toast.success(t('application_details.save_success')); return;
}
await saveData();
}); });
if (!settings && !settingsError) { if (!settings && !settingsError) {
@ -88,31 +106,51 @@ const SignInExperience = () => {
{error && <div>{`error occurred: ${error.body.message}`}</div>} {error && <div>{`error occurred: ${error.body.message}`}</div>}
{data && ( {data && (
<FormProvider {...methods}> <FormProvider {...methods}>
<form className={classNames(detailsStyles.body, styles.form)} onSubmit={onSubmit}> <form onSubmit={onSubmit}>
{tab === 'experience' && ( <div className={classNames(detailsStyles.body, styles.form)}>
<> {tab === 'experience' && (
<BrandingForm /> <>
<TermsForm /> <BrandingForm />
</> <TermsForm />
)} </>
{tab === 'methods' && <SignInMethodsForm />} )}
{tab === 'others' && <LanguagesForm />} {tab === 'methods' && <SignInMethodsForm />}
</form> {tab === 'others' && <LanguagesForm />}
<div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
type="primary"
htmlType="submit"
title="general.save_changes"
/>
</div> </div>
</div> <div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
type="primary"
htmlType="submit"
title="general.save_changes"
/>
</div>
</div>
</form>
</FormProvider> </FormProvider>
)} )}
</Card> </Card>
</div> </div>
<Card className={styles.preview}>TODO</Card> <Card className={styles.preview}>TODO</Card>
{data && (
<ReactModal
isOpen={Boolean(dataToCompare)}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
>
{dataToCompare && (
<SaveAlert
before={data}
after={dataToCompare}
onClose={() => {
setDataToCompare(undefined);
}}
onConfirm={saveData}
/>
)}
</ReactModal>
)}
</div> </div>
); );
}; };

View file

@ -76,3 +76,21 @@ export const signInExperienceParser = {
}; };
}, },
}; };
export const compareSignInMethods = (
before: SignInExperience,
after: SignInExperience
): boolean => {
if (before.socialSignInConnectorIds.length !== after.socialSignInConnectorIds.length) {
return false;
}
if (before.socialSignInConnectorIds.some((id) => !after.socialSignInConnectorIds.includes(id))) {
return false;
}
const { signInMethods: beforeMethods } = before;
const { signInMethods: afterMethods } = after;
return Object.values(SignInMethodKey).every((key) => beforeMethods[key] === afterMethods[key]);
};

View file

@ -11,6 +11,8 @@ const translation = {
loading: 'Loading...', loading: 'Loading...',
redirecting: 'Redirecting...', redirecting: 'Redirecting...',
added: 'Added', added: 'Added',
cancel: 'Cancel',
confirm: 'Confirm',
}, },
main_flow: { main_flow: {
input: { input: {
@ -423,6 +425,13 @@ const translation = {
no_connector: no_connector:
'You havent set up any social connectors yet. Your sign in experience wont go live until you finish the settings first. ', 'You havent set up any social connectors yet. Your sign in experience wont go live until you finish the settings first. ',
}, },
save_alert: {
title: 'Reminder',
description:
'You are changing sign in methods from one to another. This will impact some of your users. Are you sure you want to do that?',
before: 'Before',
after: 'After',
},
}, },
settings: { settings: {
title: 'Settings', title: 'Settings',

View file

@ -13,6 +13,8 @@ const translation = {
loading: '读取中...', loading: '读取中...',
redirecting: '页面跳转中...', redirecting: '页面跳转中...',
added: '已添加', added: '已添加',
cancel: '取消',
confirm: '确认',
}, },
main_flow: { main_flow: {
input: { input: {
@ -379,17 +381,17 @@ const translation = {
terms_of_use_placeholder: 'Terms of use url', terms_of_use_placeholder: 'Terms of use url',
}, },
sign_in_methods: { sign_in_methods: {
title: 'SIGN IN METHODS', title: '登录方式',
primary: 'Primary sign in method', primary: '首选登录方式',
enable_secondary: 'Enable secondary sign in', enable_secondary: '启用其他登录方式',
enable_secondary_description: enable_secondary_description:
"Once it's turned on, you app will support more sign in method(s) besides the primary one. ", "Once it's turned on, you app will support more sign in method(s) besides the primary one. ",
methods: 'Sign in method', methods: '登录方式',
methods_sms: 'Phone number sign in', methods_sms: '短信验证码登录',
methods_email: 'Email sign in', methods_email: '邮箱登录',
methods_social: 'Social sign in', methods_social: '社交账号登录',
methods_username: 'Username-with-password sign in', methods_username: '用户名密码登录',
methods_primary_tag: '(Primary)', methods_primary_tag: '(首选)',
define_social_methods: 'Define social sign in methods', define_social_methods: 'Define social sign in methods',
transfer: { transfer: {
title: 'Social connectors', title: 'Social connectors',
@ -418,6 +420,12 @@ const translation = {
setup: '设置', setup: '设置',
no_connector: '你尚未添加社交登录连接器。你需要先完成设置才能启用。', no_connector: '你尚未添加社交登录连接器。你需要先完成设置才能启用。',
}, },
save_alert: {
title: '提示',
description: '你正在修改登录方式,这可能会影响部分用户。是否继续保存修改?',
before: '修改前',
after: '修改后',
},
}, },
settings: { settings: {
title: '设置', title: '设置',