0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor(console): refactor modals with modallayout (#435)

This commit is contained in:
Xiao Yijun 2022-03-22 17:40:06 +08:00 committed by GitHub
parent 2e08ec9db0
commit 4f41162ac3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 211 additions and 226 deletions

View file

@ -30,5 +30,14 @@
"_.unit(${1:2})"
],
"description": "Use underscore unit function."
},
"Use dimensions": {
"scope": "scss",
"prefix": "used",
"body": [
"@use '@/scss/dimensions' as dim;",
"$0"
],
"description": "Add @use dimensions in header."
}
}

View file

@ -9,6 +9,10 @@
margin-left: _.unit(4);
}
.icon {
flex-shrink: 0;
}
.title {
font: var(--font-body);
color: var(--color-primary);

View file

@ -13,7 +13,7 @@ type Props = {
const ItemPreview = ({ title, subtitle, icon, to }: Props) => {
return (
<div className={styles.item}>
{icon}
{icon && <div className={styles.icon}>{icon}</div>}
<div>
{to && (
<Link

View file

@ -1,9 +1,10 @@
@use '@/scss/dimensions' as dim;
@use '@/scss/underscore' as _;
.container {
display: flex;
flex-direction: column;
max-width: 800px;
max-width: dim.$modal-layout-max-width;
padding: _.unit(8);
.header {

View file

@ -1,25 +1,10 @@
@use '@/scss/underscore' as _;
.card {
padding: _.unit(8);
max-width: 800px;
}
.headline {
display: flex;
justify-content: space-between;
> *:not(:first-child) {
margin-left: _.unit(3);
}
> svg {
cursor: pointer;
}
}
@use '@/scss/dimensions' as dim;
.form {
margin-top: _.unit(8);
padding: _.unit(8) 0;
display: flex;
align-items: flex-end;
}
.textField {
@ -27,6 +12,5 @@
}
.submit {
margin-top: _.unit(8);
text-align: right;
padding-left: dim.$form-text-field-width;
}

View file

@ -1,15 +1,12 @@
import { Resource } from '@logto/schemas';
import React from 'react';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './index.module.scss';
@ -24,26 +21,43 @@ type Props = {
const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>();
const [loading, setLoading] = useState(false);
const api = useApi();
const onSubmit = handleSubmit(async (data) => {
if (loading) {
return;
}
setLoading(true);
try {
const createdApiResource = await api.post('/api/resources', { json: data }).json<Resource>();
onClose?.(createdApiResource);
} catch (error: unknown) {
console.error(error);
} finally {
setLoading(false);
}
});
return (
<Card className={styles.card}>
<div className={styles.headline}>
<CardTitle title="api_resources.create" subtitle="api_resources.subtitle" />
<IconButton size="large" onClick={() => onClose?.()}>
<Close />
</IconButton>
<ModalLayout
title="api_resources.create"
subtitle="api_resources.subtitle"
footer={
<div className={styles.submit}>
<Button
disabled={loading}
htmlType="submit"
title="admin_console.api_resources.create"
size="large"
type="primary"
onClick={onSubmit}
/>
</div>
<form className={styles.form} onSubmit={onSubmit}>
}
onClose={onClose}
>
<form className={styles.form}>
<FormField
isRequired
title="admin_console.api_resources.api_name"
@ -58,16 +72,8 @@ const CreateForm = ({ onClose }: Props) => {
>
<TextInput {...register('indicator', { required: true })} />
</FormField>
<div className={styles.submit}>
<Button
htmlType="submit"
title="admin_console.api_resources.create"
size="large"
type="primary"
/>
</div>
</form>
</Card>
</ModalLayout>
);
};

View file

@ -1,6 +1,7 @@
import { Resource } from '@logto/schemas';
import { conditional } from '@silverhand/essentials/lib/utilities/conditional.js';
import React, { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
import { useNavigate } from 'react-router-dom';
@ -49,6 +50,9 @@ const ApiResources = () => {
if (createdApiResource) {
void mutate(conditional(data && [...data, createdApiResource]));
toast.success(
t('api_resources.api_resource_created', { name: createdApiResource.name })
);
navigate(buildDetailsLink(createdApiResource.id));
}
}}

View file

@ -1,24 +1,9 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
}
.headline {
display: flex;
justify-content: space-between;
> *:not(:first-child) {
margin-left: _.unit(3);
}
> svg {
cursor: pointer;
}
}
.form {
margin-top: _.unit(8);
padding: _.unit(8) 0;
}
.textField {
@ -26,8 +11,7 @@
}
.submit {
margin-top: _.unit(8);
text-align: right;
padding-left: dim.$form-text-field-width;
}
.error {

View file

@ -6,14 +6,11 @@ import Modal from 'react-modal';
import useSWR from 'swr';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import RadioGroup, { Radio } from '@/components/RadioGroup';
import TextInput from '@/components/TextInput';
import useApi, { RequestError } from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as modalStyles from '@/scss/modal.module.scss';
import { applicationTypeI18nKey } from '@/types/applications';
@ -46,6 +43,7 @@ const CreateForm = ({ onClose }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: setting } = useSWR<Setting, RequestError>('/api/settings');
const api = useApi();
const [loading, setLoading] = useState(false);
const isGetStartedSkipped = setting?.adminConsole.applicationSkipGetStarted;
@ -55,6 +53,12 @@ const CreateForm = ({ onClose }: Props) => {
};
const onSubmit = handleSubmit(async (data) => {
if (loading) {
return;
}
setLoading(true);
try {
const createdApp = await api.post('/api/applications', { json: data }).json<Application>();
setCreatedApp(createdApp);
@ -64,20 +68,30 @@ const CreateForm = ({ onClose }: Props) => {
} else {
setIsQuickStartGuideOpen(true);
}
} catch (error: unknown) {
console.error(error);
} finally {
setLoading(false);
}
});
return (
<Card className={styles.card}>
<div className={styles.headline}>
<CardTitle title="applications.create" subtitle="applications.subtitle" />
<IconButton size="large" onClick={() => onClose?.()}>
<Close />
</IconButton>
<ModalLayout
title="applications.create"
subtitle="applications.subtitle"
footer={
<div className={styles.submit}>
<Button
disabled={loading}
htmlType="submit"
title="admin_console.applications.create"
size="large"
type="primary"
onClick={onSubmit}
/>
</div>
<form className={styles.form} onSubmit={onSubmit}>
}
onClose={onClose}
>
<form className={styles.form}>
<FormField title="admin_console.applications.select_application_type">
<RadioGroup ref={ref} name={name} value={value} onChange={onChange}>
{Object.values(ApplicationType).map((value) => (
@ -106,21 +120,13 @@ const CreateForm = ({ onClose }: Props) => {
>
<TextInput {...register('description')} />
</FormField>
<div className={styles.submit}>
<Button
htmlType="submit"
title="admin_console.applications.create"
size="large"
type="primary"
/>
</div>
</form>
{!isGetStartedSkipped && createdApp && (
<Modal isOpen={isQuickStartGuideOpen} className={modalStyles.fullScreen}>
<GetStarted appName={createdApp.name} onClose={closeModal} />
</Modal>
)}
</Card>
</ModalLayout>
);
};

View file

@ -1,15 +1,10 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
min-width: _.unit(100);
}
.header h1 {
font: var(--font-title-large);
margin-top: 0;
}
.body {
.content {
padding: _.unit(8) 0;
min-width: dim.$modal-layout-min-width;
font: var(--font-body-2);
.info {
@ -39,9 +34,6 @@
}
.footer {
border-top: 1px solid var(--color-neutral-80);
margin-top: _.unit(6);
padding-top: _.unit(6);
display: flex;
justify-content: right;

View file

@ -5,8 +5,8 @@ import ReactModal from 'react-modal';
import { useSearchParams } from 'react-router-dom';
import Button from '@/components/Button';
import Card from '@/components/Card';
import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import Eye from '@/icons/Eye';
import * as modalStyles from '@/scss/modal.module.scss';
@ -44,11 +44,20 @@ const CreateSuccess = ({ username }: Props) => {
return (
<ReactModal isOpen className={modalStyles.content} overlayClassName={modalStyles.overlay}>
<Card className={styles.card}>
<div className={styles.header}>
<h1>{t('user_details.created_title')}</h1>
<ModalLayout
title="user_details.created_title"
footer={
<div className={styles.footer}>
<Button title="admin_console.user_details.created_button_close" onClick={handleClose} />
<Button
type="primary"
title="admin_console.user_details.created_button_copy"
onClick={handleCopy}
/>
</div>
<div className={styles.body}>
}
>
<div className={styles.content}>
<div>{t('user_details.created_guide')}</div>
<div className={styles.info}>
<div className={styles.infoLine}>
@ -72,15 +81,7 @@ const CreateSuccess = ({ username }: Props) => {
</div>
</div>
</div>
<div className={styles.footer}>
<Button title="admin_console.user_details.created_button_close" onClick={handleClose} />
<Button
type="primary"
title="admin_console.user_details.created_button_copy"
onClick={handleCopy}
/>
</div>
</Card>
</ModalLayout>
</ReactModal>
);
};

View file

@ -1,29 +1,17 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
min-width: 400px;
> :not(:first-child) {
margin-top: _.unit(6);
}
.content {
min-width: dim.$modal-layout-min-width;
padding: _.unit(8) 0;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.footer {
border-top: 1px solid var(--color-neutral-80);
margin-top: _.unit(6);
padding-top: _.unit(6);
.footer {
display: flex;
justify-content: right;
button:not(:last-child) {
margin-right: _.unit(2);
}
}
}

View file

@ -4,11 +4,8 @@ import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './DeleteForm.module.scss';
@ -39,16 +36,15 @@ const DeleteForm = ({ id, onClose }: Props) => {
};
return (
<Card className={styles.card}>
<div className={styles.header}>
<CardTitle title="user_details.delete_title" />
<IconButton size="large" onClick={onClose}>
<Close />
</IconButton>
</div>
<div>{t('user_details.delete_description')}</div>
<ModalLayout
title="user_details.delete_title"
footer={
<div className={styles.footer}>
<Button type="outline" title="admin_console.user_details.delete_cancel" onClick={onClose} />
<Button
type="outline"
title="admin_console.user_details.delete_cancel"
onClick={onClose}
/>
<Button
disabled={loading}
type="danger"
@ -56,7 +52,13 @@ const DeleteForm = ({ id, onClose }: Props) => {
onClick={handleDelete}
/>
</div>
</Card>
}
onClose={onClose}
>
<div className={styles.content}>
<div>{t('user_details.delete_description')}</div>
</div>
</ModalLayout>
);
};

View file

@ -1,15 +1,14 @@
import { User } from '@logto/schemas';
import React from 'react';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './ResetPasswordForm.module.scss';
@ -23,22 +22,28 @@ type Props = {
};
const ResetPasswordForm = ({ onClose, userId }: Props) => {
const { t } = useTranslation(undefined, {
keyPrefix: 'admin_console',
});
const { handleSubmit, register } = useForm<FormData>();
const api = useApi();
const [loading, setLoading] = useState(false);
const onSubmit = handleSubmit(async (data) => {
setLoading(true);
try {
await api.patch(`/api/users/${userId}/password`, { json: data }).json<User>();
onClose?.();
toast.success(t('user_details.reset_password.reset_password_success'));
} finally {
setLoading(false);
}
});
return (
<Card className={styles.card}>
<div className={styles.headline}>
<CardTitle title="user_details.reset_password.title" />
<IconButton size="large" onClick={() => onClose?.()}>
<Close />
</IconButton>
</div>
<ModalLayout title="user_details.reset_password.title" onClose={onClose}>
<form className={styles.form} onSubmit={onSubmit}>
<FormField
isRequired
@ -49,6 +54,7 @@ const ResetPasswordForm = ({ onClose, userId }: Props) => {
</FormField>
<div className={styles.submit}>
<Button
disabled={loading}
htmlType="submit"
title="admin_console.user_details.reset_password.reset_password"
size="large"
@ -56,7 +62,7 @@ const ResetPasswordForm = ({ onClose, userId }: Props) => {
/>
</div>
</form>
</Card>
</ModalLayout>
);
};

View file

@ -1,24 +1,9 @@
@use '@/scss/underscore' as _;
@use '@/scss/dimensions' as dim;
.card {
padding: _.unit(8);
}
.headline {
display: flex;
justify-content: space-between;
> *:not(:first-child) {
margin-left: _.unit(3);
}
> svg {
cursor: pointer;
}
}
.form {
margin-top: _.unit(8);
padding: _.unit(8) 0;
}
.textField {
@ -26,12 +11,5 @@
}
.submit {
margin-top: _.unit(8);
text-align: right;
}
.error {
font: var(--font-body-2);
color: var(--color-error);
margin-top: _.unit(2);
padding-left: dim.$form-text-field-width;
}

View file

@ -1,15 +1,12 @@
import { User } from '@logto/schemas';
import React from 'react';
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CardTitle from '@/components/CardTitle';
import FormField from '@/components/FormField';
import IconButton from '@/components/IconButton';
import ModalLayout from '@/components/ModalLayout';
import TextInput from '@/components/TextInput';
import useApi from '@/hooks/use-api';
import Close from '@/icons/Close';
import * as styles from './index.module.scss';
@ -27,20 +24,42 @@ const CreateForm = ({ onClose }: Props) => {
const { handleSubmit, register } = useForm<FormData>();
const api = useApi();
const [loading, setLoading] = useState(false);
const onSubmit = handleSubmit(async (data) => {
if (loading) {
return;
}
setLoading(true);
try {
const createdUser = await api.post('/api/users', { json: data }).json<User>();
onClose?.(createdUser, btoa(data.password));
} finally {
setLoading(false);
}
});
return (
<Card className={styles.card}>
<div className={styles.headline}>
<CardTitle title="users.create" subtitle="users.subtitle" />
<IconButton size="large" onClick={() => onClose?.()}>
<Close />
</IconButton>
<ModalLayout
title="users.create"
subtitle="users.subtitle"
footer={
<div className={styles.submit}>
<Button
disabled={loading}
htmlType="submit"
title="admin_console.users.create"
size="large"
type="primary"
onClick={onSubmit}
/>
</div>
<form className={styles.form} onSubmit={onSubmit}>
}
onClose={onClose}
>
<form className={styles.form}>
<FormField
isRequired
title="admin_console.users.create_form_username"
@ -62,16 +81,8 @@ const CreateForm = ({ onClose }: Props) => {
>
<TextInput {...register('password', { required: true })} />
</FormField>
<div className={styles.submit}>
<Button
htmlType="submit"
title="admin_console.users.create"
size="large"
type="primary"
/>
</div>
</form>
</Card>
</ModalLayout>
);
};

View file

@ -0,0 +1,3 @@
$modal-layout-max-width: 800px;
$modal-layout-min-width: 400px;
$form-text-field-width: 556px;

View file

@ -1,3 +1,5 @@
@use '@/scss/dimensions' as dim;
@function unit($factor: 1, $unit: 'px') {
@return #{$factor * 4}#{$unit};
}
@ -16,7 +18,7 @@
}
@mixin form-text-field {
width: 556px;
width: dim.$form-text-field-width;
}
@mixin vertical-bar {

View file

@ -135,6 +135,7 @@ const translation = {
create: 'Create API Resource',
api_name: 'API Name',
api_identifier: 'API Identifier',
api_resource_created: 'The API resource {{name}} has been successfully created!',
},
api_resource_details: {
back_to_api_resources: 'Back to my API resources',
@ -222,6 +223,7 @@ const translation = {
title: 'Reset Password',
label: 'New password:',
reset_password: 'Reset password',
reset_password_success: 'Reset password successfully.',
},
tab_settings: 'Settings',
tab_logs: 'User Logs',

View file

@ -135,6 +135,7 @@ const translation = {
create: 'Create API Resource',
api_name: 'API Name',
api_identifier: 'API Identifier',
api_resource_created: 'The API resource {{name}} has been successfully created!',
},
api_resource_details: {
back_to_api_resources: 'Back to my API resources',
@ -221,6 +222,7 @@ const translation = {
title: '重置密码',
label: '新密码:',
reset_password: '重置密码',
reset_password_success: '密码已成功重置。',
},
tab_settings: '设置',
tab_logs: '用户日志',