import {forwardRef} from 'react'; import {css, type Theme} from '@emotion/react'; import styled from '@emotion/styled'; import TeamAvatar from 'sentry/components/avatar/teamAvatar'; import UserAvatar from 'sentry/components/avatar/userAvatar'; import {Tooltip} from 'sentry/components/tooltip'; import {space} from 'sentry/styles/space'; import type {Actor} from 'sentry/types/core'; import type {Team} from 'sentry/types/organization'; import type {AvatarUser} from 'sentry/types/user'; import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils'; type UserAvatarProps = React.ComponentProps; type Props = { avatarSize?: number; className?: string; maxVisibleAvatars?: number; renderCollapsedAvatars?: ( avatarSize: number, numCollapsedAvatars: number ) => React.ReactNode; renderTooltip?: UserAvatarProps['renderTooltip']; renderUsersFirst?: boolean; teams?: Team[]; tooltipOptions?: UserAvatarProps['tooltipOptions']; typeAvatars?: string; users?: (Actor | AvatarUser)[]; }; export const CollapsedAvatars = forwardRef(function CollapsedAvatars( { size, children, }: { children: React.ReactNode; size: number; }, ref: React.ForwardedRef ) { const hasStreamlinedUI = useHasStreamlinedUI(); if (hasStreamlinedUI) { return {children}; } return ( {children} ); }); function AvatarList({ avatarSize = 28, maxVisibleAvatars = 5, typeAvatars = 'users', tooltipOptions = {}, className, users = [], teams = [], renderUsersFirst = false, renderTooltip, renderCollapsedAvatars, }: Props) { const numTeams = teams.length; const numVisibleTeams = maxVisibleAvatars - numTeams > 0 ? numTeams : maxVisibleAvatars; const maxVisibleUsers = maxVisibleAvatars - numVisibleTeams > 0 ? maxVisibleAvatars - numVisibleTeams : 0; // Reverse the order since css flex-reverse is used to display the avatars const visibleTeamAvatars = teams.slice(0, numVisibleTeams).reverse(); const visibleUserAvatars = users.slice(0, maxVisibleUsers).reverse(); let numCollapsedAvatars = users.length + teams.length - (visibleUserAvatars.length + visibleTeamAvatars.length); if (numCollapsedAvatars === 1) { if (visibleTeamAvatars.length < teams.length) { visibleTeamAvatars.unshift(teams[teams.length - 1]!); } else if (visibleUserAvatars.length < users.length) { visibleUserAvatars.unshift(users[users.length - 1]!); } numCollapsedAvatars = 0; } if (!tooltipOptions.position) { tooltipOptions.position = 'top'; } return ( {!!numCollapsedAvatars && (renderCollapsedAvatars ? ( renderCollapsedAvatars(avatarSize, numCollapsedAvatars) ) : ( {numCollapsedAvatars < 99 && +} {numCollapsedAvatars} ))} {renderUsersFirst ? visibleTeamAvatars.map(team => ( )) : visibleUserAvatars.map(user => ( ))} {!renderUsersFirst ? visibleTeamAvatars.map(team => ( )) : visibleUserAvatars.map(user => ( ))} ); } export default AvatarList; // used in releases list page to do some alignment export const AvatarListWrapper = styled('div')` display: flex; align-items: center; flex-direction: row-reverse; `; const AvatarStyle = (p: {theme: Theme}) => css` border: 2px solid ${p.theme.background}; margin-left: -8px; cursor: default; &:hover { z-index: 1; } ${AvatarListWrapper}:hover & { border-color: ${p.theme.translucentBorder}; cursor: pointer; } `; const StyledUserAvatar = styled(UserAvatar)` overflow: hidden; border-radius: 50%; ${AvatarStyle}; `; const StyledTeamAvatar = styled(TeamAvatar)` overflow: hidden; ${AvatarStyle} `; const CollapsedAvatarsCicle = styled('div')<{size: number}>` display: flex; align-items: center; justify-content: center; position: relative; text-align: center; font-weight: ${p => p.theme.fontWeightBold}; background-color: ${p => p.theme.gray200}; color: ${p => p.theme.gray300}; font-size: ${p => Math.floor(p.size / 2.3)}px; width: ${p => p.size}px; height: ${p => p.size}px; border-radius: 50%; ${AvatarStyle}; `; const CollapsedAvatarPill = styled('div')` ${AvatarStyle}; display: flex; align-items: center; gap: ${space(0.25)}; font-weight: ${p => p.theme.fontWeightNormal}; color: ${p => p.theme.gray300}; height: 24px; padding: 0 ${space(1)}; background-color: ${p => p.theme.surface400}; border: 1px solid ${p => p.theme.border}; border-radius: 24px; ${AvatarListWrapper}:hover & { background-color: ${p => p.theme.surface100}; cursor: pointer; } `; const Plus = styled('span')` font-size: 10px; margin-left: 1px; margin-right: -1px; `;