Skip to content

Commit

Permalink
feat(issues): Expand participants/viewers in issue sidebar (#58098)
Browse files Browse the repository at this point in the history
  • Loading branch information
scttcper authored Oct 16, 2023
1 parent 3ec1aac commit 1b4020d
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 36 deletions.
23 changes: 12 additions & 11 deletions static/app/components/avatar/avatarList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ function AvatarList({
const numVisibleTeams = maxVisibleAvatars - numTeams > 0 ? numTeams : maxVisibleAvatars;
const maxVisibleUsers =
maxVisibleAvatars - numVisibleTeams > 0 ? maxVisibleAvatars - numVisibleTeams : 0;
const visibleTeamAvatars = teams?.slice(0, numVisibleTeams);
const visibleUserAvatars = users.slice(0, maxVisibleUsers);
// 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();
const numCollapsedAvatars = users.length - visibleUserAvatars.length;

if (!tooltipOptions.position) {
Expand All @@ -51,15 +52,6 @@ function AvatarList({
</CollapsedAvatars>
</Tooltip>
)}
{visibleTeamAvatars?.map(team => (
<StyledTeamAvatar
key={`${team.id}-${team.name}`}
team={team}
size={avatarSize}
tooltipOptions={tooltipOptions}
hasTooltip
/>
))}
{visibleUserAvatars.map(user => (
<StyledUserAvatar
key={`${user.id}-${user.email}`}
Expand All @@ -70,6 +62,15 @@ function AvatarList({
hasTooltip
/>
))}
{visibleTeamAvatars?.map(team => (
<StyledTeamAvatar
key={`${team.id}-${team.name}`}
team={team}
size={avatarSize}
tooltipOptions={tooltipOptions}
hasTooltip
/>
))}
</AvatarListWrapper>
);
}
Expand Down
63 changes: 60 additions & 3 deletions static/app/views/issueDetails/groupSidebar.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {Tags} from 'sentry-fixture/tags';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, screen, waitFor, within} from 'sentry-test/reactTestingLibrary';
import {
render,
screen,
userEvent,
waitFor,
within,
} from 'sentry-test/reactTestingLibrary';

import MemberListStore from 'sentry/stores/memberListStore';

Expand Down Expand Up @@ -208,8 +214,59 @@ describe('GroupSidebar', function () {
/>
);

expect(await screen.findByText('Participants (2)')).toBeInTheDocument();
expect(screen.getByText('Viewers (2)')).toBeInTheDocument();
expect(
await screen.findByRole('heading', {name: 'Participants (2)'})
).toBeInTheDocument();
expect(screen.getByRole('heading', {name: 'Viewers (2)'})).toBeInTheDocument();
});

it('expands participants and viewers', async () => {
const org = {
...organization,
features: ['participants-purge'],
};
const teams = [{...TestStubs.Team(), type: 'team'}];
const users = [
TestStubs.User({
id: '2',
name: 'John Smith',
email: '[email protected]',
type: 'user',
}),
TestStubs.User({
id: '3',
name: 'Sohn Jmith',
email: '[email protected]',
type: 'user',
}),
];
render(
<GroupSidebar
group={{
...group,
participants: [...teams, ...users],
seenBy: users,
}}
project={project}
organization={org}
event={TestStubs.Event()}
environments={[]}
/>,
{
organization: org,
}
);

expect(
await screen.findByRole('heading', {name: 'Participants (1 Team, 2 Individuals)'})
).toBeInTheDocument();
expect(screen.queryByText('#team-slug')).not.toBeInTheDocument();

await userEvent.click(
screen.getAllByRole('button', {name: 'Expand Participants'})[0]
);

await waitFor(() => expect(screen.getByText('#team-slug')).toBeVisible());
});

describe('displays mobile tags when issue platform is mobile', function () {
Expand Down
109 changes: 87 additions & 22 deletions static/app/views/issueDetails/groupSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import TagFacets, {
import QuestionTooltip from 'sentry/components/questionTooltip';
import * as SidebarSection from 'sentry/components/sidebarSection';
import {backend, frontend} from 'sentry/data/platformCategories';
import {t} from 'sentry/locale';
import {t, tn} from 'sentry/locale';
import ConfigStore from 'sentry/stores/configStore';
import {space} from 'sentry/styles/space';
import {
Expand All @@ -44,6 +44,8 @@ import {useApiQuery} from 'sentry/utils/queryClient';
import {useLocation} from 'sentry/utils/useLocation';
import {getGroupDetailsQueryData} from 'sentry/views/issueDetails/utils';

import {ParticipantList} from './participantList';

type Props = {
environments: string[];
group: Group;
Expand Down Expand Up @@ -131,6 +133,7 @@ export default function GroupSidebar({
);
};

const hasParticipantsFeature = organization.features.includes('participants-purge');
const renderParticipantData = () => {
const {participants} = group;
if (!participants.length) {
Expand All @@ -144,10 +147,47 @@ export default function GroupSidebar({
(p): p is TeamParticipant => p.type === 'team'
);

const getParticipantTitle = (): React.ReactNode => {
if (!hasParticipantsFeature) {
return `${group.participants.length}`;
}

const individualText = tn(
'%s Individual',
'%s Individuals',
userParticipants.length
);
const teamText = tn('%s Team', '%s Teams', teamParticipants.length);

if (teamParticipants.length === 0) {
return individualText;
}

if (userParticipants.length === 0) {
return teamText;
}

return (
<Fragment>
{teamText}, {individualText}
</Fragment>
);
};

const avatars = (
<StyledAvatarList
users={userParticipants}
teams={teamParticipants}
avatarSize={28}
maxVisibleAvatars={hasParticipantsFeature ? 12 : 13}
typeAvatars="participants"
/>
);

return (
<SidebarSection.Wrap>
<SidebarSection.Title>
{t('Participants (%s)', participants.length)}
{t('Participants')} <TitleNumber>({getParticipantTitle()})</TitleNumber>
<QuestionTooltip
size="xs"
position="top"
Expand All @@ -157,13 +197,23 @@ export default function GroupSidebar({
/>
</SidebarSection.Title>
<SidebarSection.Content>
<StyledAvatarList
users={userParticipants}
teams={teamParticipants}
avatarSize={28}
maxVisibleAvatars={13}
typeAvatars="participants"
/>
{hasParticipantsFeature ? (
<ParticipantList
users={userParticipants}
teams={teamParticipants}
description={t('participants')}
>
{avatars}
</ParticipantList>
) : (
<StyledAvatarList
users={userParticipants}
teams={teamParticipants}
avatarSize={28}
maxVisibleAvatars={13}
typeAvatars="participants"
/>
)}
</SidebarSection.Content>
</SidebarSection.Wrap>
);
Expand All @@ -178,29 +228,40 @@ export default function GroupSidebar({
return null;
}

const avatars = (
<StyledAvatarList
users={displayUsers}
avatarSize={28}
maxVisibleAvatars={hasParticipantsFeature ? 12 : 13}
renderTooltip={user => (
<Fragment>
{userDisplayName(user)}
<br />
<DateTime date={(user as AvatarUser).lastSeen} />
</Fragment>
)}
/>
);

return (
<SidebarSection.Wrap>
<SidebarSection.Title>
{t('Viewers (%s)', displayUsers.length)}{' '}
{t('Viewers')}
<TitleNumber>({displayUsers.length})</TitleNumber>
<QuestionTooltip
size="xs"
position="top"
title={t('People who have viewed this issue')}
/>
</SidebarSection.Title>
<SidebarSection.Content>
<StyledAvatarList
users={displayUsers}
avatarSize={28}
maxVisibleAvatars={13}
renderTooltip={user => (
<Fragment>
{userDisplayName(user)}
<br />
<DateTime date={(user as AvatarUser).lastSeen} />
</Fragment>
)}
/>
{hasParticipantsFeature ? (
<ParticipantList users={displayUsers} teams={[]} description={t('users')}>
{avatars}
</ParticipantList>
) : (
avatars
)}
</SidebarSection.Content>
</SidebarSection.Wrap>
);
Expand Down Expand Up @@ -270,3 +331,7 @@ const StyledAvatarList = styled(AvatarList)`
justify-content: flex-end;
padding-left: ${space(0.75)};
`;

const TitleNumber = styled('span')`
font-weight: normal;
`;
47 changes: 47 additions & 0 deletions static/app/views/issueDetails/participantList.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {Team} from 'sentry-fixture/team';
import {User} from 'sentry-fixture/user';

import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';

import {ParticipantList} from './participantList';

describe('ParticipantList', () => {
const users = [
User({id: '1', name: 'John Doe', email: '[email protected]'}),
User({id: '2', name: 'Jane Doe', email: '[email protected]'}),
];

const teams = [
Team({id: '1', slug: 'team-1', memberCount: 3}),
Team({id: '2', slug: 'team-2', memberCount: 5}),
];

it('expands and collapses the list when clicked', async () => {
render(
<ParticipantList teams={teams} users={users} description="Participants">
Click Me
</ParticipantList>
);
expect(screen.queryByText('#team-1')).not.toBeInTheDocument();
await userEvent.click(screen.getByText('Click Me'));
await waitFor(() => expect(screen.getByText('#team-1')).toBeVisible());

expect(screen.getByText('Teams (2)')).toBeInTheDocument();
expect(screen.getByText('Individuals (2)')).toBeInTheDocument();

await userEvent.click(screen.getByText('Click Me'));
await waitFor(() => expect(screen.getByText('#team-1')).not.toBeVisible());
});

it('does not display section headers when there is only users or teams', async () => {
render(
<ParticipantList teams={[]} users={users} description="Participants">
Click Me
</ParticipantList>
);
await userEvent.click(screen.getByRole('button', {name: 'Click Me'}));
await waitFor(() => expect(screen.getByText('John Doe')).toBeVisible());

expect(screen.queryByText('Individuals (2)')).not.toBeInTheDocument();
});
});
Loading

0 comments on commit 1b4020d

Please sign in to comment.