diff --git a/packages/console/src/pages/UserDetails/components/UserLogs.tsx b/packages/console/src/pages/UserDetails/components/UserLogs.tsx new file mode 100644 index 000000000..1963bf643 --- /dev/null +++ b/packages/console/src/pages/UserDetails/components/UserLogs.tsx @@ -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 ( +
+ +
+ ); +}; + +export default UserLogs; diff --git a/packages/console/src/pages/UserDetails/components/UserSettings.tsx b/packages/console/src/pages/UserDetails/components/UserSettings.tsx new file mode 100644 index 000000000..3ebe11b7e --- /dev/null +++ b/packages/console/src/pages/UserDetails/components/UserSettings.tsx @@ -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; + primaryPhone: Nullable; + username: Nullable; + name: Nullable; + avatar: Nullable; + 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(); + + 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 = { + name, + avatar, + roleNames, + customData, + }; + + const updatedUser = await api + .patch(`/api/users/${userData.id}`, { json: payload }) + .json(); + onUserUpdated(updatedUser); + toast.success(t('general.saved')); + }); + + return ( +
+
+ {getValues('primaryEmail') && ( + + + + )} + {getValues('primaryPhone') && ( + + + + )} + {getValues('username') && ( + + + + )} + + + + + !value || uriValidator(value) || t('errors.invalid_uri_format'), + })} + hasError={Boolean(errors.avatar)} + errorMessage={errors.avatar?.message} + placeholder={t('user_details.field_avatar_placeholder')} + /> + + + { + onUserUpdated(); + }} + /> + + + + +
+
+
+
+
+ + + ); +}; + +export default UserSettings; diff --git a/packages/console/src/pages/UserDetails/index.tsx b/packages/console/src/pages/UserDetails/index.tsx index bd3125dea..b6971c9d9 100644 --- a/packages/console/src/pages/UserDetails/index.tsx +++ b/packages/console/src/pages/UserDetails/index.tsx @@ -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; - primaryPhone: Nullable; - username: Nullable; - name: Nullable; - avatar: Nullable; - 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(userId && `/api/users/${userId}`); const isLoading = !data && !error; - const { - handleSubmit, - register, - control, - reset, - formState: { isSubmitting, errors }, - getValues, - } = useForm(); - - 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 = { - name, - avatar, - roleNames, - customData, }; - - const updatedUser = await api.patch(`/api/users/${data.id}`, { json: payload }).json(); - void mutate(updatedUser); - toast.success(t('general.saved')); - }); + }, [data]); return (
@@ -202,89 +143,15 @@ const UserDetails = () => { {t('general.settings_nav')} {t('user_details.tab_logs')} - {isLogs ? ( -
- -
- ) : ( -
-
- {getValues('primaryEmail') && ( - - - - )} - {getValues('primaryPhone') && ( - - - - )} - {getValues('username') && ( - - - - )} - - - - - - !value || uriValidator(value) || t('errors.invalid_uri_format'), - })} - hasError={Boolean(errors.avatar)} - errorMessage={errors.avatar?.message} - placeholder={t('user_details.field_avatar_placeholder')} - /> - - - { - void mutate(); - }} - /> - - - - -
-
-
-
-
-
+ {isLogs && } + {!isLogs && userFormData && ( + { + void mutate(user); + }} + /> )}