Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 37 additions & 29 deletions static/app/views/issueList/supergroups/supergroupDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,24 +153,10 @@ function SupergroupIssueList({
end,
} = location.query;
const query = typeof searchQuery === 'string' ? searchQuery : '';
const totalPages = Math.ceil(groupIds.length / PAGE_SIZE);
const pageGroupIds = groupIds.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
const issueIdFilter = `issue.id:[${pageGroupIds.join(',')}]`;

// Fetch all groups on this page
const {data: allGroups, isPending: allPending} = useQuery(
apiOptions.as<Group[]>()('/organizations/$organizationIdOrSlug/issues/', {
path: {organizationIdOrSlug: organization.slug},
query: {
group: pageGroupIds.map(String),
project: ALL_ACCESS_PROJECTS,
},
staleTime: 30_000,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The query for matched issues is limited by per_page: PAGE_SIZE, so if more than 25 issues match, only the first 25 are hoisted to the top.
Severity: MEDIUM

Suggested Fix

Remove the per_page: PAGE_SIZE parameter from the API query that fetches the matchedIds. This will ensure that all matching issue IDs from the full groupIds list are retrieved, allowing the sorting logic to correctly hoist all matched issues to the beginning of the list before pagination is applied.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: static/app/views/issueList/supergroups/supergroupDrawer.tsx#L168

Potential issue: The logic to hoist issues matching a stream query is incomplete. The
API call to find these matched issues is limited by `per_page: PAGE_SIZE`, which is 25.
If more than 25 issues match the query across all `groupIds`, only the first 25 are
identified and hoisted to the top of the list. Any matched issues beyond this initial
set will not be sorted to the top and will remain scattered across subsequent pages.
This contradicts the intended behavior of sorting all matched issues first before
applying pagination.

Did we get this right? 👍 / 👎 to inform future reviews.

})
);

// Search with the stream query to find which ones match
const {data: matchedGroups} = useQuery({
// Search with the stream query across all member issues so matched issues
// can be hoisted to page 1 before pagination.
const {data: matchedGroups, isPending: matchedPending} = useQuery({
...apiOptions.as<Group[]>()('/organizations/$organizationIdOrSlug/issues/', {
path: {organizationIdOrSlug: organization.slug},
query: {
Expand All @@ -179,14 +165,39 @@ function SupergroupIssueList({
statsPeriod,
start,
end,
query: `${query} ${issueIdFilter}`,
query: `${query} issue.id:[${groupIds.join(',')}]`,
per_page: PAGE_SIZE,
},
staleTime: 30_000,
}),
enabled: !!filterWithCurrentSearch,
});

if (allPending) {
const matchedIds = new Set(matchedGroups?.map(g => g.id));
const sortedGroupIds = [...groupIds].sort(
(a, b) => Number(matchedIds.has(String(b))) - Number(matchedIds.has(String(a)))
);

const totalPages = Math.ceil(sortedGroupIds.length / PAGE_SIZE);
const safePage = Math.min(page, totalPages - 1);
const pageGroupIds = sortedGroupIds.slice(
safePage * PAGE_SIZE,
(safePage + 1) * PAGE_SIZE
);

// Fetch all groups on this page
const {data: allGroups, isPending: allPending} = useQuery(
apiOptions.as<Group[]>()('/organizations/$organizationIdOrSlug/issues/', {
path: {organizationIdOrSlug: organization.slug},
query: {
group: pageGroupIds.map(String),
project: ALL_ACCESS_PROJECTS,
},
staleTime: 30_000,
})
);

if (allPending || (filterWithCurrentSearch && matchedPending)) {
return (
<Fragment>
{filterWithCurrentSearch && (
Expand All @@ -212,14 +223,11 @@ function SupergroupIssueList({
);
}

const matchedIds = new Set(matchedGroups?.map(g => g.id));
const groupMap = new Map(allGroups?.map(g => [g.id, g]));

// Sort: matched first, then the rest
const sortedGroups = [...pageGroupIds]
const sortedGroups = pageGroupIds
.map(id => groupMap.get(String(id)))
.filter((g): g is Group => g !== undefined)
.sort((a, b) => Number(matchedIds.has(b.id)) - Number(matchedIds.has(a.id)));
.filter((g): g is Group => g !== undefined);

const visibleGroupIds = sortedGroups.map(g => g.id);

Expand Down Expand Up @@ -266,22 +274,22 @@ function SupergroupIssueList({
{totalPages > 1 && (
<Flex justify="end" align="center" gap="sm" padding="md 0">
<Text size="sm" variant="muted">
{`${page * PAGE_SIZE + 1}-${Math.min((page + 1) * PAGE_SIZE, groupIds.length)} of ${groupIds.length}`}
{`${safePage * PAGE_SIZE + 1}-${Math.min((safePage + 1) * PAGE_SIZE, sortedGroupIds.length)} of ${sortedGroupIds.length}`}
</Text>
<Flex gap="xs">
<Button
size="xs"
icon={<IconChevron direction="left" />}
aria-label={t('Previous')}
disabled={page === 0}
onClick={() => setPage(p => p - 1)}
disabled={safePage === 0}
onClick={() => setPage(safePage - 1)}
/>
<Button
size="xs"
icon={<IconChevron direction="right" />}
aria-label={t('Next')}
disabled={page >= totalPages - 1}
onClick={() => setPage(p => p + 1)}
disabled={safePage >= totalPages - 1}
onClick={() => setPage(safePage + 1)}
/>
</Flex>
</Flex>
Expand Down
Loading