diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx index 0f7034e40e..2c2ee17b21 100644 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx +++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCard.tsx @@ -7,7 +7,7 @@ import { GroupCardActions } from './GroupCardActions/GroupCardActions'; import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined'; import { RoleBadge } from 'component/common/RoleBadge/RoleBadge'; import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings'; -import { GroupCardAvatars } from './GroupCardAvatars/NewGroupCardAvatars'; +import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup'; const StyledLink = styled(Link)(({ theme }) => ({ textDecoration: 'none', @@ -132,7 +132,7 @@ export const GroupCard = ({ 0} - show={} + show={} elseShow={ This group has no users. diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx deleted file mode 100644 index 584c8065da..0000000000 --- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { styled } from '@mui/material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import type { IGroupUser } from 'interfaces/group'; -import type React from 'react'; -import { type ReactNode, useMemo, useState } from 'react'; -import { GroupPopover } from './GroupPopover/GroupPopover'; -import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; -import { objectId } from 'utils/objectId'; - -const StyledContainer = styled('div')(() => ({ - display: 'flex', - flexDirection: 'column', -})); - -const StyledAvatars = styled('div')(({ theme }) => ({ - display: 'inline-flex', - alignItems: 'center', - flexWrap: 'wrap', - marginLeft: theme.spacing(1), -})); - -const StyledAvatar = (component: typeof UserAvatar) => - styled(component)(({ theme }) => ({ - outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, - marginLeft: theme.spacing(-1), - '&:hover': { - outlineColor: theme.palette.primary.main, - }, - })); - -const StyledHeader = styled('h3')(({ theme }) => ({ - margin: theme.spacing(0, 0, 1), - fontSize: theme.typography.caption.fontSize, - color: theme.palette.text.primary, - fontWeight: theme.typography.fontWeightRegular, -})); - -interface IGroupCardAvatarsProps { - users: { - name: string; - description?: string; - imageUrl?: string; - }[]; - header?: ReactNode; - avatarLimit?: number; - AvatarComponent?: typeof UserAvatar; -} - -export const GroupCardAvatars = ({ - AvatarComponent, - ...props -}: IGroupCardAvatarsProps) => { - const Avatar = StyledAvatar(AvatarComponent ?? UserAvatar); - - return ; -}; - -type GroupCardAvatarsInnerProps = Omit< - IGroupCardAvatarsProps, - 'AvatarComponent' -> & { - AvatarComponent: typeof UserAvatar; -}; - -const GroupCardAvatarsInner = ({ - users = [], - header = null, - avatarLimit = 9, - AvatarComponent, -}: GroupCardAvatarsInnerProps) => { - const shownUsers = useMemo( - () => - users - .sort((a, b) => { - if ( - Object.hasOwn(a, 'joinedAt') && - Object.hasOwn(b, 'joinedAt') - ) { - return ( - (b as IGroupUser)?.joinedAt!.getTime() - - (a as IGroupUser)?.joinedAt!.getTime() - ); - } - return 0; - }) - .slice(0, avatarLimit), - [users], - ); - - const [anchorEl, setAnchorEl] = useState(null); - const [popupUser, setPopupUser] = useState<{ - name: string; - description?: string; - imageUrl?: string; - }>(); - - const onPopoverOpen = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const onPopoverClose = () => { - setAnchorEl(null); - }; - - const avatarOpen = Boolean(anchorEl); - - return ( - - {header}} - elseShow={header} - /> - - {shownUsers.map((user) => ( - { - onPopoverOpen(event); - setPopupUser(user); - }} - onMouseLeave={onPopoverClose} - /> - ))} - avatarLimit} - show={ - - +{users.length - shownUsers.length} - - } - /> - - - - ); -}; diff --git a/frontend/src/component/admin/users/InactiveUsersList/DeleteUser/DeleteUser.tsx b/frontend/src/component/admin/users/InactiveUsersList/DeleteUser/DeleteUser.tsx index 0c81f98692..cf1f063b33 100644 --- a/frontend/src/component/admin/users/InactiveUsersList/DeleteUser/DeleteUser.tsx +++ b/frontend/src/component/admin/users/InactiveUsersList/DeleteUser/DeleteUser.tsx @@ -1,19 +1,12 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { REMOVE_USER_ERROR } from 'hooks/api/actions/useAdminUsersApi/useAdminUsersApi'; -import { Alert, styled } from '@mui/material'; +import { Alert } from '@mui/material'; import useLoading from 'hooks/useLoading'; import { Typography } from '@mui/material'; import { useThemeStyles } from 'themes/themeStyles'; -import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import type { IInactiveUser } from '../../../../../hooks/api/getters/useInactiveUsers/useInactiveUsers'; -const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({ - width: theme.spacing(5), - height: theme.spacing(5), - margin: 0, -})); - interface IDeleteUserProps { showDialog: boolean; closeDialog: () => void; diff --git a/frontend/src/component/common/AvatarGroup/AvatarGroup.tsx b/frontend/src/component/common/AvatarGroup/AvatarGroup.tsx new file mode 100644 index 0000000000..ad6d0d5b28 --- /dev/null +++ b/frontend/src/component/common/AvatarGroup/AvatarGroup.tsx @@ -0,0 +1,92 @@ +import { styled } from '@mui/material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import type { IGroupUser } from 'interfaces/group'; +import { useMemo } from 'react'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; // usage +import { objectId } from 'utils/objectId'; + +const StyledAvatars = styled('div')(({ theme }) => ({ + display: 'inline-flex', + alignItems: 'center', + flexWrap: 'wrap', + marginLeft: theme.spacing(1), + justifyContent: 'start', +})); + +const StyledAvatar = (component: typeof UserAvatar) => + styled(component)(({ theme }) => ({ + outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`, + margin: 0, + marginLeft: theme.spacing(-1), + '&:hover': { + outlineColor: theme.palette.primary.main, + }, + })); + +type User = { + name: string; + description?: string; + imageUrl?: string; +}; +type AvatarGroupProps = { + users: User[]; + avatarLimit?: number; + AvatarComponent?: typeof UserAvatar; +}; + +export const AvatarGroup = ({ + AvatarComponent, + ...props +}: AvatarGroupProps) => { + const Avatar = StyledAvatar(AvatarComponent ?? UserAvatar); + + return ; +}; + +type GroupCardAvatarsInnerProps = Omit & { + AvatarComponent: typeof UserAvatar; +}; + +const GroupCardAvatarsInner = ({ + users = [], + avatarLimit = 9, + AvatarComponent, +}: GroupCardAvatarsInnerProps) => { + const shownUsers = useMemo( + () => + users + .sort((a, b) => { + if ( + Object.hasOwn(a, 'joinedAt') && + Object.hasOwn(b, 'joinedAt') + ) { + return ( + (b as IGroupUser)?.joinedAt!.getTime() - + (a as IGroupUser)?.joinedAt!.getTime() + ); + } + return 0; + }) + .slice(0, avatarLimit), + [users], + ); + + return ( + + {shownUsers.map((user) => ( + + ))} + avatarLimit} + show={ + + +{users.length - shownUsers.length} + + } + /> + + ); +}; diff --git a/frontend/src/component/common/UserAvatar/UserAvatar.tsx b/frontend/src/component/common/UserAvatar/UserAvatar.tsx index 811f02808f..6c329c2ddd 100644 --- a/frontend/src/component/common/UserAvatar/UserAvatar.tsx +++ b/frontend/src/component/common/UserAvatar/UserAvatar.tsx @@ -8,7 +8,7 @@ import { import type { IUser } from 'interfaces/user'; import type { FC } from 'react'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; - +import { HtmlTooltip } from '../HtmlTooltip/HtmlTooltip'; const StyledAvatar = styled(Avatar)(({ theme }) => ({ width: theme.spacing(3.5), height: theme.spacing(3.5), @@ -24,36 +24,51 @@ export interface IUserAvatarProps extends AvatarProps { Pick >; src?: string; - title?: string; - onMouseEnter?: (event: any) => void; - onMouseLeave?: () => void; className?: string; sx?: SxProps; - hideTitle?: boolean; + disableTooltip?: boolean; } +const tooltipContent = ( + user: IUserAvatarProps['user'], +): { main: string; secondary?: string } | undefined => { + if (!user) { + return undefined; + } + + const [mainIdentifier, secondaryInfo] = [ + user.email || user.username, + user.name, + ]; + + if (mainIdentifier) { + return { main: mainIdentifier, secondary: secondaryInfo }; + } else if (secondaryInfo) { + return { main: secondaryInfo }; + } else if (user.id) { + return { main: `User ID: ${user.id}` }; + } + + return undefined; +}; + +const TooltipSecondaryContent = styled('div')(({ theme }) => ({ + color: theme.palette.text.secondary, + fontSize: theme.typography.body2.fontSize, +})); +const TooltipMainContent = styled('div')(({ theme }) => ({ + fontSize: theme.typography.body1.fontSize, +})); + export const UserAvatar: FC = ({ user, src, - title, - onMouseEnter, - onMouseLeave, className, sx, children, - hideTitle, + disableTooltip, ...props }) => { - if (!hideTitle && !title && !onMouseEnter && user) { - title = `${user?.name || user?.email || user?.username} (id: ${ - user?.id - })`; - } - - if (!src && user) { - src = user?.imageUrl; - } - let fallback: string | undefined; if (!children && user) { fallback = user?.name || user?.email || user?.username; @@ -66,17 +81,14 @@ export const UserAvatar: FC = ({ } } - return ( + const Avatar = ( = ({ /> ); + + const tooltip = disableTooltip ? undefined : tooltipContent(user); + if (tooltip) { + return ( + + + {tooltip.secondary} + + {tooltip.main} + + } + > + {Avatar} + + ); + } + + return Avatar; }; diff --git a/frontend/src/component/feature/FeatureView/Collaborators.tsx b/frontend/src/component/feature/FeatureView/Collaborators.tsx index d08744f74b..829eca8cea 100644 --- a/frontend/src/component/feature/FeatureView/Collaborators.tsx +++ b/frontend/src/component/feature/FeatureView/Collaborators.tsx @@ -1,6 +1,5 @@ import { styled } from '@mui/material'; -import { GroupCardAvatars } from 'component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars'; -import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip'; +import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; import type { Collaborator } from 'interfaces/featureToggle'; import type { FC } from 'react'; @@ -30,11 +29,7 @@ const LastModifiedBy: FC = ({ id, name, imageUrl }) => { Last modified by - - - - - + view change @@ -47,7 +42,7 @@ const CollaboratorList: FC<{ collaborators: Collaborator[] }> = ({ return ( Collaborators - { {feature.createdBy?.name} diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx index 485f27e44d..03a59c5978 100644 --- a/frontend/src/component/feature/FeatureView/FeatureView.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx @@ -43,8 +43,6 @@ import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/Feat import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog'; import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi'; import { FavoriteIconButton } from 'component/common/FavoriteIconButton/FavoriteIconButton'; -import { ReactComponent as ChildLinkIcon } from 'assets/icons/link-child.svg'; -import { ReactComponent as ParentLinkIcon } from 'assets/icons/link-parent.svg'; import { ChildrenTooltip } from './FeatureOverview/FeatureOverviewMetaData/ChildrenTooltip'; import copy from 'copy-to-clipboard'; import useToast from 'hooks/useToast'; @@ -85,16 +83,6 @@ const StyledDependency = styled('div')(({ theme }) => ({ width: 'max-content', })); -const StyleChildLinkIcon = styled(ChildLinkIcon)(({ theme }) => ({ - width: theme.fontSizes.smallBody, - height: theme.fontSizes.smallBody, -})); - -const StyledParentLinkIcon = styled(ParentLinkIcon)(({ theme }) => ({ - width: theme.fontSizes.smallBody, - height: theme.fontSizes.smallBody, -})); - const StyledFeatureViewHeader = styled('h1')(({ theme }) => ({ fontSize: theme.fontSizes.mainHeader, fontWeight: 'normal', diff --git a/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx b/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx index 7a4c40a50a..0633557a02 100644 --- a/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx +++ b/frontend/src/component/project/NewProjectCard/ProjectOwners/ProjectOwners.tsx @@ -1,9 +1,9 @@ import type { FC } from 'react'; import { styled } from '@mui/material'; -import { GroupCardAvatars } from 'component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars'; import type { ProjectSchema, ProjectSchemaOwners } from 'openapi'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { AvatarGroup } from 'component/common/AvatarGroup/AvatarGroup'; interface IProjectOwnersProps { owners?: ProjectSchema['owners']; @@ -29,7 +29,7 @@ const useOwnersMap = () => { } if (owner.ownerType === 'group') { return { - name: owner.name || '', + name: owner.name, description: 'group', }; } @@ -50,17 +50,30 @@ const StyledUserName = styled('p')(({ theme }) => ({ alignSelf: 'end', })); +const StyledContainer = styled('div')(() => ({ + display: 'flex', + flexDirection: 'column', +})); + +const StyledHeader = styled('h3')(({ theme }) => ({ + margin: theme.spacing(0, 0, 1), + fontSize: theme.typography.caption.fontSize, + color: theme.palette.text.primary, + fontWeight: theme.typography.fontWeightRegular, +})); + export const ProjectOwners: FC = ({ owners = [] }) => { const ownersMap = useOwnersMap(); const users = owners.map(ownersMap); return ( <> - + + + {owners.length === 1 ? 'Owner' : 'Owners'} + + + ; createdBy?: { - id: string; + id: number; name: string; imageUrl: string; };