Skip to content

Commit

Permalink
feat(issue-details): Add collapsing to activity section (#79607)
Browse files Browse the repository at this point in the history
this pr adds collapsing to the activity section. for issues with more
less than 7 activities, all will be shown. if 7 or more, the 3 most
recent and first seen will be shown, while the rest are collapsed. you
can click to expand, while collapsing will be in the header for the
section
![Screenshot 2024-10-23 at 9 57
36 AM](https://github.com/user-attachments/assets/80cd103a-6a91-42e3-81da-d32c0a257025)

![Screenshot 2024-10-23 at 9 35
35 AM](https://github.com/user-attachments/assets/d02b8557-ac72-45cc-8c85-74dc4f9bf3ec)
  • Loading branch information
roggenkemper authored Oct 23, 2024
1 parent 98fed65 commit 471ae95
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {Group} from 'sentry/types/group';
import type {Project} from 'sentry/types/project';
import useOrganization from 'sentry/utils/useOrganization';
import {Divider} from 'sentry/views/issueDetails/divider';
import {SidebarSectionTitle} from 'sentry/views/issueDetails/streamline/sidebar';

import useStreamLinedExternalIssueData from './hooks/useGroupExternalIssues';

Expand All @@ -38,7 +39,7 @@ export function StreamlinedExternalIssueList({
if (isLoading) {
return (
<div data-test-id="linked-issues">
<StyledSectionTitle>{t('Issue Tracking')}</StyledSectionTitle>
<SidebarSectionTitle>{t('Issue Tracking')}</SidebarSectionTitle>
<SidebarSection.Content>
<Placeholder height="25px" />
</SidebarSection.Content>
Expand All @@ -48,7 +49,7 @@ export function StreamlinedExternalIssueList({

return (
<div data-test-id="linked-issues">
<StyledSectionTitle>{t('Issue Tracking')}</StyledSectionTitle>
<SidebarSectionTitle>{t('Issue Tracking')}</SidebarSectionTitle>
<SidebarSection.Content>
{integrations.length || linkedIssues.length ? (
<IssueActionWrapper>
Expand Down Expand Up @@ -143,11 +144,6 @@ const IssueActionWrapper = styled('div')`
line-height: 1.2;
`;

const StyledSectionTitle = styled(SidebarSection.Title)`
margin-top: ${space(0.25)};
color: ${p => p.theme.headingColor};
`;

const LinkedIssue = styled(LinkButton)`
display: flex;
align-items: center;
Expand Down
9 changes: 1 addition & 8 deletions static/app/types/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,6 @@ export enum GroupActivityType {
UNMERGE_SOURCE = 'unmerge_source',
UNMERGE_DESTINATION = 'unmerge_destination',
FIRST_SEEN = 'first_seen',
LAST_SEEN = 'last_seen',
ASSIGNED = 'assigned',
UNASSIGNED = 'unassigned',
MERGE = 'merge',
Expand Down Expand Up @@ -485,11 +484,6 @@ interface GroupActivityFirstSeen extends GroupActivityBase {
type: GroupActivityType.FIRST_SEEN;
}

interface GroupActivityLastSeen extends GroupActivityBase {
data: Record<string, any>;
type: GroupActivityType.LAST_SEEN;
}

interface GroupActivityMarkReviewed extends GroupActivityBase {
data: Record<string, any>;
type: GroupActivityType.MARK_REVIEWED;
Expand Down Expand Up @@ -685,8 +679,7 @@ export type GroupActivity =
| GroupActivityAutoSetOngoing
| GroupActivitySetEscalating
| GroupActivitySetPriority
| GroupActivityDeletedAttachment
| GroupActivityLastSeen;
| GroupActivityDeletedAttachment;

export type Activity = GroupActivity;

Expand Down
27 changes: 27 additions & 0 deletions static/app/views/issueDetails/streamline/activitySection.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import ConfigStore from 'sentry/stores/configStore';
import GroupStore from 'sentry/stores/groupStore';
import ProjectsStore from 'sentry/stores/projectsStore';
import type {GroupActivity} from 'sentry/types/group';
import {GroupActivityType} from 'sentry/types/group';
import StreamlinedActivitySection from 'sentry/views/issueDetails/streamline/activitySection';

Expand Down Expand Up @@ -89,4 +90,30 @@ describe('StreamlinedActivitySection', function () {
screen.queryByRole('button', {name: 'Comment Actions'})
).not.toBeInTheDocument();
});

it('collapses activity when there are more than 5 items', async function () {
const activities: GroupActivity[] = Array.from({length: 7}, (_, index) => ({
type: GroupActivityType.NOTE,
id: `note-${index + 1}`,
data: {text: `Test Note ${index + 1}`},
dateCreated: '2020-01-01T00:00:00',
user: UserFixture({id: '2'}),
project,
}));

const updatedActivityGroup = GroupFixture({
id: '1338',
activity: activities,
project,
});

render(<StreamlinedActivitySection group={updatedActivityGroup} />);
expect(await screen.findByText('Test Note 1')).toBeInTheDocument();
expect(await screen.findByText('Test Note 7')).toBeInTheDocument();
expect(screen.queryByText('Test Note 6')).not.toBeInTheDocument();
expect(await screen.findByText('4 comments hidden')).toBeInTheDocument();

await userEvent.click(await screen.findByRole('button', {name: 'Show all activity'}));
expect(await screen.findByText('Test Note 6')).toBeInTheDocument();
});
});
175 changes: 124 additions & 51 deletions static/app/views/issueDetails/streamline/activitySection.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import {useCallback, useState} from 'react';
import {Fragment, useCallback, useState} from 'react';
import styled from '@emotion/styled';

import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
import {NoteBody} from 'sentry/components/activity/note/body';
import {NoteInputWithStorage} from 'sentry/components/activity/note/inputWithStorage';
import {Button} from 'sentry/components/button';
import useMutateActivity from 'sentry/components/feedback/useMutateActivity';
import * as SidebarSection from 'sentry/components/sidebarSection';
import Timeline from 'sentry/components/timeline';
import TimeSince from 'sentry/components/timeSince';
import {IconEllipsis} from 'sentry/icons';
import {t} from 'sentry/locale';
import GroupStore from 'sentry/stores/groupStore';
import {space} from 'sentry/styles/space';
import type {NoteType} from 'sentry/types/alerts';
import type {Group, GroupActivity} from 'sentry/types/group';
import {GroupActivityType} from 'sentry/types/group';
import type {Release} from 'sentry/types/release';
import type {Team} from 'sentry/types/organization';
import type {User} from 'sentry/types/user';
import {uniqueId} from 'sentry/utils/guid';
import useOrganization from 'sentry/utils/useOrganization';
Expand All @@ -23,14 +24,59 @@ import {useUser} from 'sentry/utils/useUser';
import {groupActivityTypeIconMapping} from 'sentry/views/issueDetails/streamline/groupActivityIcons';
import getGroupActivityItem from 'sentry/views/issueDetails/streamline/groupActivityItem';
import {NoteDropdown} from 'sentry/views/issueDetails/streamline/noteDropdown';
import {SidebarSectionTitle} from 'sentry/views/issueDetails/streamline/sidebar';

function TimelineItem({
item,
handleDelete,
group,
teams,
}: {
group: Group;
handleDelete: (item: GroupActivity) => void;
item: GroupActivity;
teams: Team[];
}) {
const organization = useOrganization();
const authorName = item.user ? item.user.name : 'Sentry';
const {title, message} = getGroupActivityItem(
item,
organization,
group.project.id,
<Author>{authorName}</Author>,
teams
);

export interface GroupRelease {
firstRelease: Release;
lastRelease: Release;
const Icon = groupActivityTypeIconMapping[item.type]?.Component ?? null;

return (
<ActivityTimelineItem
title={
<TitleWrapper>
{title}
<NoteDropdownWrapper>
{item.type === GroupActivityType.NOTE && (
<NoteDropdown onDelete={() => handleDelete(item)} user={item.user} />
)}
</NoteDropdownWrapper>
</TitleWrapper>
}
timestamp={<SmallTimestamp date={item.dateCreated} />}
icon={
Icon && (
<Icon {...groupActivityTypeIconMapping[item.type].defaultProps} size="xs" />
)
}
>
{typeof message === 'string' ? <NoteBody text={message} /> : message}
</ActivityTimelineItem>
);
}
function StreamlinedActivitySection({group}: {group: Group}) {

export default function StreamlinedActivitySection({group}: {group: Group}) {
const organization = useOrganization();
const {teams} = useTeamsById();
const [showAll, setShowAll] = useState(false);

const [inputId, setInputId] = useState(uniqueId());

Expand Down Expand Up @@ -90,7 +136,14 @@ function StreamlinedActivitySection({group}: {group: Group}) {

return (
<div>
<StyledSectionTitle>{t('Activity')}</StyledSectionTitle>
<TitleSection>
<SidebarSectionTitle>{t('Activity')}</SidebarSectionTitle>
{showAll && (
<CollapseButton borderless size="zero" onClick={() => setShowAll(false)}>
{t('Collapse')}
</CollapseButton>
)}
</TitleSection>
<Timeline.Container>
<NoteInputWithStorage
key={inputId}
Expand All @@ -103,48 +156,53 @@ function StreamlinedActivitySection({group}: {group: Group}) {
source="issue-details"
{...noteProps}
/>
{group.activity.map(item => {
const authorName = item.user ? item.user.name : 'Sentry';
const {title, message} = getGroupActivityItem(
item,
organization,
group.project.id,
<Author>{authorName}</Author>,
teams
);

const Icon = groupActivityTypeIconMapping[item.type]?.Component ?? null;

return (
{(group.activity.length < 5 || showAll) &&
group.activity.map(item => {
return (
<TimelineItem
item={item}
handleDelete={handleDelete}
group={group}
teams={teams}
key={item.id}
/>
);
})}
{!showAll && group.activity.length >= 5 && (
<Fragment>
{group.activity.slice(0, 2).map(item => {
return (
<TimelineItem
item={item}
handleDelete={handleDelete}
group={group}
teams={teams}
key={item.id}
/>
);
})}
<ActivityTimelineItem
title={
<TitleWrapper>
{title}
<NoteDropdownWrapper>
{item.type === GroupActivityType.NOTE && (
<NoteDropdown
onDelete={() => handleDelete(item)}
user={item.user}
/>
)}
</NoteDropdownWrapper>
</TitleWrapper>
<ShowAllButton
aria-label={t('Show all activity')}
onClick={() => setShowAll(true)}
borderless
size="zero"
>
{t('%s comments hidden', group.activity.length - 3)}
</ShowAllButton>
}
timestamp={<SmallTimestamp date={item.dateCreated} />}
icon={
Icon && (
<Icon
{...groupActivityTypeIconMapping[item.type].defaultProps}
size="xs"
/>
)
}
key={item.id}
>
{typeof message === 'string' ? <NoteBody text={message} /> : message}
</ActivityTimelineItem>
);
})}
icon={<RotatedEllipsisIcon />}
/>
<TimelineItem
item={group.activity[group.activity.length - 1]}
handleDelete={handleDelete}
group={group}
teams={teams}
key={group.activity[group.activity.length - 1].id}
/>
</Fragment>
)}
</Timeline.Container>
</div>
);
Expand Down Expand Up @@ -172,9 +230,24 @@ const SmallTimestamp = styled(TimeSince)`
font-size: ${p => p.theme.fontSizeSmall};
`;

export default StreamlinedActivitySection;
const ShowAllButton = styled(Button)`
font-size: ${p => p.theme.fontSizeSmall};
color: ${p => p.theme.subText};
font-weight: ${p => p.theme.fontWeightNormal};
`;

const TitleSection = styled('div')`
display: flex;
flex-direction: row;
justify-content: space-between;
`;

const CollapseButton = styled(Button)`
font-weight: ${p => p.theme.fontWeightNormal};
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeSmall};
`;

const StyledSectionTitle = styled(SidebarSection.Title)`
margin-bottom: ${space(1)};
color: ${p => p.theme.headingColor};
const RotatedEllipsisIcon = styled(IconEllipsis)`
transform: rotate(90deg);
`;
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import Version from 'sentry/components/version';
import {t, tct} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Group} from 'sentry/types/group';
import type {Release} from 'sentry/types/release';
import {useApiQuery} from 'sentry/utils/queryClient';
import useOrganization from 'sentry/utils/useOrganization';
import type {GroupRelease} from 'sentry/views/issueDetails/streamline/activitySection';

export interface GroupRelease {
firstRelease: Release;
lastRelease: Release;
}

export default function FirstLastSeenSection({group}: {group: Group}) {
const organization = useOrganization();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,4 @@ export const groupActivityTypeIconMapping: Record<
},
[GroupActivityType.SET_PRIORITY]: {Component: IconEdit, defaultProps: {}},
[GroupActivityType.DELETED_ATTACHMENT]: {Component: IconDelete, defaultProps: {}},
[GroupActivityType.LAST_SEEN]: {Component: IconFlag, defaultProps: {}},
};

0 comments on commit 471ae95

Please sign in to comment.