Skip to content

Commit

Permalink
Fix GitHub repo selector: limit to 3 repos, sort by stars descending,…
Browse files Browse the repository at this point in the history
… resolve linting issues
  • Loading branch information
openhands-agent committed Dec 26, 2024
1 parent b21e0cd commit 61cf217
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 38 deletions.
3 changes: 2 additions & 1 deletion frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.14",
"typescript": "^5.6.3",
"typescript": "^5.7.2",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^1.6.0"
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/api/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,31 @@ export const retrieveLatestGitHubCommit = async (

return response.data[0];
};

export const searchPublicRepositories = async (
query: string,
per_page = 3,
sort: "" | "updated" | "stars" | "forks" = "stars",
order: "desc" | "asc" = "desc",
): Promise<GitHubRepository[]> => {
if (!query.trim()) {
return [];
}

try {
const response = await github.get<{ items: GitHubRepository[] }>(
"/search/repositories",
{
params: {
q: query,
per_page,
sort,
order,
},
},
);
return response.data.items;
} catch (error) {
return [];
}
};
112 changes: 76 additions & 36 deletions frontend/src/components/features/github/github-repo-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import { setSelectedRepository } from "#/state/initial-query-slice";
import { useConfig } from "#/hooks/query/use-config";
import { searchPublicRepositories } from "#/api/github";
import { useDebounce } from "#/hooks/use-debounce";

interface GitHubRepositoryWithFlag extends GitHubRepository {
fromPublicRepoSearch?: boolean;
stargazers_count?: number;
}

interface GitHubRepositorySelectorProps {
onSelect: () => void;
repositories: GitHubRepository[];
repositories: GitHubRepositoryWithFlag[];
}

export function GitHubRepositorySelector({
Expand All @@ -16,25 +23,41 @@ export function GitHubRepositorySelector({
}: GitHubRepositorySelectorProps) {
const { data: config } = useConfig();
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
const [searchQuery, setSearchQuery] = React.useState<string>("");
const [searchedRepos, setSearchedRepos] = React.useState<
GitHubRepositoryWithFlag[]
>([]);
const debouncedSearchQuery = useDebounce(searchQuery, 300);

React.useEffect(() => {
const searchPublicRepo = async () => {
if (!debouncedSearchQuery) {
setSearchedRepos([]);
return;
}
const repos = await searchPublicRepositories(debouncedSearchQuery);
const sortedRepos = repos.map((repo) => ({
...repo,
fromPublicRepoSearch: true,
}));
setSearchedRepos(sortedRepos);
};

// Add option to install app onto more repos
const finalRepositories =
config?.APP_MODE === "saas"
? [{ id: -1000, full_name: "Add more repositories..." }, ...repositories]
: repositories;
searchPublicRepo();
}, [debouncedSearchQuery]);

const finalRepositories: GitHubRepositoryWithFlag[] = [
...searchedRepos.filter(
(repo) => !repositories.find((r) => r.id === repo.id),
),
...repositories,
];

const dispatch = useDispatch();

const handleRepoSelection = (id: string | null) => {
const repo = finalRepositories.find((r) => r.id.toString() === id);
if (id === "-1000") {
if (config?.APP_SLUG)
window.open(
`https://github.com/apps/${config.APP_SLUG}/installations/new`,
"_blank",
);
} else if (repo) {
// set query param
if (repo) {
dispatch(setSelectedRepository(repo.full_name));
posthog.capture("repository_selected");
onSelect();
Expand All @@ -43,22 +66,10 @@ export function GitHubRepositorySelector({
};

const handleClearSelection = () => {
// clear query param
dispatch(setSelectedRepository(null));
};

const emptyContent = config?.APP_SLUG ? (
<a
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
target="_blank"
rel="noreferrer noopener"
className="underline"
>
Add more repositories...
</a>
) : (
"No results found."
);
const emptyContent = "No results found.";

return (
<Autocomplete
Expand All @@ -74,20 +85,49 @@ export function GitHubRepositorySelector({
},
}}
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
onInputChange={(value) => setSearchQuery(value)}
clearButtonProps={{ onClick: handleClearSelection }}
listboxProps={{
emptyContent,
}}
defaultFilter={(textValue, inputValue) =>
!inputValue ||
textValue.toLowerCase().includes(inputValue.toLowerCase())
}
>
{finalRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
>
{repo.full_name}
</AutocompleteItem>
))}
{() => (
<>
{config?.APP_MODE === "saas" && config?.APP_SLUG && (
<AutocompleteItem key="install">
<a
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
target="_blank"
rel="noreferrer noopener"
onClick={(e) => e.stopPropagation()}
>
Add more repositories...
</a>
</AutocompleteItem>
)}
{finalRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"
key={repo.id}
value={repo.id}
className="data-[selected=true]:bg-default-100"
textValue={repo.full_name}
>
{repo.full_name}
{repo.fromPublicRepoSearch &&
repo.stargazers_count !== undefined && (
<span className="ml-1 text-gray-400">
({repo.stargazers_count}⭐)
</span>
)}
</AutocompleteItem>
))}
</>
)}
</Autocomplete>
);
}
12 changes: 12 additions & 0 deletions frontend/src/hooks/use-debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useEffect, useState } from "react";

export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debouncedValue;
}

0 comments on commit 61cf217

Please sign in to comment.