From c6821aab77ec2235ba98848e54359511c485469e Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Tue, 23 May 2023 16:30:45 +0800 Subject: [PATCH] feat(console): add hover tooltip for micro user avatars (#3861) --- .../components/SourceUserItem/index.tsx | 7 +-- .../components/TargetUserItem/index.tsx | 7 +-- .../Tip/TipBubble/index.module.scss | 1 + .../components/UserAvatar/index.module.scss | 28 +++++++++++ .../src/components/UserAvatar/index.tsx | 49 ++++++++++++++++--- .../src/components/UserName/index.module.scss | 15 +++--- .../console/src/components/UserName/index.tsx | 13 ++--- .../src/pages/AuditLogDetails/index.tsx | 2 +- .../src/pages/RoleDetails/RoleUsers/index.tsx | 2 +- .../components/AssignRolesModal/index.tsx | 3 +- .../console/src/pages/UserDetails/index.tsx | 2 +- packages/console/src/pages/Users/index.tsx | 2 +- packages/console/src/utils/user.ts | 11 +++-- 13 files changed, 102 insertions(+), 40 deletions(-) diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx b/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx index 5ebc0e5b3..19b537b9e 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx +++ b/packages/console/src/components/RoleUsersTransfer/components/SourceUserItem/index.tsx @@ -1,5 +1,4 @@ import type { User } from '@logto/schemas'; -import { useTranslation } from 'react-i18next'; import Checkbox from '@/components/Checkbox'; import UserAvatar from '@/components/UserAvatar'; @@ -16,8 +15,6 @@ type Props = { }; function SourceUserItem({ user, isSelected, onSelect }: Props) { - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - return (
- -
{getUserTitle(user) ?? t('users.unnamed')}
+ +
{getUserTitle(user)}
{user.isSuspended && }
); diff --git a/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx b/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx index 446a50a28..7b3cde2e6 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx +++ b/packages/console/src/components/RoleUsersTransfer/components/TargetUserItem/index.tsx @@ -1,5 +1,4 @@ import type { User } from '@logto/schemas'; -import { useTranslation } from 'react-i18next'; import Close from '@/assets/images/close.svg'; import IconButton from '@/components/IconButton'; @@ -15,13 +14,11 @@ type Props = { }; function TargetUserItem({ user, onDelete }: Props) { - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - return (
- -
{getUserTitle(user) ?? t('users.unnamed')}
+ +
{getUserTitle(user)}
{user.isSuspended && }
; + type Props = { className?: string; size?: 'micro' | 'small' | 'medium' | 'large' | 'xlarge'; - user?: Partial>; + user?: Partial; + hasTooltip?: boolean; }; -function UserAvatar({ className, size = 'medium', user }: Props) { +function UserInfoTipContent({ user }: { user: Partial }) { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { name, primaryEmail, primaryPhone, username } = user; + const itemsToDisplay = [ + { label: t('user_details.field_name'), value: name }, + { label: t('user_details.field_email'), value: primaryEmail }, + { + label: t('user_details.field_phone'), + value: conditional(primaryPhone && formatToInternationalPhoneNumber(primaryPhone)), + }, + { label: t('user_details.field_username'), value: username }, + ]; + return ( + <> + {itemsToDisplay + .filter(({ value }) => Boolean(value)) + .map(({ label, value }) => ( +
+ {label}: + {value} +
+ ))} + + ); +} + +function UserAvatar({ className, size = 'medium', user, hasTooltip = false }: Props) { const avatarClassName = classNames(styles.avatar, styles[size]); const wrapperClassName = classNames(styles.wrapper, styles[size], className); const defaultColorPalette = [ @@ -61,11 +93,16 @@ function UserAvatar({ className, size = 'medium', user }: Props) { ); return ( -
-
- {nameToDisplay ? nameToDisplay.charAt(0) : } + )} + > +
+
+ {nameToDisplay ? nameToDisplay.charAt(0) : } +
-
+ ); } diff --git a/packages/console/src/components/UserName/index.module.scss b/packages/console/src/components/UserName/index.module.scss index 1256a043e..bd9461ce4 100644 --- a/packages/console/src/components/UserName/index.module.scss +++ b/packages/console/src/components/UserName/index.module.scss @@ -5,21 +5,18 @@ color: var(--color-text); .title { - display: inline-block; + display: inline-flex; + align-items: center; max-width: 100%; @include _.text-ellipsis; - margin-right: _.unit(1); - } - .id { - display: inline-block; - font: var(--body-small); - color: var(--color-text-secondary); - @include _.text-ellipsis; + span { + margin-left: _.unit(2); + } } .link { - display: inline-block; + display: inline-flex; text-decoration: none; color: var(--color-text-link); } diff --git a/packages/console/src/components/UserName/index.tsx b/packages/console/src/components/UserName/index.tsx index ee9e19be5..77c3fd8e8 100644 --- a/packages/console/src/components/UserName/index.tsx +++ b/packages/console/src/components/UserName/index.tsx @@ -1,10 +1,13 @@ import type { User } from '@logto/schemas'; +import { conditionalString } from '@silverhand/essentials'; import classNames from 'classnames'; -import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import useSWR from 'swr'; import type { RequestError } from '@/hooks/use-api'; +import { getUserTitle } from '@/utils/user'; + +import UserAvatar from '../UserAvatar'; import * as styles from './index.module.scss'; @@ -15,10 +18,8 @@ type Props = { function UserName({ userId, isLink = false }: Props) { const { data, error } = useSWR(`api/users/${userId}`); - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const isLoading = !data && !error; - const name = data?.name ?? t('users.unnamed'); + const name = conditionalString(data && getUserTitle(data)); if (isLoading) { return null; @@ -28,12 +29,12 @@ function UserName({ userId, isLink = false }: Props) {
{isLink ? ( - {name} + + {name} ) : (
{name}
)} -
{userId}
); } diff --git a/packages/console/src/pages/AuditLogDetails/index.tsx b/packages/console/src/pages/AuditLogDetails/index.tsx index 7aa10ab69..d96c3acf5 100644 --- a/packages/console/src/pages/AuditLogDetails/index.tsx +++ b/packages/console/src/pages/AuditLogDetails/index.tsx @@ -43,7 +43,7 @@ function AuditLogDetails() { conditional( userId && t('log_details.back_to', { - name: conditional(userData && getUserTitle(userData)) ?? t('users.unnamed'), + name: getUserTitle(userData), }) ) ?? conditional( diff --git a/packages/console/src/pages/RoleDetails/RoleUsers/index.tsx b/packages/console/src/pages/RoleDetails/RoleUsers/index.tsx index d4ef30110..c074e8b7e 100644 --- a/packages/console/src/pages/RoleDetails/RoleUsers/index.tsx +++ b/packages/console/src/pages/RoleDetails/RoleUsers/index.tsx @@ -98,7 +98,7 @@ function RoleUsers() { return ( } to={`/users/${id}`} diff --git a/packages/console/src/pages/UserDetails/UserRoles/components/AssignRolesModal/index.tsx b/packages/console/src/pages/UserDetails/UserRoles/components/AssignRolesModal/index.tsx index 98e497426..e6110c26c 100644 --- a/packages/console/src/pages/UserDetails/UserRoles/components/AssignRolesModal/index.tsx +++ b/packages/console/src/pages/UserDetails/UserRoles/components/AssignRolesModal/index.tsx @@ -10,6 +10,7 @@ import ModalLayout from '@/components/ModalLayout'; import UserRolesTransfer from '@/components/UserRolesTransfer'; import useApi from '@/hooks/use-api'; import * as modalStyles from '@/scss/modal.module.scss'; +import { getUserTitle } from '@/utils/user'; type Props = { user: User; @@ -19,7 +20,7 @@ type Props = { function AssignRolesModal({ user, onClose }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const userName = user.name ?? t('users.unnamed'); + const userName = getUserTitle(user); const [isSubmitting, setIsSubmitting] = useState(false); const [roles, setRoles] = useState([]); diff --git a/packages/console/src/pages/UserDetails/index.tsx b/packages/console/src/pages/UserDetails/index.tsx index b3cde4362..20215ba37 100644 --- a/packages/console/src/pages/UserDetails/index.tsx +++ b/packages/console/src/pages/UserDetails/index.tsx @@ -114,7 +114,7 @@ function UserDetails() {
-
{getUserTitle(data) ?? t('users.unnamed')}
+
{getUserTitle(data)}
{isSuspendedUser && } {userSubtitle && ( diff --git a/packages/console/src/pages/Users/index.tsx b/packages/console/src/pages/Users/index.tsx index 8c85f31fb..f6d10bffb 100644 --- a/packages/console/src/pages/Users/index.tsx +++ b/packages/console/src/pages/Users/index.tsx @@ -84,7 +84,7 @@ function Users() { return ( } to={buildDetailsPathname(id)} diff --git a/packages/console/src/utils/user.ts b/packages/console/src/utils/user.ts index 417f39ef6..cd30b291d 100644 --- a/packages/console/src/utils/user.ts +++ b/packages/console/src/utils/user.ts @@ -1,16 +1,19 @@ import type { User } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; +import { t } from 'i18next'; import { formatToInternationalPhoneNumber } from './phone'; -const getUserIdentity = (user: User) => { - const { primaryEmail, primaryPhone, username } = user; +const getSecondaryUserInfo = (user?: User) => { + const { primaryEmail, primaryPhone, username } = user ?? {}; const formattedPhoneNumber = conditional( primaryPhone && formatToInternationalPhoneNumber(primaryPhone) ); return primaryEmail ?? formattedPhoneNumber ?? username; }; -export const getUserTitle = (user: User) => user.name ?? getUserIdentity(user); +export const getUserTitle = (user?: User) => + user?.name ?? getSecondaryUserInfo(user) ?? t('admin_console.users.unnamed'); -export const getUserSubtitle = (user: User) => conditional(user.name && getUserIdentity(user)); +export const getUserSubtitle = (user?: User) => + conditional(user?.name && getSecondaryUserInfo(user));