Skip to content

Commit 9c12138

Browse files
committed
ref(ui): Simplify badges implementation
- The UserBadge no longer has a bunch of custom styling and uses the existing styles - The MemberBadge delegates to the User badge now
1 parent 13dcc60 commit 9c12138

File tree

13 files changed

+74
-165
lines changed

13 files changed

+74
-165
lines changed

static/app/components/idBadge/baseBadge.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,27 @@ import styled from '@emotion/styled';
33

44
import Avatar from 'sentry/components/avatar';
55
import {space} from 'sentry/styles/space';
6-
import type {AvatarProject, Organization, Team} from 'sentry/types';
6+
import type {AvatarProject, AvatarUser, Organization, Team} from 'sentry/types';
77

88
export interface BaseBadgeProps {
9-
displayName: React.ReactNode;
109
avatarProps?: Record<string, any>;
1110
avatarSize?: number;
1211
className?: string;
1312
description?: React.ReactNode;
1413
// Hides the main display name
1514
hideAvatar?: boolean;
1615
hideName?: boolean;
16+
}
17+
18+
interface AllBaseBadgeProps extends BaseBadgeProps {
19+
displayName: React.ReactNode;
1720
organization?: Organization;
1821
project?: AvatarProject;
1922
team?: Team;
23+
user?: AvatarUser;
2024
}
2125

22-
const BaseBadge = memo(
26+
export const BaseBadge = memo(
2327
({
2428
displayName,
2529
hideName = false,
@@ -28,17 +32,18 @@ const BaseBadge = memo(
2832
avatarSize = 24,
2933
description,
3034
team,
35+
user,
3136
organization,
3237
project,
3338
className,
34-
}: BaseBadgeProps) => (
39+
}: AllBaseBadgeProps) => (
3540
<Wrapper className={className}>
3641
{!hideAvatar && (
37-
<StyledAvatar
42+
<Avatar
3843
{...avatarProps}
3944
size={avatarSize}
40-
hideName={hideName}
4145
team={team}
46+
user={user}
4247
organization={organization}
4348
project={project}
4449
data-test-id="badge-styled-avatar"
@@ -57,19 +62,13 @@ const BaseBadge = memo(
5762
)
5863
);
5964

60-
export default BaseBadge;
61-
6265
const Wrapper = styled('div')`
6366
display: flex;
67+
gap: ${space(1)};
6468
align-items: center;
6569
flex-shrink: 0;
6670
`;
6771

68-
const StyledAvatar = styled(Avatar)<{hideName: boolean}>`
69-
margin-right: ${p => (p.hideName ? 0 : space(1))};
70-
flex-shrink: 0;
71-
`;
72-
7372
const DisplayNameAndDescription = styled('div')`
7473
display: flex;
7574
flex-direction: column;

static/app/components/idBadge/getBadge.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import ProjectBadge, {type ProjectBadgeProps} from './projectBadge';
55
import {TeamBadge, type TeamBadgeProps} from './teamBadge';
66
import UserBadge, {type UserBadgeProps} from './userBadge';
77

8-
type DisplayName = BaseBadgeProps['displayName'];
9-
108
interface AddedBaseBadgeProps {
11-
displayName?: DisplayName;
9+
displayName?: React.ReactNode;
1210
}
11+
1312
interface GetOrganizationBadgeProps
1413
extends AddedBaseBadgeProps,
1514
Omit<BaseBadgeProps, 'displayName' | 'organization'>,

static/app/components/idBadge/memberBadge.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('MemberBadge', function () {
2929
});
3030

3131
it('does not use a link when orgId = null', function () {
32-
render(<MemberBadge member={member} useLink />);
32+
render(<MemberBadge member={member} />);
3333

3434
expect(screen.queryByRole('link', {name: 'Foo Bar'})).not.toBeInTheDocument();
3535
expect(screen.getByText('Foo Bar')).toBeInTheDocument();
Lines changed: 15 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
1-
import styled from '@emotion/styled';
2-
import omit from 'lodash/omit';
3-
4-
import UserAvatar from 'sentry/components/avatar/userAvatar';
5-
import type {LinkProps} from 'sentry/components/links/link';
6-
import Link from 'sentry/components/links/link';
7-
import {space} from 'sentry/styles/space';
81
import type {AvatarUser, Member} from 'sentry/types';
2+
import useOrganization from 'sentry/utils/useOrganization';
3+
4+
import UserBadge, {UserBadgeProps} from './userBadge';
95

10-
export interface MemberBadgeProps {
6+
export interface MemberBadgeProps extends Omit<UserBadgeProps, 'user'> {
117
member: Member;
12-
avatarSize?: React.ComponentProps<typeof UserAvatar>['size'];
13-
className?: string;
14-
displayEmail?: string;
15-
displayName?: React.ReactNode;
16-
hideEmail?: boolean;
17-
orgId?: string;
18-
useLink?: boolean;
8+
/**
9+
* Do not link to the members page
10+
*/
11+
disableLink?: boolean;
1912
}
2013

2114
function getMemberUser(member: Member): AvatarUser {
@@ -32,82 +25,16 @@ function getMemberUser(member: Member): AvatarUser {
3225
};
3326
}
3427

35-
function MemberBadge({
36-
avatarSize = 24,
37-
useLink = true,
38-
hideEmail = false,
39-
displayName,
40-
displayEmail,
41-
member,
42-
orgId,
43-
className,
44-
}: MemberBadgeProps) {
28+
function MemberBadge({member, disableLink, ...props}: MemberBadgeProps) {
4529
const user = getMemberUser(member);
46-
const title =
47-
displayName ||
48-
user.name ||
49-
user.email ||
50-
user.username ||
51-
user.ipAddress ||
52-
// Because this can be used to render EventUser models, or User *interface*
53-
// objects from serialized Event models. we try both ipAddress and ip_address.
54-
user.ip_address;
55-
56-
return (
57-
<StyledUserBadge className={className}>
58-
<StyledAvatar user={user} size={avatarSize} />
59-
<StyledNameAndEmail>
60-
<StyledName
61-
useLink={useLink && !!orgId}
62-
hideEmail={hideEmail}
63-
to={(member && orgId && `/settings/${orgId}/members/${member.id}/`) || ''}
64-
>
65-
{title}
66-
</StyledName>
67-
{!hideEmail && <StyledEmail>{displayEmail || user.email}</StyledEmail>}
68-
</StyledNameAndEmail>
69-
</StyledUserBadge>
70-
);
71-
}
30+
const org = useOrganization({allowNull: true});
7231

73-
const StyledUserBadge = styled('div')`
74-
display: flex;
75-
align-items: center;
76-
`;
32+
const membersUrl =
33+
member && org && !disableLink
34+
? `/settings/${org.slug}/members/${member.id}/`
35+
: undefined;
7736

78-
const StyledNameAndEmail = styled('div')`
79-
flex-shrink: 1;
80-
min-width: 0;
81-
line-height: 1;
82-
`;
83-
84-
const StyledEmail = styled('div')`
85-
font-size: 0.875em;
86-
margin-top: ${space(0.25)};
87-
color: ${p => p.theme.gray300};
88-
${p => p.theme.overflowEllipsis};
89-
`;
90-
91-
interface NameProps {
92-
hideEmail: boolean;
93-
to: LinkProps['to'];
94-
useLink: boolean;
95-
children?: React.ReactNode;
37+
return <UserBadge to={membersUrl} user={user} {...props} />;
9638
}
9739

98-
const StyledName = styled(({useLink, to, ...props}: NameProps) => {
99-
const forwardProps = omit(props, 'hideEmail');
100-
return useLink ? <Link to={to} {...forwardProps} /> : <span {...forwardProps} />;
101-
})`
102-
font-weight: ${(p: NameProps) => (p.hideEmail ? 'inherit' : 'bold')};
103-
line-height: 1.15em;
104-
${p => p.theme.overflowEllipsis};
105-
`;
106-
107-
const StyledAvatar = styled(UserAvatar)`
108-
min-width: ${space(3)};
109-
min-height: ${space(3)};
110-
margin-right: ${space(1)};
111-
`;
112-
11340
export default MemberBadge;

static/app/components/idBadge/organizationBadge.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName';
2-
import BaseBadge from 'sentry/components/idBadge/baseBadge';
1+
import {Organization} from 'sentry/types';
32

4-
type BaseBadgeProps = React.ComponentProps<typeof BaseBadge>;
5-
type Organization = NonNullable<BaseBadgeProps['organization']>;
3+
import BadgeDisplayName from './badgeDisplayName';
4+
import {BaseBadge, type BaseBadgeProps} from './baseBadge';
65

7-
export interface OrganizationBadgeProps
8-
extends Partial<Omit<BaseBadgeProps, 'project' | 'organization' | 'team'>> {
9-
// A full organization is not used, but required to satisfy types with
10-
// withOrganization()
6+
export interface OrganizationBadgeProps extends BaseBadgeProps {
117
organization: Organization;
12-
// If true, will use default max-width, or specify one as a string
8+
/**
9+
* When true will default max-width, or specify one as a string
10+
*/
1311
hideOverflow?: boolean | string;
1412
}
1513

static/app/components/idBadge/projectBadge.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
import {cloneElement} from 'react';
22
import styled from '@emotion/styled';
33

4-
import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName';
5-
import BaseBadge from 'sentry/components/idBadge/baseBadge';
64
import type {LinkProps} from 'sentry/components/links/link';
75
import Link from 'sentry/components/links/link';
6+
import {AvatarProject} from 'sentry/types';
87
import getPlatformName from 'sentry/utils/getPlatformName';
98
import useOrganization from 'sentry/utils/useOrganization';
109

11-
type BaseBadgeProps = React.ComponentProps<typeof BaseBadge>;
12-
type Project = NonNullable<BaseBadgeProps['project']>;
10+
import BadgeDisplayName from './badgeDisplayName';
11+
import {BaseBadge, type BaseBadgeProps} from './baseBadge';
1312

14-
export interface ProjectBadgeProps
15-
extends Partial<Omit<BaseBadgeProps, 'project' | 'organization' | 'team'>> {
16-
project: Project;
17-
className?: string;
13+
export interface ProjectBadgeProps extends BaseBadgeProps {
14+
project: AvatarProject;
1815
/**
1916
* If true, this component will not be a link to project details page
2017
*/

static/app/components/idBadge/teamBadge.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import {useLegacyStore} from 'sentry/stores/useLegacyStore';
33
import type {Team} from 'sentry/types';
44

55
import BadgeDisplayName from './badgeDisplayName';
6-
import BaseBadge, {type BaseBadgeProps} from './baseBadge';
6+
import {BaseBadge, type BaseBadgeProps} from './baseBadge';
77

8-
export interface TeamBadgeProps
9-
extends Partial<Omit<BaseBadgeProps, 'project' | 'organization' | 'team'>> {
8+
export interface TeamBadgeProps extends BaseBadgeProps {
109
team: Team;
1110
/**
1211
* When true will default max-width, or specify one as a string
Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
11
import styled from '@emotion/styled';
22

3-
import UserAvatar from 'sentry/components/avatar/userAvatar';
43
import {space} from 'sentry/styles/space';
54
import type {AvatarUser} from 'sentry/types';
65

7-
export interface UserBadgeProps {
8-
avatarSize?: React.ComponentProps<typeof UserAvatar>['size'];
9-
className?: string;
6+
import Link from '../links/link';
7+
8+
import BadgeDisplayName from './badgeDisplayName';
9+
import {BaseBadge, type BaseBadgeProps} from './baseBadge';
10+
11+
export interface UserBadgeProps extends BaseBadgeProps {
1012
displayEmail?: React.ReactNode | string;
1113
displayName?: React.ReactNode;
1214
hideEmail?: boolean;
15+
to?: string;
1316
user?: AvatarUser;
1417
}
1518

1619
function UserBadge({
17-
avatarSize = 24,
1820
hideEmail = false,
1921
displayName,
2022
displayEmail,
2123
user,
22-
className,
24+
to,
25+
...props
2326
}: UserBadgeProps) {
2427
const title =
2528
displayName ||
@@ -34,45 +37,33 @@ function UserBadge({
3437
user.ip ||
3538
user.id));
3639

40+
const name = <Name hideEmail={!!hideEmail}>{title}</Name>;
41+
3742
return (
38-
<StyledUserBadge className={className}>
39-
<StyledAvatar user={user} size={avatarSize} />
40-
<StyledNameAndEmail>
41-
<StyledName hideEmail={!!hideEmail}>{title}</StyledName>
42-
{!hideEmail && <StyledEmail>{displayEmail || user?.email}</StyledEmail>}
43-
</StyledNameAndEmail>
44-
</StyledUserBadge>
43+
<BaseBadge
44+
displayName={
45+
<BadgeDisplayName>
46+
{to ? <Link to={to}>{name}</Link> : name}
47+
{!hideEmail && <Email>{displayEmail || user?.email}</Email>}
48+
</BadgeDisplayName>
49+
}
50+
user={user}
51+
{...props}
52+
/>
4553
);
4654
}
4755

48-
const StyledUserBadge = styled('div')`
49-
display: flex;
50-
align-items: center;
51-
`;
52-
53-
const StyledNameAndEmail = styled('div')`
54-
flex-shrink: 1;
55-
min-width: 0;
56-
line-height: normal;
56+
const Name = styled('span')<{hideEmail: boolean}>`
57+
font-weight: ${p => (p.hideEmail ? 'inherit' : 'bold')};
58+
line-height: 1.15em;
59+
${p => p.theme.overflowEllipsis};
5760
`;
5861

59-
const StyledEmail = styled('div')`
62+
const Email = styled('div')`
6063
font-size: 0.875em;
6164
margin-top: ${space(0.25)};
6265
color: ${p => p.theme.gray300};
6366
${p => p.theme.overflowEllipsis};
6467
`;
6568

66-
export const StyledName = styled('span')<{hideEmail: boolean}>`
67-
font-weight: ${p => (p.hideEmail ? 'inherit' : 'bold')};
68-
line-height: 1.15em;
69-
${p => p.theme.overflowEllipsis};
70-
`;
71-
72-
const StyledAvatar = styled(UserAvatar)`
73-
min-width: ${space(3)};
74-
min-height: ${space(3)};
75-
margin-right: ${space(1)};
76-
`;
77-
7869
export default UserBadge;

static/app/components/replays/breadcrumbs/breadcrumbItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ function CrumbErrorIssue({frame}: {frame: FeedbackFrame | ErrorFrame}) {
155155
const {groupId} = useReplayGroupContext();
156156

157157
const projectBadge = project ? (
158-
<ProjectBadge project={project} avatarSize={16} disableLink displayName={false} />
158+
<ProjectBadge project={project} avatarSize={16} disableLink hideName />
159159
) : null;
160160

161161
if (String(frame.data.groupId) === groupId) {

static/app/components/selectMembers/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class SelectMembers extends Component<Props, State> {
7777
];
7878

7979
renderUserBadge = (user: User) => (
80-
<IdBadge avatarSize={24} user={user} hideEmail useLink={false} />
80+
<IdBadge avatarSize={24} user={user} hideEmail disableLink />
8181
);
8282

8383
createMentionableUser = (user: User): MentionableUser => ({

0 commit comments

Comments
 (0)