Skip to content

Commit cc8df41

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 594a67c commit cc8df41

File tree

16 files changed

+80
-179
lines changed

16 files changed

+80
-179
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
22

33
import {render, screen} from 'sentry-test/reactTestingLibrary';
44

5-
import BaseBadge from 'sentry/components/idBadge/baseBadge';
5+
import {BaseBadge} from 'sentry/components/idBadge/baseBadge';
66

77
describe('BadgeBadge', function () {
88
it('has a display name', function () {

static/app/components/idBadge/baseBadge.tsx

Lines changed: 12 additions & 14 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,20 +32,20 @@ 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}
44-
data-test-id="badge-styled-avatar"
4549
/>
4650
)}
4751

@@ -57,19 +61,13 @@ const BaseBadge = memo(
5761
)
5862
);
5963

60-
export default BaseBadge;
61-
6264
const Wrapper = styled('div')`
6365
display: flex;
66+
gap: ${space(1)};
6467
align-items: center;
6568
flex-shrink: 0;
6669
`;
6770

68-
const StyledAvatar = styled(Avatar)<{hideName: boolean}>`
69-
margin-right: ${p => (p.hideName ? 0 : space(1))};
70-
flex-shrink: 0;
71-
`;
72-
7371
const DisplayNameAndDescription = styled('div')`
7472
display: flex;
7573
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/index.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('IdBadge', function () {
1717

1818
it('renders the correct component when `team` property is passed', function () {
1919
render(<IdBadge team={TeamFixture()} />);
20-
expect(screen.getByTestId('badge-styled-avatar')).toHaveTextContent('TS');
20+
expect(screen.getByTestId('letter_avatar-avatar')).toHaveTextContent('TS');
2121
expect(screen.getByTestId('badge-display-name')).toHaveTextContent('#team-slug');
2222
});
2323

@@ -28,7 +28,7 @@ describe('IdBadge', function () {
2828

2929
it('renders the correct component when `organization` property is passed', function () {
3030
render(<IdBadge organization={OrganizationFixture()} />);
31-
expect(screen.getByTestId('badge-styled-avatar')).toHaveTextContent('OS');
31+
expect(screen.getByTestId('default-avatar')).toHaveTextContent('OS');
3232
expect(screen.getByTestId('badge-display-name')).toHaveTextContent('org-slug');
3333
});
3434

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,15 @@ describe('MemberBadge', function () {
1414
});
1515

1616
it('renders with link when member and orgId are supplied', function () {
17-
render(<MemberBadge member={member} orgId="orgId" />, {context: routerContext});
17+
render(<MemberBadge member={member} />, {context: routerContext});
1818

1919
expect(screen.getByTestId('letter_avatar-avatar')).toBeInTheDocument();
2020
expect(screen.getByRole('link', {name: 'Foo Bar'})).toBeInTheDocument();
2121
expect(screen.getByText('foo@example.com')).toBeInTheDocument();
2222
});
2323

24-
it('does not use a link when useLink = false', function () {
25-
render(<MemberBadge member={member} useLink={false} orgId="orgId" />);
26-
27-
expect(screen.queryByRole('link', {name: 'Foo Bar'})).not.toBeInTheDocument();
28-
expect(screen.getByText('Foo Bar')).toBeInTheDocument();
29-
});
30-
31-
it('does not use a link when orgId = null', function () {
32-
render(<MemberBadge member={member} useLink />);
24+
it('does not use a link when disableLink', function () {
25+
render(<MemberBadge member={member} disableLink />);
3326

3427
expect(screen.queryByRole('link', {name: 'Foo Bar'})).not.toBeInTheDocument();
3528
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.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import OrganizationBadge from 'sentry/components/idBadge/organizationBadge';
77
describe('OrganizationBadge', function () {
88
it('renders with Avatar and organization name', function () {
99
render(<OrganizationBadge organization={OrganizationFixture()} />);
10-
expect(screen.getByTestId('badge-styled-avatar')).toBeInTheDocument();
10+
expect(screen.getByTestId('default-avatar')).toBeInTheDocument();
1111
expect(screen.getByTestId('badge-display-name')).toHaveTextContent('org-slug');
1212
});
1313
});

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.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('TeamBadge', function () {
1212

1313
it('renders with Avatar and team name', function () {
1414
render(<TeamBadge team={TeamFixture()} />);
15-
expect(screen.getByTestId('badge-styled-avatar')).toBeInTheDocument();
15+
expect(screen.getByTestId('letter_avatar-avatar')).toBeInTheDocument();
1616
expect(screen.getByText(/#team-slug/)).toBeInTheDocument();
1717
});
1818

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

0 commit comments

Comments
 (0)