Skip to content

Commit 03253e7

Browse files
authored
feat(code): add paginated github repo picker (#1854)
1 parent f40543e commit 03253e7

5 files changed

Lines changed: 312 additions & 20 deletions

File tree

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1292,10 +1292,43 @@ export class PostHogAPIClient {
12921292
async getGithubRepositories(
12931293
integrationId: string | number,
12941294
): Promise<string[]> {
1295+
const repositories: string[] = [];
1296+
let offset = 0;
1297+
1298+
while (true) {
1299+
const page = await this.getGithubRepositoriesPage(
1300+
integrationId,
1301+
offset,
1302+
500,
1303+
);
1304+
repositories.push(...page.repositories);
1305+
1306+
if (!page.hasMore) {
1307+
return repositories;
1308+
}
1309+
1310+
offset += page.repositories.length;
1311+
}
1312+
}
1313+
1314+
async getGithubRepositoriesPage(
1315+
integrationId: string | number,
1316+
offset: number,
1317+
limit: number,
1318+
search?: string,
1319+
): Promise<{
1320+
repositories: string[];
1321+
hasMore: boolean;
1322+
}> {
12951323
const teamId = await this.getTeamId();
12961324
const url = new URL(
12971325
`${this.api.baseUrl}/api/environments/${teamId}/integrations/${integrationId}/github_repos/`,
12981326
);
1327+
url.searchParams.set("offset", String(offset));
1328+
url.searchParams.set("limit", String(limit));
1329+
if (search?.trim()) {
1330+
url.searchParams.set("search", search.trim());
1331+
}
12991332
const response = await this.api.fetcher.fetch({
13001333
method: "get",
13011334
url,
@@ -1309,7 +1342,10 @@ export class PostHogAPIClient {
13091342
}
13101343

13111344
const data = await response.json();
1312-
return this.normalizeGithubRepositories(data);
1345+
return {
1346+
repositories: this.normalizeGithubRepositories(data),
1347+
hasMore: data.has_more ?? false,
1348+
};
13131349
}
13141350

13151351
async refreshGithubRepositories(

apps/code/src/renderer/features/folder-picker/components/GitHubRepoPicker.tsx

Lines changed: 105 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import {
88
ComboboxInput,
99
ComboboxItem,
1010
ComboboxList,
11+
ComboboxListFooter,
1112
ComboboxTrigger,
1213
} from "@posthog/quill";
13-
import { type RefObject, useEffect, useRef, useState } from "react";
14+
import { defaultFilter } from "cmdk";
15+
import { type RefObject, useEffect, useMemo, useRef, useState } from "react";
1416

15-
const COMBOBOX_LIMIT = 50;
17+
const COMBOBOX_INITIAL_LIMIT = 50;
1618

1719
interface GitHubRepoPickerProps {
1820
value: string | null;
@@ -27,6 +29,12 @@ interface GitHubRepoPickerProps {
2729
showSearchInput?: boolean;
2830
onRefresh?: () => void;
2931
isRefreshing?: boolean;
32+
open?: boolean;
33+
onOpenChange?: (open: boolean) => void;
34+
searchQuery?: string;
35+
onSearchQueryChange?: (value: string) => void;
36+
hasMore?: boolean;
37+
onLoadMore?: () => void;
3038
}
3139

3240
export function GitHubRepoPicker({
@@ -40,18 +48,47 @@ export function GitHubRepoPicker({
4048
showSearchInput = true,
4149
onRefresh,
4250
isRefreshing = false,
51+
open: controlledOpen,
52+
onOpenChange,
53+
searchQuery: controlledSearchQuery,
54+
onSearchQueryChange,
55+
hasMore: controlledHasMore,
56+
onLoadMore,
4357
}: GitHubRepoPickerProps) {
4458
const triggerRef = useRef<HTMLButtonElement>(null);
45-
const [searchQuery, setSearchQuery] = useState("");
59+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
60+
const [uncontrolledSearchQuery, setUncontrolledSearchQuery] = useState("");
61+
const [visibleLimit, setVisibleLimit] = useState(COMBOBOX_INITIAL_LIMIT);
62+
const open = controlledOpen ?? uncontrolledOpen;
63+
const searchQuery = controlledSearchQuery ?? uncontrolledSearchQuery;
64+
const remoteMode =
65+
controlledSearchQuery !== undefined ||
66+
onSearchQueryChange !== undefined ||
67+
controlledHasMore !== undefined ||
68+
onLoadMore !== undefined;
69+
const showInlineLoadingState = remoteMode && open && isLoading;
4670
const onlyRepo = repositories.length === 1 ? repositories[0] : null;
71+
const trimmedSearchQuery = searchQuery.trim();
72+
const filteredRepositoryCount = useMemo(() => {
73+
if (!trimmedSearchQuery) {
74+
return repositories.length;
75+
}
76+
77+
return repositories.reduce(
78+
(count, repo) =>
79+
count + (defaultFilter(repo, trimmedSearchQuery) > 0 ? 1 : 0),
80+
0,
81+
);
82+
}, [repositories, trimmedSearchQuery]);
83+
const hasMore = controlledHasMore ?? filteredRepositoryCount > visibleLimit;
4784

4885
useEffect(() => {
4986
if (onlyRepo && value !== onlyRepo) {
5087
onChange(onlyRepo);
5188
}
5289
}, [onlyRepo, value, onChange]);
5390

54-
if (isLoading) {
91+
if (isLoading && !showInlineLoadingState) {
5592
return (
5693
<Button variant="outline" disabled size="sm">
5794
<GithubLogo size={16} weight="regular" style={{ flexShrink: 0 }} />
@@ -60,7 +97,7 @@ export function GitHubRepoPicker({
6097
);
6198
}
6299

63-
if (repositories.length === 0) {
100+
if (repositories.length === 0 && !showInlineLoadingState) {
64101
return (
65102
<Button variant="outline" disabled size="sm">
66103
<GithubLogo size={16} weight="regular" style={{ flexShrink: 0 }} />
@@ -92,13 +129,27 @@ export function GitHubRepoPicker({
92129
return (
93130
<Combobox
94131
items={repositories}
95-
limit={COMBOBOX_LIMIT}
132+
limit={visibleLimit}
96133
value={value}
97134
onValueChange={(v) => {
98135
onChange(v ? (v as string) : null);
99136
}}
137+
open={open}
138+
onOpenChange={(nextOpen) => {
139+
setUncontrolledOpen(nextOpen);
140+
onOpenChange?.(nextOpen);
141+
if (!nextOpen) {
142+
setUncontrolledSearchQuery("");
143+
onSearchQueryChange?.("");
144+
setVisibleLimit(COMBOBOX_INITIAL_LIMIT);
145+
}
146+
}}
100147
inputValue={searchQuery}
101-
onInputValueChange={setSearchQuery}
148+
onInputValueChange={(nextSearchQuery) => {
149+
setUncontrolledSearchQuery(nextSearchQuery);
150+
onSearchQueryChange?.(nextSearchQuery);
151+
setVisibleLimit(COMBOBOX_INITIAL_LIMIT);
152+
}}
102153
disabled={disabled}
103154
>
104155
<ComboboxTrigger
@@ -150,7 +201,11 @@ export function GitHubRepoPicker({
150201
) : null}
151202
</div>
152203
) : null}
153-
<ComboboxEmpty>No repositories found.</ComboboxEmpty>
204+
<ComboboxEmpty>
205+
{showInlineLoadingState
206+
? "Loading repositories..."
207+
: "No repositories found."}
208+
</ComboboxEmpty>
154209
<ComboboxList>
155210
{(repo: string) => (
156211
<ComboboxItem key={repo} value={repo}>
@@ -159,12 +214,48 @@ export function GitHubRepoPicker({
159214
)}
160215
</ComboboxList>
161216

162-
{repositories.length > COMBOBOX_LIMIT && (
163-
<div className="px-2 py-1.5 text-center text-muted-foreground text-xs">
164-
{searchQuery
165-
? `Showing up to ${COMBOBOX_LIMIT} matches — refine your search`
166-
: `Showing ${COMBOBOX_LIMIT} of ${repositories.length} — type to filter`}
167-
</div>
217+
{(hasMore ||
218+
(remoteMode
219+
? repositories.length > COMBOBOX_INITIAL_LIMIT
220+
: filteredRepositoryCount > COMBOBOX_INITIAL_LIMIT)) && (
221+
<ComboboxListFooter>
222+
<div className="px-2 pb-2">
223+
<div className="px-1 pb-2 text-center text-muted-foreground text-xs">
224+
{remoteMode
225+
? trimmedSearchQuery
226+
? `Showing ${repositories.length}${hasMore ? "+" : ""} matches`
227+
: `Showing ${repositories.length}${hasMore ? "+" : ""} repositories`
228+
: trimmedSearchQuery
229+
? `Showing ${Math.min(visibleLimit, filteredRepositoryCount)} of ${filteredRepositoryCount} matches`
230+
: `Showing ${Math.min(visibleLimit, repositories.length)} of ${repositories.length}`}
231+
</div>
232+
{hasMore ? (
233+
<Button
234+
variant="outline"
235+
size="sm"
236+
className="w-full justify-center"
237+
onMouseDown={(event) => {
238+
event.preventDefault();
239+
event.stopPropagation();
240+
}}
241+
onClick={(event) => {
242+
event.preventDefault();
243+
event.stopPropagation();
244+
if (remoteMode) {
245+
onLoadMore?.();
246+
return;
247+
}
248+
249+
setVisibleLimit(
250+
(currentLimit) => currentLimit + COMBOBOX_INITIAL_LIMIT,
251+
);
252+
}}
253+
>
254+
Load more
255+
</Button>
256+
) : null}
257+
</div>
258+
</ComboboxListFooter>
168259
)}
169260
</ComboboxContent>
170261
</Combobox>

apps/code/src/renderer/features/inbox/components/DataSourceSetup.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { useAuthenticatedClient } from "@features/auth/hooks/authClient";
22
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
33
import { GitHubRepoPicker } from "@features/folder-picker/components/GitHubRepoPicker";
4-
import { useRepositoryIntegration } from "@hooks/useIntegrations";
4+
import {
5+
useGithubRepositories,
6+
useRepositoryIntegration,
7+
} from "@hooks/useIntegrations";
58
import { Box, Button, Flex, Text, TextField } from "@radix-ui/themes";
69
import { trpcClient } from "@renderer/trpc";
710
import { useCallback, useEffect, useRef, useState } from "react";
@@ -67,6 +70,14 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
6770
refreshRepositories,
6871
hasGithubIntegration,
6972
} = useRepositoryIntegration();
73+
const [repoPickerSearchQuery, setRepoPickerSearchQuery] = useState("");
74+
const [isRepoPickerOpen, setIsRepoPickerOpen] = useState(false);
75+
const {
76+
repositories: visibleRepositories,
77+
isPending: visibleRepositoriesLoading,
78+
hasMore: visibleRepositoriesHasMore,
79+
loadMore: loadMoreVisibleRepositories,
80+
} = useGithubRepositories(repoPickerSearchQuery, isRepoPickerOpen);
7081
const [repo, setRepo] = useState<string | null>(null);
7182
const [loading, setLoading] = useState(false);
7283
const [connecting, setConnecting] = useState(false);
@@ -194,6 +205,21 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
194205
});
195206
}, [refreshRepositories]);
196207

208+
const handleRepoPickerOpenChange = useCallback((open: boolean) => {
209+
setIsRepoPickerOpen(open);
210+
if (!open) {
211+
setRepoPickerSearchQuery("");
212+
}
213+
}, []);
214+
215+
const handleRepoPickerSearchChange = useCallback((value: string) => {
216+
setRepoPickerSearchQuery(value);
217+
}, []);
218+
219+
const handleLoadMoreRepositories = useCallback(() => {
220+
loadMoreVisibleRepositories();
221+
}, [loadMoreVisibleRepositories]);
222+
197223
if (!hasGithubIntegration) {
198224
return (
199225
<SetupFormContainer title="Connect GitHub">
@@ -229,10 +255,18 @@ function GitHubSetup({ onComplete, onCancel }: SetupFormProps) {
229255
<GitHubRepoPicker
230256
value={repo}
231257
onChange={setRepo}
232-
repositories={repositories}
233-
isLoading={isLoadingRepos}
258+
repositories={isRepoPickerOpen ? visibleRepositories : repositories}
259+
isLoading={
260+
isLoadingRepos || (isRepoPickerOpen && visibleRepositoriesLoading)
261+
}
234262
isRefreshing={isRefreshingRepos}
235263
onRefresh={handleRefreshRepositories}
264+
open={isRepoPickerOpen}
265+
onOpenChange={handleRepoPickerOpenChange}
266+
searchQuery={repoPickerSearchQuery}
267+
onSearchQueryChange={handleRepoPickerSearchChange}
268+
hasMore={visibleRepositoriesHasMore}
269+
onLoadMore={handleLoadMoreRepositories}
236270
placeholder="Select repository..."
237271
size="2"
238272
/>

apps/code/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { useAutoFocusOnTyping } from "@hooks/useAutoFocusOnTyping";
2424
import { useConnectivity } from "@hooks/useConnectivity";
2525
import {
2626
useGithubBranches,
27+
useGithubRepositories,
2728
useRepositoryIntegration,
2829
} from "@hooks/useIntegrations";
2930
import { ButtonGroup } from "@posthog/quill";
@@ -79,6 +80,8 @@ export function TaskInput({
7980
const [isDraggingFile, setIsDraggingFile] = useState(false);
8081
const [isCreatingBranch, setIsCreatingBranch] = useState(false);
8182
const [selectedBranch, setSelectedBranch] = useState<string | null>(null);
83+
const [cloudRepoSearchQuery, setCloudRepoSearchQuery] = useState("");
84+
const [isCloudRepoPickerOpen, setIsCloudRepoPickerOpen] = useState(false);
8285
const [cloudBranchSearchQuery, setCloudBranchSearchQuery] = useState("");
8386
const [isCloudBranchPickerOpen, setIsCloudBranchPickerOpen] = useState(false);
8487
const [selectedEnvironment, setSelectedEnvironmentRaw] = useState<
@@ -114,6 +117,12 @@ export function TaskInput({
114117
isRefreshingRepos,
115118
refreshRepositories,
116119
} = useRepositoryIntegration();
120+
const {
121+
repositories: visibleCloudRepositories,
122+
isPending: cloudRepositoriesLoading,
123+
hasMore: cloudRepositoriesHasMore,
124+
loadMore: loadMoreCloudRepositories,
125+
} = useGithubRepositories(cloudRepoSearchQuery, isCloudRepoPickerOpen);
117126
const [selectedRepository, setSelectedRepository] = useState<string | null>(
118127
() => lastUsedCloudRepository?.toLowerCase() ?? null,
119128
);
@@ -219,6 +228,21 @@ export function TaskInput({
219228
setIsCloudBranchPickerOpen(true);
220229
}, []);
221230

231+
const handleCloudRepoPickerOpenChange = useCallback((open: boolean) => {
232+
setIsCloudRepoPickerOpen(open);
233+
if (!open) {
234+
setCloudRepoSearchQuery("");
235+
}
236+
}, []);
237+
238+
const handleCloudRepoSearchChange = useCallback((value: string) => {
239+
setCloudRepoSearchQuery(value);
240+
}, []);
241+
242+
const handleLoadMoreCloudRepositories = useCallback(() => {
243+
loadMoreCloudRepositories();
244+
}, [loadMoreCloudRepositories]);
245+
222246
const handleCloudBranchPickerClose = useCallback(() => {
223247
setIsCloudBranchPickerOpen(false);
224248
setCloudBranchSearchQuery("");
@@ -521,10 +545,23 @@ export function TaskInput({
521545
<GitHubRepoPicker
522546
value={selectedRepository}
523547
onChange={handleRepositorySelect}
524-
repositories={repositories}
525-
isLoading={isLoadingRepos}
548+
repositories={
549+
isCloudRepoPickerOpen
550+
? visibleCloudRepositories
551+
: repositories
552+
}
553+
isLoading={
554+
isLoadingRepos ||
555+
(isCloudRepoPickerOpen && cloudRepositoriesLoading)
556+
}
526557
isRefreshing={isRefreshingRepos}
527558
onRefresh={handleRefreshRepositories}
559+
open={isCloudRepoPickerOpen}
560+
onOpenChange={handleCloudRepoPickerOpenChange}
561+
searchQuery={cloudRepoSearchQuery}
562+
onSearchQueryChange={handleCloudRepoSearchChange}
563+
hasMore={cloudRepositoriesHasMore}
564+
onLoadMore={handleLoadMoreCloudRepositories}
528565
placeholder="Select repository..."
529566
size="1"
530567
disabled={isCreatingTask}

0 commit comments

Comments
 (0)