diff --git a/static/app/components/idBadge/baseBadge.spec.tsx b/static/app/components/idBadge/baseBadge.spec.tsx index b54e525ca670bb..5ead308af721c8 100644 --- a/static/app/components/idBadge/baseBadge.spec.tsx +++ b/static/app/components/idBadge/baseBadge.spec.tsx @@ -2,7 +2,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; import {render, screen} from 'sentry-test/reactTestingLibrary'; -import BaseBadge from 'sentry/components/idBadge/baseBadge'; +import {BaseBadge} from 'sentry/components/idBadge/baseBadge'; describe('BadgeBadge', function () { it('has a display name', function () { diff --git a/static/app/components/idBadge/baseBadge.tsx b/static/app/components/idBadge/baseBadge.tsx index ffd89fafc383b2..d963f062496dc2 100644 --- a/static/app/components/idBadge/baseBadge.tsx +++ b/static/app/components/idBadge/baseBadge.tsx @@ -3,10 +3,9 @@ import styled from '@emotion/styled'; import Avatar from 'sentry/components/avatar'; import {space} from 'sentry/styles/space'; -import type {AvatarProject, Organization, Team} from 'sentry/types'; +import type {AvatarProject, AvatarUser, Organization, Team} from 'sentry/types'; export interface BaseBadgeProps { - displayName: React.ReactNode; avatarProps?: Record; avatarSize?: number; className?: string; @@ -14,12 +13,17 @@ export interface BaseBadgeProps { // Hides the main display name hideAvatar?: boolean; hideName?: boolean; +} + +interface AllBaseBadgeProps extends BaseBadgeProps { + displayName: React.ReactNode; organization?: Organization; project?: AvatarProject; team?: Team; + user?: AvatarUser; } -const BaseBadge = memo( +export const BaseBadge = memo( ({ displayName, hideName = false, @@ -28,20 +32,20 @@ const BaseBadge = memo( avatarSize = 24, description, team, + user, organization, project, className, - }: BaseBadgeProps) => ( + }: AllBaseBadgeProps) => ( {!hideAvatar && ( - )} @@ -57,19 +61,13 @@ const BaseBadge = memo( ) ); -export default BaseBadge; - const Wrapper = styled('div')` display: flex; + gap: ${space(1)}; align-items: center; flex-shrink: 0; `; -const StyledAvatar = styled(Avatar)<{hideName: boolean}>` - margin-right: ${p => (p.hideName ? 0 : space(1))}; - flex-shrink: 0; -`; - const DisplayNameAndDescription = styled('div')` display: flex; flex-direction: column; diff --git a/static/app/components/idBadge/getBadge.tsx b/static/app/components/idBadge/getBadge.tsx index 5ef66d9e8902ec..675b3dfc3069ef 100644 --- a/static/app/components/idBadge/getBadge.tsx +++ b/static/app/components/idBadge/getBadge.tsx @@ -5,11 +5,10 @@ import ProjectBadge, {type ProjectBadgeProps} from './projectBadge'; import {TeamBadge, type TeamBadgeProps} from './teamBadge'; import UserBadge, {type UserBadgeProps} from './userBadge'; -type DisplayName = BaseBadgeProps['displayName']; - interface AddedBaseBadgeProps { - displayName?: DisplayName; + displayName?: React.ReactNode; } + interface GetOrganizationBadgeProps extends AddedBaseBadgeProps, Omit, diff --git a/static/app/components/idBadge/index.spec.tsx b/static/app/components/idBadge/index.spec.tsx index b7a0e8702026c3..c193be4e3be580 100644 --- a/static/app/components/idBadge/index.spec.tsx +++ b/static/app/components/idBadge/index.spec.tsx @@ -17,7 +17,7 @@ describe('IdBadge', function () { it('renders the correct component when `team` property is passed', function () { render(); - expect(screen.getByTestId('badge-styled-avatar')).toHaveTextContent('TS'); + expect(screen.getByTestId('letter_avatar-avatar')).toHaveTextContent('TS'); expect(screen.getByTestId('badge-display-name')).toHaveTextContent('#team-slug'); }); @@ -28,7 +28,7 @@ describe('IdBadge', function () { it('renders the correct component when `organization` property is passed', function () { render(); - expect(screen.getByTestId('badge-styled-avatar')).toHaveTextContent('OS'); + expect(screen.getByTestId('default-avatar')).toHaveTextContent('OS'); expect(screen.getByTestId('badge-display-name')).toHaveTextContent('org-slug'); }); diff --git a/static/app/components/idBadge/memberBadge.spec.tsx b/static/app/components/idBadge/memberBadge.spec.tsx index 492b24830c49e9..0f75758a794e59 100644 --- a/static/app/components/idBadge/memberBadge.spec.tsx +++ b/static/app/components/idBadge/memberBadge.spec.tsx @@ -14,22 +14,15 @@ describe('MemberBadge', function () { }); it('renders with link when member and orgId are supplied', function () { - render(, {context: routerContext}); + render(, {context: routerContext}); expect(screen.getByTestId('letter_avatar-avatar')).toBeInTheDocument(); expect(screen.getByRole('link', {name: 'Foo Bar'})).toBeInTheDocument(); expect(screen.getByText('foo@example.com')).toBeInTheDocument(); }); - it('does not use a link when useLink = false', function () { - render(); - - expect(screen.queryByRole('link', {name: 'Foo Bar'})).not.toBeInTheDocument(); - expect(screen.getByText('Foo Bar')).toBeInTheDocument(); - }); - - it('does not use a link when orgId = null', function () { - render(); + it('does not use a link when disableLink', function () { + render(); expect(screen.queryByRole('link', {name: 'Foo Bar'})).not.toBeInTheDocument(); expect(screen.getByText('Foo Bar')).toBeInTheDocument(); diff --git a/static/app/components/idBadge/memberBadge.tsx b/static/app/components/idBadge/memberBadge.tsx index 1b47c10e787682..10dedfea6fc5d8 100644 --- a/static/app/components/idBadge/memberBadge.tsx +++ b/static/app/components/idBadge/memberBadge.tsx @@ -1,21 +1,14 @@ -import styled from '@emotion/styled'; -import omit from 'lodash/omit'; - -import UserAvatar from 'sentry/components/avatar/userAvatar'; -import type {LinkProps} from 'sentry/components/links/link'; -import Link from 'sentry/components/links/link'; -import {space} from 'sentry/styles/space'; import type {AvatarUser, Member} from 'sentry/types'; +import useOrganization from 'sentry/utils/useOrganization'; + +import UserBadge, {type UserBadgeProps} from './userBadge'; -export interface MemberBadgeProps { +export interface MemberBadgeProps extends Omit { member: Member; - avatarSize?: React.ComponentProps['size']; - className?: string; - displayEmail?: string; - displayName?: React.ReactNode; - hideEmail?: boolean; - orgId?: string; - useLink?: boolean; + /** + * Do not link to the members page + */ + disableLink?: boolean; } function getMemberUser(member: Member): AvatarUser { @@ -32,82 +25,16 @@ function getMemberUser(member: Member): AvatarUser { }; } -function MemberBadge({ - avatarSize = 24, - useLink = true, - hideEmail = false, - displayName, - displayEmail, - member, - orgId, - className, -}: MemberBadgeProps) { +function MemberBadge({member, disableLink, ...props}: MemberBadgeProps) { const user = getMemberUser(member); - const title = - displayName || - user.name || - user.email || - user.username || - user.ipAddress || - // Because this can be used to render EventUser models, or User *interface* - // objects from serialized Event models. we try both ipAddress and ip_address. - user.ip_address; - - return ( - - - - - {title} - - {!hideEmail && {displayEmail || user.email}} - - - ); -} + const org = useOrganization({allowNull: true}); -const StyledUserBadge = styled('div')` - display: flex; - align-items: center; -`; + const membersUrl = + member && org && !disableLink + ? `/settings/${org.slug}/members/${member.id}/` + : undefined; -const StyledNameAndEmail = styled('div')` - flex-shrink: 1; - min-width: 0; - line-height: 1; -`; - -const StyledEmail = styled('div')` - font-size: 0.875em; - margin-top: ${space(0.25)}; - color: ${p => p.theme.gray300}; - ${p => p.theme.overflowEllipsis}; -`; - -interface NameProps { - hideEmail: boolean; - to: LinkProps['to']; - useLink: boolean; - children?: React.ReactNode; + return ; } -const StyledName = styled(({useLink, to, ...props}: NameProps) => { - const forwardProps = omit(props, 'hideEmail'); - return useLink ? : ; -})` - font-weight: ${(p: NameProps) => (p.hideEmail ? 'inherit' : 'bold')}; - line-height: 1.15em; - ${p => p.theme.overflowEllipsis}; -`; - -const StyledAvatar = styled(UserAvatar)` - min-width: ${space(3)}; - min-height: ${space(3)}; - margin-right: ${space(1)}; -`; - export default MemberBadge; diff --git a/static/app/components/idBadge/organizationBadge.spec.tsx b/static/app/components/idBadge/organizationBadge.spec.tsx index 3b0a05914b5a9b..62142005eedf19 100644 --- a/static/app/components/idBadge/organizationBadge.spec.tsx +++ b/static/app/components/idBadge/organizationBadge.spec.tsx @@ -7,7 +7,7 @@ import OrganizationBadge from 'sentry/components/idBadge/organizationBadge'; describe('OrganizationBadge', function () { it('renders with Avatar and organization name', function () { render(); - expect(screen.getByTestId('badge-styled-avatar')).toBeInTheDocument(); + expect(screen.getByTestId('default-avatar')).toBeInTheDocument(); expect(screen.getByTestId('badge-display-name')).toHaveTextContent('org-slug'); }); }); diff --git a/static/app/components/idBadge/organizationBadge.tsx b/static/app/components/idBadge/organizationBadge.tsx index ed4ab1cc108496..8fe525c66859fc 100644 --- a/static/app/components/idBadge/organizationBadge.tsx +++ b/static/app/components/idBadge/organizationBadge.tsx @@ -1,15 +1,13 @@ -import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName'; -import BaseBadge from 'sentry/components/idBadge/baseBadge'; +import type {Organization} from 'sentry/types'; -type BaseBadgeProps = React.ComponentProps; -type Organization = NonNullable; +import BadgeDisplayName from './badgeDisplayName'; +import {BaseBadge, type BaseBadgeProps} from './baseBadge'; -export interface OrganizationBadgeProps - extends Partial> { - // A full organization is not used, but required to satisfy types with - // withOrganization() +export interface OrganizationBadgeProps extends BaseBadgeProps { organization: Organization; - // If true, will use default max-width, or specify one as a string + /** + * When true will default max-width, or specify one as a string + */ hideOverflow?: boolean | string; } diff --git a/static/app/components/idBadge/projectBadge.tsx b/static/app/components/idBadge/projectBadge.tsx index 46003081563798..390a8c549ad128 100644 --- a/static/app/components/idBadge/projectBadge.tsx +++ b/static/app/components/idBadge/projectBadge.tsx @@ -1,20 +1,17 @@ import {cloneElement} from 'react'; import styled from '@emotion/styled'; -import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName'; -import BaseBadge from 'sentry/components/idBadge/baseBadge'; import type {LinkProps} from 'sentry/components/links/link'; import Link from 'sentry/components/links/link'; +import type {AvatarProject} from 'sentry/types'; import getPlatformName from 'sentry/utils/getPlatformName'; import useOrganization from 'sentry/utils/useOrganization'; -type BaseBadgeProps = React.ComponentProps; -type Project = NonNullable; +import BadgeDisplayName from './badgeDisplayName'; +import {BaseBadge, type BaseBadgeProps} from './baseBadge'; -export interface ProjectBadgeProps - extends Partial> { - project: Project; - className?: string; +export interface ProjectBadgeProps extends BaseBadgeProps { + project: AvatarProject; /** * If true, this component will not be a link to project details page */ diff --git a/static/app/components/idBadge/teamBadge.spec.tsx b/static/app/components/idBadge/teamBadge.spec.tsx index 58cdbf57421546..4a7bb79e7e1ca0 100644 --- a/static/app/components/idBadge/teamBadge.spec.tsx +++ b/static/app/components/idBadge/teamBadge.spec.tsx @@ -12,7 +12,7 @@ describe('TeamBadge', function () { it('renders with Avatar and team name', function () { render(); - expect(screen.getByTestId('badge-styled-avatar')).toBeInTheDocument(); + expect(screen.getByTestId('letter_avatar-avatar')).toBeInTheDocument(); expect(screen.getByText(/#team-slug/)).toBeInTheDocument(); }); diff --git a/static/app/components/idBadge/teamBadge.tsx b/static/app/components/idBadge/teamBadge.tsx index 8bbbcb978ade24..b1b4d79786ee76 100644 --- a/static/app/components/idBadge/teamBadge.tsx +++ b/static/app/components/idBadge/teamBadge.tsx @@ -3,10 +3,9 @@ import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import type {Team} from 'sentry/types'; import BadgeDisplayName from './badgeDisplayName'; -import BaseBadge, {type BaseBadgeProps} from './baseBadge'; +import {BaseBadge, type BaseBadgeProps} from './baseBadge'; -export interface TeamBadgeProps - extends Partial> { +export interface TeamBadgeProps extends BaseBadgeProps { team: Team; /** * When true will default max-width, or specify one as a string diff --git a/static/app/components/idBadge/userBadge.tsx b/static/app/components/idBadge/userBadge.tsx index 55e4d90c1aecc2..8930b194cc6efb 100644 --- a/static/app/components/idBadge/userBadge.tsx +++ b/static/app/components/idBadge/userBadge.tsx @@ -1,25 +1,28 @@ import styled from '@emotion/styled'; -import UserAvatar from 'sentry/components/avatar/userAvatar'; import {space} from 'sentry/styles/space'; import type {AvatarUser} from 'sentry/types'; -export interface UserBadgeProps { - avatarSize?: React.ComponentProps['size']; - className?: string; +import Link from '../links/link'; + +import BadgeDisplayName from './badgeDisplayName'; +import {BaseBadge, type BaseBadgeProps} from './baseBadge'; + +export interface UserBadgeProps extends BaseBadgeProps { displayEmail?: React.ReactNode | string; displayName?: React.ReactNode; hideEmail?: boolean; + to?: string; user?: AvatarUser; } function UserBadge({ - avatarSize = 24, hideEmail = false, displayName, displayEmail, user, - className, + to, + ...props }: UserBadgeProps) { const title = displayName || @@ -34,45 +37,33 @@ function UserBadge({ user.ip || user.id)); + const name = {title}; + return ( - - - - {title} - {!hideEmail && {displayEmail || user?.email}} - - + + {to ? {name} : name} + {!hideEmail && {displayEmail || user?.email}} + + } + user={user} + {...props} + /> ); } -const StyledUserBadge = styled('div')` - display: flex; - align-items: center; -`; - -const StyledNameAndEmail = styled('div')` - flex-shrink: 1; - min-width: 0; - line-height: normal; +const Name = styled('span')<{hideEmail: boolean}>` + font-weight: ${p => (p.hideEmail ? 'inherit' : 'bold')}; + line-height: 1.15em; + ${p => p.theme.overflowEllipsis}; `; -const StyledEmail = styled('div')` +const Email = styled('div')` font-size: 0.875em; margin-top: ${space(0.25)}; color: ${p => p.theme.gray300}; ${p => p.theme.overflowEllipsis}; `; -export const StyledName = styled('span')<{hideEmail: boolean}>` - font-weight: ${p => (p.hideEmail ? 'inherit' : 'bold')}; - line-height: 1.15em; - ${p => p.theme.overflowEllipsis}; -`; - -const StyledAvatar = styled(UserAvatar)` - min-width: ${space(3)}; - min-height: ${space(3)}; - margin-right: ${space(1)}; -`; - export default UserBadge; diff --git a/static/app/components/selectMembers/index.tsx b/static/app/components/selectMembers/index.tsx index 47dd111eafca58..bd7e26fe3ac1a5 100644 --- a/static/app/components/selectMembers/index.tsx +++ b/static/app/components/selectMembers/index.tsx @@ -77,7 +77,7 @@ class SelectMembers extends Component { ]; renderUserBadge = (user: User) => ( - + ); createMentionableUser = (user: User): MentionableUser => ({ diff --git a/static/app/views/settings/account/accountNotificationFineTuning.tsx b/static/app/views/settings/account/accountNotificationFineTuning.tsx index f5af817f3ba0c1..78460c1d9da323 100644 --- a/static/app/views/settings/account/accountNotificationFineTuning.tsx +++ b/static/app/views/settings/account/accountNotificationFineTuning.tsx @@ -71,7 +71,6 @@ function AccountNotificationsByProject({projects, field}: ANBPProps) { diff --git a/static/app/views/settings/organizationTeams/teamMembersRow.tsx b/static/app/views/settings/organizationTeams/teamMembersRow.tsx index e947b82344ba74..80550a59c432d8 100644 --- a/static/app/views/settings/organizationTeams/teamMembersRow.tsx +++ b/static/app/views/settings/organizationTeams/teamMembersRow.tsx @@ -34,7 +34,7 @@ function TeamMembersRow({ return (
- +
{ private selectRef = createRef(); renderUserBadge = (user: User) => ( - + ); createMentionableUser = (user: User): Owner => ({