Skip to content

Commit ce16fd6

Browse files
committed
feat(onboarding): Add paginated browse + search to repo selector
Pre-populate the repo dropdown on mount using useInfiniteApiQuery with per_page pagination, then switch to server-side search when the user types. Adds infinite scroll via onMenuScrollToBottom. Refs VDY-46
1 parent bc5a262 commit ce16fd6

File tree

2 files changed

+59
-18
lines changed

2 files changed

+59
-18
lines changed

static/app/views/onboarding/components/scmRepoSelector.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useMemo} from 'react';
1+
import {useCallback, useMemo} from 'react';
22

33
import {Select} from '@sentry/scraps/select';
44

@@ -9,6 +9,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
99
import {useOrganization} from 'sentry/utils/useOrganization';
1010

1111
import {ScmSearchControl} from './scmSearchControl';
12+
import {ScmVirtualizedMenuList} from './scmVirtualizedMenuList';
1213
import {useScmRepoSearch} from './useScmRepoSearch';
1314
import {useScmRepoSelection} from './useScmRepoSelection';
1415

@@ -27,6 +28,9 @@ export function ScmRepoSelector({integration}: ScmRepoSelectorProps) {
2728
isError,
2829
debouncedSearch,
2930
setSearch,
31+
hasNextPage,
32+
fetchNextPage,
33+
isFetchingNextPage,
3034
} = useScmRepoSearch(integration.id, selectedRepository);
3135

3236
const {busy, handleSelect, handleRemove} = useScmRepoSelection({
@@ -73,6 +77,12 @@ export function ScmRepoSelector({integration}: ScmRepoSelectorProps) {
7377
}
7478
}
7579

80+
const handleMenuScrollToBottom = useCallback(() => {
81+
if (hasNextPage && !isFetchingNextPage) {
82+
fetchNextPage();
83+
}
84+
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
85+
7686
function noOptionsMessage() {
7787
if (isError) {
7888
return t('Failed to search repositories. Please try again.');
@@ -82,7 +92,7 @@ export function ScmRepoSelector({integration}: ScmRepoSelectorProps) {
8292
'No repositories found. Check your installation permissions to ensure your integration has access.'
8393
);
8494
}
85-
return t('Type to search repositories');
95+
return t('No repositories found');
8696
}
8797

8898
return (
@@ -96,14 +106,15 @@ export function ScmRepoSelector({integration}: ScmRepoSelectorProps) {
96106
setSearch(value);
97107
}
98108
}}
109+
onMenuScrollToBottom={handleMenuScrollToBottom}
99110
// Disable client-side filtering; search is handled server-side.
100111
filterOption={() => true}
101112
noOptionsMessage={noOptionsMessage}
102-
isLoading={isFetching}
113+
isLoading={isFetching || isFetchingNextPage}
103114
isDisabled={busy}
104115
clearable
105116
searchable
106-
components={{Control: ScmSearchControl}}
117+
components={{Control: ScmSearchControl, MenuList: ScmVirtualizedMenuList}}
107118
/>
108119
);
109120
}

static/app/views/onboarding/components/useScmRepoSearch.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,41 @@ import {useMemo, useState} from 'react';
22

33
import type {IntegrationRepository, Repository} from 'sentry/types/integrations';
44
import {getApiUrl} from 'sentry/utils/api/getApiUrl';
5-
import {fetchDataQuery, useQuery} from 'sentry/utils/queryClient';
5+
import {fetchDataQuery, useInfiniteApiQuery, useQuery} from 'sentry/utils/queryClient';
66
import {useDebouncedValue} from 'sentry/utils/useDebouncedValue';
77
import {useOrganization} from 'sentry/utils/useOrganization';
88

99
interface ScmRepoSearchResult {
1010
repos: IntegrationRepository[];
1111
}
1212

13+
const PER_PAGE = 100;
14+
1315
export function useScmRepoSearch(integrationId: string, selectedRepo?: Repository) {
1416
const organization = useOrganization();
1517
const [search, setSearch] = useState('');
1618
const debouncedSearch = useDebouncedValue(search, 200);
1719

20+
const reposUrl = getApiUrl(
21+
'/organizations/$organizationIdOrSlug/integrations/$integrationId/repos/',
22+
{
23+
path: {
24+
organizationIdOrSlug: organization.slug,
25+
integrationId,
26+
},
27+
}
28+
);
29+
30+
// Browse: paginated fetch that fires on mount, pre-populates the dropdown
31+
const browseQuery = useInfiniteApiQuery<ScmRepoSearchResult>({
32+
queryKey: [{infinite: true, version: 'v1'}, reposUrl, {query: {per_page: PER_PAGE}}],
33+
staleTime: 30_000,
34+
});
35+
36+
// Search: fires when user types, returns full filtered result set
1837
const searchQuery = useQuery({
1938
queryKey: [
20-
getApiUrl(
21-
'/organizations/$organizationIdOrSlug/integrations/$integrationId/repos/',
22-
{
23-
path: {
24-
organizationIdOrSlug: organization.slug,
25-
integrationId,
26-
},
27-
}
28-
),
39+
reposUrl,
2940
{method: 'GET', query: {search: debouncedSearch, accessibleOnly: true}},
3041
] as const,
3142
queryFn: async context => {
@@ -37,11 +48,26 @@ export function useScmRepoSearch(integrationId: string, selectedRepo?: Repositor
3748
enabled: !!debouncedSearch,
3849
});
3950

51+
const isSearching = !!debouncedSearch;
52+
53+
// Flatten paginated browse results into a single list
54+
const browseRepos = useMemo(
55+
() => browseQuery.data?.pages.flatMap(page => page[0].repos) ?? [],
56+
[browseQuery.data]
57+
);
58+
59+
const searchRepos = useMemo(
60+
() => searchQuery.data?.[0]?.repos ?? [],
61+
[searchQuery.data]
62+
);
63+
64+
const activeRepos = isSearching ? searchRepos : browseRepos;
65+
4066
const selectedRepoSlug = selectedRepo?.externalSlug;
4167

4268
const {reposByIdentifier, dropdownItems} = useMemo(
4369
() =>
44-
(searchQuery.data?.[0]?.repos ?? []).reduce<{
70+
activeRepos.reduce<{
4571
dropdownItems: Array<{
4672
disabled: boolean;
4773
label: string;
@@ -63,15 +89,19 @@ export function useScmRepoSearch(integrationId: string, selectedRepo?: Repositor
6389
dropdownItems: [],
6490
}
6591
),
66-
[searchQuery.data, selectedRepoSlug]
92+
[activeRepos, selectedRepoSlug]
6793
);
6894

6995
return {
7096
reposByIdentifier,
7197
dropdownItems,
72-
isFetching: searchQuery.isFetching,
73-
isError: searchQuery.isError,
98+
isFetching: isSearching ? searchQuery.isFetching : browseQuery.isFetching,
99+
isError: isSearching ? searchQuery.isError : browseQuery.isError,
74100
debouncedSearch,
75101
setSearch,
102+
// Infinite scroll support
103+
hasNextPage: !isSearching && (browseQuery.hasNextPage ?? false),
104+
fetchNextPage: browseQuery.fetchNextPage,
105+
isFetchingNextPage: browseQuery.isFetchingNextPage,
76106
};
77107
}

0 commit comments

Comments
 (0)