mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 11:33:14 +00:00
feat: Better support for pagination in gh branch dropdown (#430)
* feat: Better support for pagination in gh branch dropdown * cr * cr
This commit is contained in:
parent
7a06b37bd8
commit
7e5a5a7947
3 changed files with 172 additions and 20 deletions
|
|
@ -18,6 +18,8 @@ import { useState, useEffect } from "react";
|
|||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import { GitBranch, Shield } from "lucide-react";
|
||||
import { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
import { Branch } from "@/utils/github";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface BranchSelectorProps {
|
||||
disabled?: boolean;
|
||||
|
|
@ -27,6 +29,21 @@ interface BranchSelectorProps {
|
|||
streamTargetRepository?: TargetRepository;
|
||||
}
|
||||
|
||||
const findDefaultBranch = (
|
||||
branches: Branch[],
|
||||
defaultBranch: string | null,
|
||||
) => {
|
||||
const actualDefaultBranch = defaultBranch
|
||||
? branches.find((branch) => branch.name === defaultBranch)
|
||||
: null;
|
||||
|
||||
return actualDefaultBranch
|
||||
? actualDefaultBranch.name
|
||||
: branches.length > 0
|
||||
? branches[0].name
|
||||
: null;
|
||||
};
|
||||
|
||||
export function BranchSelector({
|
||||
disabled = false,
|
||||
placeholder = "Select a branch...",
|
||||
|
|
@ -35,6 +52,8 @@ export function BranchSelector({
|
|||
streamTargetRepository,
|
||||
}: BranchSelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const {
|
||||
branches,
|
||||
branchesLoading,
|
||||
|
|
@ -45,6 +64,7 @@ export function BranchSelector({
|
|||
branchesHasMore,
|
||||
branchesLoadingMore,
|
||||
loadMoreBranches,
|
||||
searchForBranch,
|
||||
defaultBranch,
|
||||
} = useGitHubAppProvider();
|
||||
|
||||
|
|
@ -61,19 +81,16 @@ export function BranchSelector({
|
|||
selectedBranch &&
|
||||
branches.some((branch) => branch.name === selectedBranch);
|
||||
|
||||
// Auto-select default branch if no branch is selected OR if the selected branch doesn't exist in this repo
|
||||
if (!selectedBranch || !currentBranchExists) {
|
||||
// Try to find the repository's actual default branch first
|
||||
const actualDefaultBranch = defaultBranch
|
||||
? branches.find((branch) => branch.name === defaultBranch)
|
||||
: null;
|
||||
|
||||
if (actualDefaultBranch) {
|
||||
setSelectedBranch(actualDefaultBranch.name);
|
||||
} else if (branches.length > 0) {
|
||||
// If default branch doesn't exist in branches list, select the first available branch
|
||||
setSelectedBranch(branches[0].name);
|
||||
}
|
||||
if (selectedBranch && !currentBranchExists) {
|
||||
searchForBranch(selectedBranch).then((b) => {
|
||||
if (b) {
|
||||
// branch was found after search. can return early
|
||||
return;
|
||||
}
|
||||
setSelectedBranch(findDefaultBranch(branches, defaultBranch));
|
||||
});
|
||||
} else if (!selectedBranch || !currentBranchExists) {
|
||||
setSelectedBranch(findDefaultBranch(branches, defaultBranch));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
@ -92,6 +109,20 @@ export function BranchSelector({
|
|||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleSearchForBranch = async () => {
|
||||
if (!searchQuery.trim() || !selectedRepository) return;
|
||||
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const foundBranch = await searchForBranch(searchQuery.trim());
|
||||
if (!foundBranch) {
|
||||
toast.warning(`Branch "${searchQuery.trim()}" not found`);
|
||||
}
|
||||
} finally {
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!selectedRepository) {
|
||||
return (
|
||||
<Button
|
||||
|
|
@ -106,7 +137,7 @@ export function BranchSelector({
|
|||
);
|
||||
}
|
||||
|
||||
if (branchesLoading) {
|
||||
if (branchesLoading && !branches.length) {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -198,9 +229,32 @@ export function BranchSelector({
|
|||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[340px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search branches..." />
|
||||
<CommandInput
|
||||
placeholder="Search branches..."
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No branches found.</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
<div className="flex flex-col items-center gap-2 py-4">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
No branches found.
|
||||
</span>
|
||||
{searchQuery.trim() && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSearchForBranch}
|
||||
disabled={isSearching}
|
||||
className="text-xs"
|
||||
>
|
||||
{isSearching
|
||||
? "Searching..."
|
||||
: `Search for "${searchQuery.trim()}"`}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{branches
|
||||
.slice()
|
||||
|
|
@ -244,6 +298,26 @@ export function BranchSelector({
|
|||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{/* Show this search button if there is a search query, and there are some results. this is for
|
||||
cases when some results do show, just not the exact result the user is looking for */}
|
||||
{searchQuery.trim() &&
|
||||
branches.some((branch) =>
|
||||
branch.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
) && (
|
||||
<div className="px-2 py-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleSearchForBranch}
|
||||
disabled={isSearching}
|
||||
className="w-full text-xs"
|
||||
>
|
||||
{isSearching
|
||||
? "Searching..."
|
||||
: `Search for "${searchQuery.trim()}"`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{branchesHasMore && (
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { useState, useEffect, useRef, useCallback, useMemo } from "react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Repository, getRepositoryBranches, Branch } from "@/utils/github";
|
||||
import {
|
||||
Repository,
|
||||
getRepositoryBranches,
|
||||
Branch,
|
||||
searchBranch,
|
||||
} from "@/utils/github";
|
||||
import { getRepository } from "@/utils/github";
|
||||
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
import {
|
||||
|
|
@ -88,6 +93,7 @@ interface UseGitHubAppReturn {
|
|||
selectedBranch: string | null;
|
||||
setSelectedBranch: (branch: string | null) => void;
|
||||
refreshBranches: () => Promise<void>;
|
||||
searchForBranch: (branchName: string) => Promise<Branch | null>;
|
||||
|
||||
// Repository metadata
|
||||
defaultBranch: string | null;
|
||||
|
|
@ -294,6 +300,39 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
}
|
||||
}, [branchesHasMore, branchesLoadingMore, branchesPage, fetchBranches]);
|
||||
|
||||
const searchForBranch = useCallback(
|
||||
async (branchName: string): Promise<Branch | null> => {
|
||||
if (!selectedRepository) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const branch = await searchBranch(
|
||||
selectedRepository.owner,
|
||||
selectedRepository.repo,
|
||||
branchName,
|
||||
);
|
||||
|
||||
if (branch) {
|
||||
// Add the found branch to the existing branches list if it's not already there
|
||||
setBranches((prev) => {
|
||||
const exists = prev.some((b) => b.name === branch.name);
|
||||
if (!exists) {
|
||||
return [...prev, branch];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
return branch;
|
||||
} catch (error) {
|
||||
console.error(`Error searching for branch ${branchName}:`, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[selectedRepository?.owner, selectedRepository?.repo],
|
||||
);
|
||||
|
||||
// Refresh repositories when installation changes
|
||||
useEffect(() => {
|
||||
if (currentInstallationId) {
|
||||
|
|
@ -483,6 +522,7 @@ export function useGitHubApp(): UseGitHubAppReturn {
|
|||
selectedBranch,
|
||||
setSelectedBranch,
|
||||
refreshBranches,
|
||||
searchForBranch,
|
||||
setBranchesPage,
|
||||
setBranches,
|
||||
|
||||
|
|
|
|||
|
|
@ -187,7 +187,10 @@ export async function getRepositoryBranches(
|
|||
/**
|
||||
* Fetches a specific repository using OAuth access token
|
||||
*/
|
||||
export async function getRepository(owner: string, repo: string) {
|
||||
export async function getRepository(
|
||||
owner: string,
|
||||
repo: string,
|
||||
): Promise<Repository> {
|
||||
const response = await fetch(
|
||||
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}`,
|
||||
{
|
||||
|
|
@ -203,8 +206,43 @@ export async function getRepository(owner: string, repo: string) {
|
|||
throw new Error(`Failed to fetch repository: ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a specific branch by name in a repository
|
||||
*/
|
||||
export async function searchBranch(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branchName: string,
|
||||
): Promise<Branch | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${getBaseApiUrl()}github/proxy/repos/${owner}/${repo}/branches/${encodeURIComponent(branchName)}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenSWE-Agent",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null; // Branch not found
|
||||
}
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
`Failed to search for branch: ${JSON.stringify(errorData)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
console.error(`Error searching for branch ${branchName}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue