Skip to content

Commit 8d59370

Browse files
cleptricclaude
andcommitted
feat(issueList): Add clickable column headers to change sort order
Allow users to click on column headers (Last Seen, Age, Trend, Events, Users) to update the sort order directly, instead of requiring them to use the separate sort dropdown selector. Active sort is highlighted and headers show hover state when clickable. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4a833d3 commit 8d59370

File tree

4 files changed

+160
-21
lines changed

4 files changed

+160
-21
lines changed

static/app/views/issueList/actions/headers.tsx

Lines changed: 140 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,25 @@ import {t} from 'sentry/locale';
88
import {space} from 'sentry/styles/space';
99
import type {PageFilters} from 'sentry/types/core';
1010
import {COLUMN_BREAKPOINTS} from 'sentry/views/issueList/actions/utils';
11+
import {IssueSortOptions} from 'sentry/views/issueList/utils';
1112

1213
type Props = {
1314
isReprocessingQuery: boolean;
1415
onSelectStatsPeriod: (statsPeriod: string) => void;
1516
selection: PageFilters;
1617
statsPeriod: string;
1718
isSavedSearchesOpen?: boolean;
19+
onSortChange?: (sort: string) => void;
20+
sort?: string;
1821
};
1922

2023
function Headers({
2124
selection,
2225
statsPeriod,
2326
onSelectStatsPeriod,
2427
isReprocessingQuery,
28+
onSortChange,
29+
sort,
2530
}: Props) {
2631
return (
2732
<Fragment>
@@ -33,15 +38,33 @@ function Headers({
3338
</Fragment>
3439
) : (
3540
<Fragment>
36-
<LastSeenLabel breakpoint={COLUMN_BREAKPOINTS.LAST_SEEN} align="right">
37-
{t('Last Seen')}
38-
</LastSeenLabel>
39-
<FirstSeenLabel breakpoint={COLUMN_BREAKPOINTS.FIRST_SEEN} align="right">
40-
{t('Age')}
41-
</FirstSeenLabel>
41+
<SortableHeader
42+
breakpoint={COLUMN_BREAKPOINTS.LAST_SEEN}
43+
align="right"
44+
sortOption={IssueSortOptions.DATE}
45+
currentSort={sort}
46+
onSortChange={onSortChange}
47+
label={t('Last Seen')}
48+
width="86px"
49+
/>
50+
<SortableHeader
51+
breakpoint={COLUMN_BREAKPOINTS.FIRST_SEEN}
52+
align="right"
53+
sortOption={IssueSortOptions.NEW}
54+
currentSort={sort}
55+
onSortChange={onSortChange}
56+
label={t('Age')}
57+
width="50px"
58+
/>
4259
<GraphLabel breakpoint={COLUMN_BREAKPOINTS.TREND}>
4360
<Flex flex="1" justify="between">
44-
{t('Trend')}
61+
<SortableHeaderText
62+
sortOption={IssueSortOptions.TRENDS}
63+
currentSort={sort}
64+
onSortChange={onSortChange}
65+
>
66+
{t('Trend')}
67+
</SortableHeaderText>
4568
<GraphToggles>
4669
{selection.datetime.period !== '24h' && (
4770
<GraphToggle
@@ -60,12 +83,24 @@ function Headers({
6083
</GraphToggles>
6184
</Flex>
6285
</GraphLabel>
63-
<EventsOrUsersLabel breakpoint={COLUMN_BREAKPOINTS.EVENTS} align="right">
64-
{t('Events')}
65-
</EventsOrUsersLabel>
66-
<EventsOrUsersLabel breakpoint={COLUMN_BREAKPOINTS.USERS} align="right">
67-
{t('Users')}
68-
</EventsOrUsersLabel>
86+
<SortableHeader
87+
breakpoint={COLUMN_BREAKPOINTS.EVENTS}
88+
align="right"
89+
sortOption={IssueSortOptions.FREQ}
90+
currentSort={sort}
91+
onSortChange={onSortChange}
92+
label={t('Events')}
93+
width="60px"
94+
/>
95+
<SortableHeader
96+
breakpoint={COLUMN_BREAKPOINTS.USERS}
97+
align="right"
98+
sortOption={IssueSortOptions.USER}
99+
currentSort={sort}
100+
onSortChange={onSortChange}
101+
label={t('Users')}
102+
width="60px"
103+
/>
69104
<PriorityLabel breakpoint={COLUMN_BREAKPOINTS.PRIORITY} align="left">
70105
{t('Priority')}
71106
</PriorityLabel>
@@ -78,6 +113,65 @@ function Headers({
78113
);
79114
}
80115

116+
function SortableHeader({
117+
breakpoint,
118+
align,
119+
sortOption,
120+
currentSort,
121+
onSortChange,
122+
label,
123+
width,
124+
}: {
125+
align: 'left' | 'right';
126+
label: string;
127+
sortOption: IssueSortOptions;
128+
width: string;
129+
breakpoint?: string;
130+
currentSort?: string;
131+
onSortChange?: (sort: string) => void;
132+
}) {
133+
const isActive = currentSort === sortOption;
134+
const isClickable = !!onSortChange;
135+
136+
return (
137+
<SortableLabel
138+
breakpoint={breakpoint}
139+
align={align}
140+
isActive={isActive}
141+
isClickable={isClickable}
142+
onClick={isClickable ? () => onSortChange(sortOption) : undefined}
143+
style={{width}}
144+
>
145+
{label}
146+
</SortableLabel>
147+
);
148+
}
149+
150+
function SortableHeaderText({
151+
sortOption,
152+
currentSort,
153+
onSortChange,
154+
children,
155+
}: {
156+
children: React.ReactNode;
157+
sortOption: IssueSortOptions;
158+
currentSort?: string;
159+
onSortChange?: (sort: string) => void;
160+
}) {
161+
const isActive = currentSort === sortOption;
162+
const isClickable = !!onSortChange;
163+
164+
return (
165+
<SortableText
166+
isActive={isActive}
167+
isClickable={isClickable}
168+
onClick={isClickable ? () => onSortChange(sortOption) : undefined}
169+
>
170+
{children}
171+
</SortableText>
172+
);
173+
}
174+
81175
export default Headers;
82176

83177
const GraphLabel = styled(IssueStreamHeaderLabel)`
@@ -106,16 +200,41 @@ const GraphToggle = styled('a')<{active: boolean}>`
106200
}
107201
`;
108202

109-
const LastSeenLabel = styled(IssueStreamHeaderLabel)`
110-
width: 86px;
111-
`;
112-
113-
const FirstSeenLabel = styled(IssueStreamHeaderLabel)`
114-
width: 50px;
203+
const SortableLabel = styled(IssueStreamHeaderLabel)<{
204+
isActive: boolean;
205+
isClickable: boolean;
206+
}>`
207+
${p =>
208+
p.isClickable &&
209+
`
210+
cursor: pointer;
211+
user-select: none;
212+
&:hover {
213+
color: ${p.theme.tokens.content.primary};
214+
}
215+
`}
216+
${p =>
217+
p.isActive &&
218+
`
219+
color: ${p.theme.tokens.content.primary};
220+
`}
115221
`;
116222

117-
const EventsOrUsersLabel = styled(IssueStreamHeaderLabel)`
118-
width: 60px;
223+
const SortableText = styled('span')<{isActive: boolean; isClickable: boolean}>`
224+
${p =>
225+
p.isClickable &&
226+
`
227+
cursor: pointer;
228+
user-select: none;
229+
&:hover {
230+
color: ${p.theme.tokens.content.primary};
231+
}
232+
`}
233+
${p =>
234+
p.isActive &&
235+
`
236+
color: ${p.theme.tokens.content.primary};
237+
`}
119238
`;
120239

121240
const PriorityLabel = styled(IssueStreamHeaderLabel)`

static/app/views/issueList/actions/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ type IssueListActionsProps = {
4848
selection: PageFilters;
4949
statsPeriod: string;
5050
onActionTaken?: (itemIds: string[], data: IssueUpdateData) => void;
51+
onSortChange?: (sort: string) => void;
52+
sort?: string;
5153
};
5254

5355
const animationProps: MotionNodeAnimationOptions = {
@@ -75,6 +77,8 @@ function ActionsBarPriority({
7577
isSavedSearchesOpen,
7678
statsPeriod,
7779
selection,
80+
onSortChange,
81+
sort,
7882
}: {
7983
allInQuerySelected: boolean;
8084
anySelected: boolean;
@@ -93,6 +97,8 @@ function ActionsBarPriority({
9397
selectedProjectSlug: string | undefined;
9498
selection: PageFilters;
9599
statsPeriod: string;
100+
onSortChange?: (sort: string) => void;
101+
sort?: string;
96102
}) {
97103
const shouldDisplayActions = anySelected && !narrowViewport;
98104

@@ -140,6 +146,8 @@ function ActionsBarPriority({
140146
statsPeriod={statsPeriod}
141147
isReprocessingQuery={displayReprocessingActions}
142148
isSavedSearchesOpen={isSavedSearchesOpen}
149+
onSortChange={onSortChange}
150+
sort={sort}
143151
/>
144152
</AnimatedHeaderItemsContainer>
145153
)}
@@ -159,6 +167,8 @@ function IssueListActions({
159167
query,
160168
selection,
161169
statsPeriod,
170+
onSortChange,
171+
sort,
162172
}: IssueListActionsProps) {
163173
const api = useApi();
164174
const queryClient = useQueryClient();
@@ -367,6 +377,8 @@ function IssueListActions({
367377
isSavedSearchesOpen={isSavedSearchesOpen}
368378
anySelected={anySelected}
369379
onSelectStatsPeriod={onSelectStatsPeriod}
380+
onSortChange={onSortChange}
381+
sort={sort}
370382
/>
371383
{!allResultsVisible && pageSelected && (
372384
<Alert system variant="warning" showIcon={false}>

static/app/views/issueList/issueListTable.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ interface IssueListTableProps {
3939
selection: PageFilters;
4040
statsLoading: boolean;
4141
statsPeriod: string;
42+
onSortChange?: (sort: string) => void;
43+
sort?: string;
4244
}
4345

4446
function IssueListTable({
@@ -63,6 +65,8 @@ function IssueListTable({
6365
paginationAnalyticsEvent,
6466
issuesSuccessfullyLoaded,
6567
pageSize,
68+
onSortChange,
69+
sort,
6670
}: IssueListTableProps) {
6771
const location = useLocation();
6872

@@ -99,6 +103,8 @@ function IssueListTable({
99103
groupIds={groupIds}
100104
allResultsVisible={allResultsVisible}
101105
displayReprocessingActions={displayReprocessingActions}
106+
onSortChange={onSortChange}
107+
sort={sort}
102108
/>
103109
)}
104110
<PanelBody>

static/app/views/issueList/overview.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,8 @@ function IssueListOverview({
917917
paginationAnalyticsEvent={paginationAnalyticsEvent}
918918
issuesSuccessfullyLoaded={issuesSuccessfullyLoaded}
919919
pageSize={MAX_ITEMS}
920+
onSortChange={onSortChange}
921+
sort={sort}
920922
/>
921923
</StyledMain>
922924
</StyledBody>

0 commit comments

Comments
 (0)