0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-24 22:41:28 -05:00

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

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

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

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...',
redirecting: 'Redirecting...',
added: 'Added',
cancel: 'Cancel',
confirm: 'Confirm',
},
main_flow: {
input: {
@ -423,6 +425,13 @@ const translation = {
no_connector:
'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: {
title: 'Settings',

View file

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