From 61c0d6f0a1765627a99feca4ab980b4eec8b3c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Tue, 26 Jul 2022 18:50:19 +0100 Subject: [PATCH] refactor: create user avatar component, clean up (#1151) * refactor: create user avatar component, clean up * fix: small changes regarding the new badge component --- .../component/admin/groups/Group/Group.tsx | 17 +-- .../GroupFormUsersTable.tsx | 18 +-- .../groups/GroupsList/GroupCard/GroupCard.tsx | 2 + .../GroupCardAvatars/GroupCardAvatars.tsx | 73 +++-------- .../GroupPopover/GroupPopover.tsx | 3 +- .../ChangePassword/ChangePassword.tsx | 18 +-- .../users/UsersList/DeleteUser/DeleteUser.tsx | 20 +-- .../admin/users/UsersList/UsersList.tsx | 21 +--- frontend/src/component/common/Badge/Badge.tsx | 7 +- .../common/UserAvatar/UserAvatar.tsx | 115 ++++++++++++++++++ .../ProjectAccessTable/ProjectAccessTable.tsx | 26 +--- .../ProjectGroupView/ProjectGroupView.tsx | 18 +-- 12 files changed, 172 insertions(+), 166 deletions(-) create mode 100644 frontend/src/component/common/UserAvatar/UserAvatar.tsx diff --git a/frontend/src/component/admin/groups/Group/Group.tsx b/frontend/src/component/admin/groups/Group/Group.tsx index a0a35cc620..f50bd58b92 100644 --- a/frontend/src/component/admin/groups/Group/Group.tsx +++ b/frontend/src/component/admin/groups/Group/Group.tsx @@ -1,6 +1,5 @@ import { useEffect, useMemo, useState, VFC } from 'react'; import { - Avatar, Button, IconButton, styled, @@ -37,12 +36,7 @@ import { ActionCell } from 'component/common/Table/cells/ActionCell/ActionCell'; import { AddGroupUser } from './AddGroupUser/AddGroupUser'; import { EditGroupUser } from './EditGroupUser/EditGroupUser'; import { RemoveGroupUser } from './RemoveGroupUser/RemoveGroupUser'; - -const StyledAvatar = styled(Avatar)(({ theme }) => ({ - width: theme.spacing(4), - height: theme.spacing(4), - margin: 'auto', -})); +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; const StyledEdit = styled(Edit)(({ theme }) => ({ fontSize: theme.fontSizes.mainHeader, @@ -87,14 +81,7 @@ export const Group: VFC = () => { accessor: 'imageUrl', Cell: ({ row: { original: user } }: any) => ( - + ), maxWidth: 85, diff --git a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx index 46cc82d0f0..abbfc907e5 100644 --- a/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx +++ b/frontend/src/component/admin/groups/GroupForm/GroupFormUsersTable/GroupFormUsersTable.tsx @@ -1,5 +1,5 @@ import { useMemo, VFC } from 'react'; -import { Avatar, IconButton, styled, Tooltip } from '@mui/material'; +import { IconButton, Tooltip } from '@mui/material'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { IGroupUser } from 'interfaces/group'; import { HighlightCell } from 'component/common/Table/cells/HighlightCell/HighlightCell'; @@ -10,12 +10,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { VirtualizedTable } from 'component/common/Table'; import { useFlexLayout, useSortBy, useTable } from 'react-table'; import { sortTypes } from 'utils/sortTypes'; - -const StyledAvatar = styled(Avatar)(({ theme }) => ({ - width: theme.spacing(4), - height: theme.spacing(4), - margin: 'auto', -})); +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; interface IGroupFormUsersTableProps { users: IGroupUser[]; @@ -33,14 +28,7 @@ export const GroupFormUsersTable: VFC = ({ accessor: 'imageUrl', Cell: ({ row: { original: user } }: any) => ( - + ), maxWidth: 85, diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx index a41e22a736..2f78c3e6bc 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx @@ -7,6 +7,7 @@ import { Badge } from 'component/common/Badge/Badge'; import { GroupCardActions } from './GroupCardActions/GroupCardActions'; import { RemoveGroup } from 'component/admin/groups/RemoveGroup/RemoveGroup'; import { useState } from 'react'; +import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined'; const StyledLink = styled(Link)(({ theme }) => ({ textDecoration: 'none', @@ -93,6 +94,7 @@ export const GroupCard = ({ group }: IGroupCardProps) => { show={group.projects.map(project => ( } sx={{ marginRight: 0.5 }} > {project} diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx index c5daf720a8..73e6245bd0 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/GroupCardAvatars.tsx @@ -1,9 +1,9 @@ -import { Avatar, Badge, styled } from '@mui/material'; +import { styled } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { IGroupUser, Role } from 'interfaces/group'; import React, { useMemo, useState } from 'react'; -import StarIcon from '@mui/icons-material/Star'; import { GroupPopover } from './GroupPopover/GroupPopover'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; const StyledAvatars = styled('div')(({ theme }) => ({ display: 'inline-flex', @@ -12,25 +12,8 @@ const StyledAvatars = styled('div')(({ theme }) => ({ marginLeft: theme.spacing(1), })); -const StyledAvatar = styled(Avatar)(({ theme }) => ({ - width: theme.spacing(4), - height: theme.spacing(4), - outline: `2px solid ${theme.palette.background.paper}`, - marginLeft: theme.spacing(-1), -})); - -const StyledAvatarMore = styled(StyledAvatar)(({ theme }) => ({ - backgroundColor: theme.palette.secondary.light, - color: theme.palette.text.primary, - fontSize: theme.fontSizes.smallerBody, - fontWeight: theme.fontWeight.bold, -})); - -const StyledStar = styled(StarIcon)(({ theme }) => ({ - color: theme.palette.warning.main, - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadiusExtraLarge, - fontSize: theme.fontSizes.smallBody, +const StyledAvatar = styled(UserAvatar)(({ theme }) => ({ + outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, marginLeft: theme.spacing(-1), })); @@ -60,50 +43,22 @@ export const GroupCardAvatars = ({ users }: IGroupCardAvatarsProps) => { return ( {shownUsers.map(user => ( - { - onPopoverOpen(event); - setPopupUser(user); - }} - onMouseLeave={onPopoverClose} - /> - } - elseShow={ - } - > - { - onPopoverOpen(event); - setPopupUser(user); - }} - onMouseLeave={onPopoverClose} - /> - - } + { + onPopoverOpen(event); + setPopupUser(user); + }} + onMouseLeave={onPopoverClose} /> ))} 9} show={ - + +{users.length - shownUsers.length} - + } /> - {user?.name} + {user?.name || user?.username}
{user?.email}
); diff --git a/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx b/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx index 6521dd27c3..251e9f7d43 100644 --- a/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx +++ b/frontend/src/component/admin/users/UsersList/ChangePassword/ChangePassword.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import classnames from 'classnames'; -import { Avatar, TextField, Typography } from '@mui/material'; +import { styled, TextField, Typography } from '@mui/material'; import { trim } from 'component/common/util'; import { modalStyles } from 'component/admin/users/util'; import { Dialogue } from 'component/common/Dialogue/Dialogue'; @@ -11,6 +11,13 @@ import { useThemeStyles } from 'themes/themeStyles'; import PasswordMatcher from 'component/user/common/ResetPasswordForm/PasswordMatcher/PasswordMatcher'; import { IUser } from 'interfaces/user'; import useAdminUsersApi from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; + +const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({ + width: theme.spacing(5), + height: theme.spacing(5), + margin: 0, +})); interface IChangePasswordProps { showDialog: boolean; @@ -85,14 +92,7 @@ const ChangePassword = ({ Changing password for user
- + ({ + width: theme.spacing(5), + height: theme.spacing(5), + margin: 0, +})); interface IDeleteUserProps { showDialog: boolean; @@ -51,14 +58,7 @@ const DeleteUser = ({ } />
- + ({ - width: theme.spacing(4), - height: theme.spacing(4), - margin: 'auto', -})); +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; const UsersList = () => { const navigate = useNavigate(); @@ -125,14 +119,7 @@ const UsersList = () => { accessor: 'imageUrl', Cell: ({ row: { original: user } }: any) => ( - + ), disableGlobalFilter: true, @@ -212,7 +199,7 @@ const UsersList = () => { setHiddenColumns, } = useTable( { - columns: columns as any[], // TODO: fix after `react-table` v8 update + columns: columns, data, initialState, sortTypes, diff --git a/frontend/src/component/common/Badge/Badge.tsx b/frontend/src/component/common/Badge/Badge.tsx index 855508bd5d..bbcce6fa69 100644 --- a/frontend/src/component/common/Badge/Badge.tsx +++ b/frontend/src/component/common/Badge/Badge.tsx @@ -24,12 +24,14 @@ interface IBadgeIconProps { } const StyledBadge = styled('div')( - ({ theme, color = 'neutral' }) => ({ + ({ theme, color = 'neutral', icon }) => ({ display: 'inline-flex', alignItems: 'center', - padding: theme.spacing(0.5, 1), + padding: theme.spacing(icon ? 0.375 : 0.625, 1), borderRadius: theme.shape.borderRadius, fontSize: theme.fontSizes.smallerBody, + fontWeight: theme.fontWeight.bold, + lineHeight: 1, backgroundColor: theme.palette[color].light, color: theme.palette[color].dark, border: `1px solid ${theme.palette[color].border}`, @@ -58,6 +60,7 @@ export const Badge: FC = forwardRef( ) => ( ({ + width: theme.spacing(4), + height: theme.spacing(4), + margin: 'auto', + backgroundColor: theme.palette.secondary.light, + color: theme.palette.text.primary, + fontSize: theme.fontSizes.smallerBody, + fontWeight: theme.fontWeight.bold, +})); + +const StyledStar = styled(StarIcon)(({ theme }) => ({ + color: theme.palette.warning.main, + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadiusExtraLarge, + fontSize: theme.fontSizes.smallBody, + marginLeft: theme.spacing(-1), +})); + +interface IUserAvatarProps extends AvatarProps { + user?: IUser; + star?: boolean; + src?: string; + title?: string; + onMouseEnter?: (event: any) => void; + onMouseLeave?: () => void; + className?: string; + sx?: SxProps; +} + +export const UserAvatar: FC = ({ + user, + star, + src, + title, + onMouseEnter, + onMouseLeave, + className, + sx, + children, + ...props +}) => { + if (!title && !onMouseEnter && user) { + title = `${user?.name || user?.email || user?.username} (id: ${ + user?.id + })`; + } + + if (!src && user) { + src = user?.imageUrl; + } + + let fallback; + if (!children && user) { + fallback = user?.name || user?.email || user?.username; + if (fallback && fallback.includes(' ')) { + fallback = `${fallback.split(' ')[0][0]}${ + fallback.split(' ')[1][0] + }`.toUpperCase(); + } else if (fallback) { + fallback = fallback[0].toUpperCase(); + } + } + + const avatar = ( + + + + ); + + return ( + } + > + {avatar} + + } + elseShow={avatar} + /> + ); +}; diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx index 6fc76c2828..f6c4b01535 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectAccessTable/ProjectAccessTable.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState, VFC } from 'react'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { VirtualizedTable, TablePlaceholder } from 'component/common/Table'; -import { Avatar, Button, styled, useMediaQuery, useTheme } from '@mui/material'; +import { Button, useMediaQuery, useTheme } from '@mui/material'; import { Delete, Edit } from '@mui/icons-material'; import { sortTypes } from 'utils/sortTypes'; import useProjectAccess, { @@ -32,16 +32,7 @@ import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { IUser } from 'interfaces/user'; import { IGroup } from 'interfaces/group'; import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell'; - -const StyledAvatar = styled(Avatar)(({ theme }) => ({ - width: theme.spacing(4), - height: theme.spacing(4), - margin: 'auto', - backgroundColor: theme.palette.secondary.light, - color: theme.palette.text.primary, - fontSize: theme.fontSizes.smallBody, - fontWeight: theme.fontWeight.bold, -})); +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; export type PageQueryType = Partial< Record<'sort' | 'order' | 'search', string> @@ -106,18 +97,9 @@ export const ProjectAccessTable: VFC = () => { accessor: 'imageUrl', Cell: ({ row: { original: row } }: any) => ( - + {row.entity.users?.length} - + ), maxWidth: 85, diff --git a/frontend/src/component/project/ProjectAccess/ProjectGroupView/ProjectGroupView.tsx b/frontend/src/component/project/ProjectAccess/ProjectGroupView/ProjectGroupView.tsx index 7fab7cfa8b..44a9afbe9e 100644 --- a/frontend/src/component/project/ProjectAccess/ProjectGroupView/ProjectGroupView.tsx +++ b/frontend/src/component/project/ProjectAccess/ProjectGroupView/ProjectGroupView.tsx @@ -1,5 +1,5 @@ import { Delete, Edit } from '@mui/icons-material'; -import { Avatar, styled, useMediaQuery, useTheme } from '@mui/material'; +import { styled, useMediaQuery, useTheme } from '@mui/material'; import { GroupUserRoleCell } from 'component/admin/groups/GroupUserRoleCell/GroupUserRoleCell'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { PageContent } from 'component/common/PageContent/PageContent'; @@ -13,6 +13,7 @@ import { HighlightCell } from 'component/common/Table/cells/HighlightCell/Highli import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import { useSearch } from 'hooks/useSearch'; import { IGroup, IGroupUser } from 'interfaces/group'; @@ -20,12 +21,6 @@ import { VFC, useState } from 'react'; import { SortingRule, useFlexLayout, useSortBy, useTable } from 'react-table'; import { sortTypes } from 'utils/sortTypes'; -const StyledAvatar = styled(Avatar)(({ theme }) => ({ - width: theme.spacing(4), - height: theme.spacing(4), - margin: 'auto', -})); - const StyledPageContent = styled(PageContent)(({ theme }) => ({ height: '100vh', padding: theme.spacing(7.5, 6), @@ -54,14 +49,7 @@ const columns = [ accessor: 'imageUrl', Cell: ({ row: { original: user } }: any) => ( - + ), maxWidth: 85,