0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

feat(console): user settings unsaved changes alert (#1411)

This commit is contained in:
Xiao Yijun 2022-07-06 22:31:18 +08:00 committed by GitHub
parent 4d7a091d24
commit 14b27b6d0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 188 additions and 150 deletions

View file

@ -0,0 +1,19 @@
import React from 'react';
import AuditLogTable from '@/components/AuditLogTable';
import * as styles from '../index.module.scss';
type Props = {
userId: string;
};
const UserLogs = ({ userId }: Props) => {
return (
<div className={styles.logs}>
<AuditLogTable userId={userId} />
</div>
);
};
export default UserLogs;

View file

@ -0,0 +1,152 @@
import { User } from '@logto/schemas';
import { Nullable } from '@silverhand/essentials';
import React, { useEffect } from 'react';
import { useForm, useController } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Button from '@/components/Button';
import CodeEditor from '@/components/CodeEditor';
import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import useApi from '@/hooks/use-api';
import * as detailsStyles from '@/scss/details.module.scss';
import { safeParseJson } from '@/utilities/json';
import { uriValidator } from '@/utilities/validator';
import * as styles from '../index.module.scss';
import UserConnectors from './UserConnectors';
type FormData = {
primaryEmail: Nullable<string>;
primaryPhone: Nullable<string>;
username: Nullable<string>;
name: Nullable<string>;
avatar: Nullable<string>;
roleNames: string[];
customData: string;
};
type Props = {
userData: User;
userFormData: FormData;
onUserUpdated: (user?: User) => void;
};
const UserSettings = ({ userData, userFormData, onUserUpdated }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
handleSubmit,
register,
control,
reset,
formState: { isSubmitting, errors, isDirty },
getValues,
} = useForm<FormData>();
const {
field: { onChange, value },
} = useController({ name: 'customData', control });
const api = useApi();
useEffect(() => {
reset(userFormData);
}, [reset, userFormData]);
const onSubmit = handleSubmit(async (formData) => {
if (isSubmitting) {
return;
}
const { customData: inputtedCustomData, name, avatar, roleNames } = formData;
const customData = inputtedCustomData ? safeParseJson(inputtedCustomData) : {};
if (!customData) {
toast.error(t('user_details.custom_data_invalid'));
return;
}
const payload: Partial<User> = {
name,
avatar,
roleNames,
customData,
};
const updatedUser = await api
.patch(`/api/users/${userData.id}`, { json: payload })
.json<User>();
onUserUpdated(updatedUser);
toast.success(t('general.saved'));
});
return (
<form className={styles.form} onSubmit={onSubmit}>
<div className={styles.fields}>
{getValues('primaryEmail') && (
<FormField title="admin_console.user_details.field_email" className={styles.textField}>
<TextInput readOnly {...register('primaryEmail')} />
</FormField>
)}
{getValues('primaryPhone') && (
<FormField title="admin_console.user_details.field_phone" className={styles.textField}>
<TextInput readOnly {...register('primaryPhone')} />
</FormField>
)}
{getValues('username') && (
<FormField title="admin_console.user_details.field_username" className={styles.textField}>
<TextInput readOnly {...register('username')} />
</FormField>
)}
<FormField title="admin_console.user_details.field_name" className={styles.textField}>
<TextInput {...register('name')} />
</FormField>
<FormField title="admin_console.user_details.field_avatar" className={styles.textField}>
<TextInput
{...register('avatar', {
validate: (value) => !value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
hasError={Boolean(errors.avatar)}
errorMessage={errors.avatar?.message}
placeholder={t('user_details.field_avatar_placeholder')}
/>
</FormField>
<FormField title="admin_console.user_details.field_connectors" className={styles.textField}>
<UserConnectors
userId={userData.id}
connectors={userData.identities}
onDelete={() => {
onUserUpdated();
}}
/>
</FormField>
<FormField
isRequired
title="admin_console.user_details.field_custom_data"
className={styles.textField}
>
<CodeEditor language="json" value={value} onChange={onChange} />
</FormField>
</div>
<div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
htmlType="submit"
type="primary"
title="admin_console.general.save_changes"
size="large"
/>
</div>
</div>
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
</form>
);
};
export default UserSettings;

View file

@ -1,52 +1,33 @@
import { User } from '@logto/schemas';
import { Nullable } from '@silverhand/essentials';
import classNames from 'classnames';
import React, { useEffect, useState } from 'react';
import { useController, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import useSWR from 'swr';
import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
import AuditLogTable from '@/components/AuditLogTable';
import Button from '@/components/Button';
import Card from '@/components/Card';
import CodeEditor from '@/components/CodeEditor';
import CopyToClipboard from '@/components/CopyToClipboard';
import DetailsSkeleton from '@/components/DetailsSkeleton';
import FormField from '@/components/FormField';
import LinkButton from '@/components/LinkButton';
import TabNav, { TabNavItem } from '@/components/TabNav';
import TextInput from '@/components/TextInput';
import { generateAvatarPlaceHolderById } from '@/consts/avatars';
import useApi, { RequestError } from '@/hooks/use-api';
import { RequestError } from '@/hooks/use-api';
import Back from '@/icons/Back';
import Delete from '@/icons/Delete';
import More from '@/icons/More';
import Reset from '@/icons/Reset';
import * as detailsStyles from '@/scss/details.module.scss';
import * as modalStyles from '@/scss/modal.module.scss';
import { safeParseJson } from '@/utilities/json';
import { uriValidator } from '@/utilities/validator';
import CreateSuccess from './components/CreateSuccess';
import DeleteForm from './components/DeleteForm';
import ResetPasswordForm from './components/ResetPasswordForm';
import UserConnectors from './components/UserConnectors';
import UserLogs from './components/UserLogs';
import UserSettings from './components/UserSettings';
import * as styles from './index.module.scss';
type FormData = {
primaryEmail: Nullable<string>;
primaryPhone: Nullable<string>;
username: Nullable<string>;
name: Nullable<string>;
avatar: Nullable<string>;
roleNames: string[];
customData: string;
};
const UserDetails = () => {
const location = useLocation();
const isLogs = location.pathname.endsWith('/logs');
@ -62,56 +43,16 @@ const UserDetails = () => {
const { data, error, mutate } = useSWR<User, RequestError>(userId && `/api/users/${userId}`);
const isLoading = !data && !error;
const {
handleSubmit,
register,
control,
reset,
formState: { isSubmitting, errors },
getValues,
} = useForm<FormData>();
const {
field: { onChange, value },
} = useController({ name: 'customData', control });
const api = useApi();
useEffect(() => {
const userFormData = useMemo(() => {
if (!data) {
return;
}
reset({
return {
...data,
customData: JSON.stringify(data.customData, null, 2),
});
}, [data, reset]);
const onSubmit = handleSubmit(async (formData) => {
if (!data || isSubmitting) {
return;
}
const { customData: inputtedCustomData, name, avatar, roleNames } = formData;
const customData = inputtedCustomData ? safeParseJson(inputtedCustomData) : {};
if (!customData) {
toast.error(t('user_details.custom_data_invalid'));
return;
}
const payload: Partial<User> = {
name,
avatar,
roleNames,
customData,
};
const updatedUser = await api.patch(`/api/users/${data.id}`, { json: payload }).json<User>();
void mutate(updatedUser);
toast.success(t('general.saved'));
});
}, [data]);
return (
<div className={detailsStyles.container}>
@ -202,89 +143,15 @@ const UserDetails = () => {
<TabNavItem href={`/users/${userId}`}>{t('general.settings_nav')}</TabNavItem>
<TabNavItem href={`/users/${userId}/logs`}>{t('user_details.tab_logs')}</TabNavItem>
</TabNav>
{isLogs ? (
<div className={styles.logs}>
<AuditLogTable userId={data.id} />
</div>
) : (
<form className={styles.form} onSubmit={onSubmit}>
<div className={styles.fields}>
{getValues('primaryEmail') && (
<FormField
title="admin_console.user_details.field_email"
className={styles.textField}
>
<TextInput readOnly {...register('primaryEmail')} />
</FormField>
)}
{getValues('primaryPhone') && (
<FormField
title="admin_console.user_details.field_phone"
className={styles.textField}
>
<TextInput readOnly {...register('primaryPhone')} />
</FormField>
)}
{getValues('username') && (
<FormField
title="admin_console.user_details.field_username"
className={styles.textField}
>
<TextInput readOnly {...register('username')} />
</FormField>
)}
<FormField
title="admin_console.user_details.field_name"
className={styles.textField}
>
<TextInput {...register('name')} />
</FormField>
<FormField
title="admin_console.user_details.field_avatar"
className={styles.textField}
>
<TextInput
{...register('avatar', {
validate: (value) =>
!value || uriValidator(value) || t('errors.invalid_uri_format'),
})}
hasError={Boolean(errors.avatar)}
errorMessage={errors.avatar?.message}
placeholder={t('user_details.field_avatar_placeholder')}
/>
</FormField>
<FormField
title="admin_console.user_details.field_connectors"
className={styles.textField}
>
<UserConnectors
userId={data.id}
connectors={data.identities}
onDelete={() => {
void mutate();
}}
/>
</FormField>
<FormField
isRequired
title="admin_console.user_details.field_custom_data"
className={styles.textField}
>
<CodeEditor language="json" value={value} onChange={onChange} />
</FormField>
</div>
<div className={detailsStyles.footer}>
<div className={detailsStyles.footerMain}>
<Button
isLoading={isSubmitting}
htmlType="submit"
type="primary"
title="admin_console.general.save_changes"
size="large"
/>
</div>
</div>
</form>
{isLogs && <UserLogs userId={data.id} />}
{!isLogs && userFormData && (
<UserSettings
userData={data}
userFormData={userFormData}
onUserUpdated={(user) => {
void mutate(user);
}}
/>
)}
</Card>
</>