mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 23:38:08 -05:00
feat(frontend): Allow searching/filtering repositories. (#9791)
This commit is contained in:
@@ -80,6 +80,22 @@ describe("MicroagentManagement", () => {
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-04T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
full_name: "user/TestRepository",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-05T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
full_name: "org/AnotherRepo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "organization",
|
||||
pushed_at: "2021-10-06T12:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
const mockMicroagents: RepositoryMicroagent[] = [
|
||||
@@ -465,4 +481,272 @@ describe("MicroagentManagement", () => {
|
||||
expect(readyMessage).toBeInTheDocument();
|
||||
expect(descriptionMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Search functionality tests
|
||||
describe("Search functionality", () => {
|
||||
it("should render search input field", async () => {
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Check that search input is rendered
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
expect(searchInput).toBeInTheDocument();
|
||||
expect(searchInput).toHaveAttribute(
|
||||
"placeholder",
|
||||
"Search repositories...",
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter repositories when typing in search input", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Initially only repositories with .openhands should be visible
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo4")).not.toBeInTheDocument();
|
||||
|
||||
// Type in search input to filter further
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, "repo2");
|
||||
|
||||
// Only repo2 should be visible
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("org/repo3/.openhands"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo4")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/TestRepository")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("org/AnotherRepo")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should perform case-insensitive search", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with uppercase
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, "REPO2");
|
||||
|
||||
// repo2 should be visible (case-insensitive match)
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("org/repo3/.openhands"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter repositories by partial matches", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with partial match
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, "repo");
|
||||
|
||||
// All repositories with "repo" in the name should be visible
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("org/repo3/.openhands"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo4")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/TestRepository")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("org/AnotherRepo")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show all repositories when search input is cleared", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, "repo2");
|
||||
|
||||
// Only repo2 should be visible
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
|
||||
// Clear the search input
|
||||
await user.clear(searchInput);
|
||||
|
||||
// All repositories should be visible again (only those with .openhands)
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("org/repo3/.openhands"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo4")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/TestRepository")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("org/AnotherRepo")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty search results", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with non-existent repository name
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, "nonexistent");
|
||||
|
||||
// No repositories should be visible
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("user/repo2/.openhands"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("org/repo3/.openhands"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo4")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/TestRepository")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("org/AnotherRepo")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle special characters in search", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with special characters
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, ".openhands");
|
||||
|
||||
// Only repositories with .openhands should be visible
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo4")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should maintain accordion functionality with filtered results", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Filter to show only repo2
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, "repo2");
|
||||
|
||||
// Click on the filtered repository accordion
|
||||
const repoAccordion = screen.getByText("user/repo2/.openhands");
|
||||
await user.click(repoAccordion);
|
||||
|
||||
// Wait for microagents to be fetched
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.getRepositoryMicroagents).toHaveBeenCalledWith(
|
||||
"user",
|
||||
"repo2",
|
||||
);
|
||||
});
|
||||
|
||||
// Check that microagents are displayed
|
||||
const microagent1 = screen.getByText("test-microagent-1");
|
||||
const microagent2 = screen.getByText("test-microagent-2");
|
||||
|
||||
expect(microagent1).toBeInTheDocument();
|
||||
expect(microagent2).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle whitespace in search input", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Type in search input with leading/trailing whitespace
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
await user.type(searchInput, " repo2 ");
|
||||
|
||||
// repo2 should still be visible (whitespace should be trimmed)
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should update search results in real-time", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
await waitFor(() => {
|
||||
expect(OpenHands.retrieveUserGitRepositories).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const searchInput = screen.getByRole("textbox", {
|
||||
name: /search repositories/i,
|
||||
});
|
||||
|
||||
// Type "repo" - should show repo2
|
||||
await user.type(searchInput, "repo");
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
|
||||
// Add "2" to make it "repo2" - should show only repo2
|
||||
await user.type(searchInput, "2");
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
|
||||
// Remove "2" to make it "repo" again - should show repo2
|
||||
await user.keyboard("{Backspace}");
|
||||
expect(screen.getByText("user/repo2/.openhands")).toBeInTheDocument();
|
||||
expect(screen.queryByText("user/repo1")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export function MicroagentManagementAccordionTitle({
|
||||
<div className="flex items-center gap-2">
|
||||
<GitProviderIcon gitProvider={repository.git_provider} />
|
||||
<div
|
||||
className="text-white text-base font-normal truncate max-w-[168px]"
|
||||
className="text-white text-base font-normal truncate max-w-[150px]"
|
||||
title={repository.full_name}
|
||||
>
|
||||
{repository.full_name}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Accordion, AccordionItem } from "@heroui/react";
|
||||
import { MicroagentManagementRepoMicroagents } from "./microagent-management-repo-microagents";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { getGitProviderBaseUrl } from "#/utils/utils";
|
||||
import { getGitProviderBaseUrl, cn } from "#/utils/utils";
|
||||
import { TabType } from "#/types/microagent-management";
|
||||
import { MicroagentManagementNoRepositories } from "./microagent-management-no-repositories";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { DOCUMENTATION_URL } from "#/utils/constants";
|
||||
import { MicroagentManagementAccordionTitle } from "./microagent-management-accordion-title";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
|
||||
type MicroagentManagementRepositoriesProps = {
|
||||
repositories: GitRepository[];
|
||||
@@ -19,9 +21,23 @@ export function MicroagentManagementRepositories({
|
||||
tabType,
|
||||
}: MicroagentManagementRepositoriesProps) {
|
||||
const { t } = useTranslation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const numberOfRepoMicroagents = repositories.length;
|
||||
|
||||
// Filter repositories based on search query
|
||||
const filteredRepositories = useMemo(() => {
|
||||
if (!searchQuery.trim()) {
|
||||
return repositories;
|
||||
}
|
||||
|
||||
const sanitizedQuery = sanitizeQuery(searchQuery);
|
||||
return repositories.filter((repository) => {
|
||||
const sanitizedRepoName = sanitizeQuery(repository.full_name);
|
||||
return sanitizedRepoName.includes(sanitizedQuery);
|
||||
});
|
||||
}, [repositories, searchQuery]);
|
||||
|
||||
if (numberOfRepoMicroagents === 0) {
|
||||
if (tabType === "personal") {
|
||||
return (
|
||||
@@ -56,30 +72,54 @@ export function MicroagentManagementRepositories({
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
variant="splitted"
|
||||
className="w-full px-0 gap-3"
|
||||
itemClasses={{
|
||||
base: "shadow-none bg-transparent border border-[#ffffff40] rounded-[6px] cursor-pointer",
|
||||
trigger: "cursor-pointer",
|
||||
}}
|
||||
selectionMode="multiple"
|
||||
>
|
||||
{repositories.map((repository) => (
|
||||
<AccordionItem
|
||||
key={repository.id}
|
||||
aria-label={repository.full_name}
|
||||
title={<MicroagentManagementAccordionTitle repository={repository} />}
|
||||
>
|
||||
<MicroagentManagementRepoMicroagents
|
||||
repoMicroagent={{
|
||||
id: repository.id,
|
||||
repositoryName: repository.full_name,
|
||||
repositoryUrl: `${getGitProviderBaseUrl(repository.git_provider)}/${repository.full_name}`,
|
||||
}}
|
||||
/>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
{/* Search Input */}
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<label htmlFor="repository-search" className="sr-only">
|
||||
{t(I18nKey.COMMON$SEARCH_REPOSITORIES)}
|
||||
</label>
|
||||
<input
|
||||
id="repository-search"
|
||||
name="repository-search"
|
||||
type="text"
|
||||
placeholder={`${t(I18nKey.COMMON$SEARCH_REPOSITORIES)}...`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={cn(
|
||||
"bg-tertiary border border-[#717888] bg-[#454545] w-full rounded-sm p-2 placeholder:italic placeholder:text-tertiary-alt",
|
||||
"disabled:bg-[#2D2F36] disabled:border-[#2D2F36] disabled:cursor-not-allowed",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Repositories Accordion */}
|
||||
<Accordion
|
||||
variant="splitted"
|
||||
className="w-full px-0 gap-3"
|
||||
itemClasses={{
|
||||
base: "shadow-none bg-transparent border border-[#ffffff40] rounded-[6px] cursor-pointer",
|
||||
trigger: "cursor-pointer",
|
||||
}}
|
||||
selectionMode="multiple"
|
||||
>
|
||||
{filteredRepositories.map((repository) => (
|
||||
<AccordionItem
|
||||
key={repository.id}
|
||||
aria-label={repository.full_name}
|
||||
title={
|
||||
<MicroagentManagementAccordionTitle repository={repository} />
|
||||
}
|
||||
>
|
||||
<MicroagentManagementRepoMicroagents
|
||||
repoMicroagent={{
|
||||
id: repository.id,
|
||||
repositoryName: repository.full_name,
|
||||
repositoryUrl: `${getGitProviderBaseUrl(repository.git_provider)}/${repository.full_name}`,
|
||||
}}
|
||||
/>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -711,4 +711,5 @@ export enum I18nKey {
|
||||
MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_USER_LEVEL_MICROAGENTS = "MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_USER_LEVEL_MICROAGENTS",
|
||||
MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_MICROAGENTS = "MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_MICROAGENTS",
|
||||
MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_ORGANIZATION_LEVEL_MICROAGENTS = "MICROAGENT_MANAGEMENT$YOU_DO_NOT_HAVE_ORGANIZATION_LEVEL_MICROAGENTS",
|
||||
COMMON$SEARCH_REPOSITORIES = "COMMON$SEARCH_REPOSITORIES",
|
||||
}
|
||||
|
||||
@@ -11197,7 +11197,7 @@
|
||||
"fr": "Que souhaitez-vous que le microagent fasse ?",
|
||||
"tr": "Mikro ajanın ne yapmasını istersiniz?",
|
||||
"de": "Was soll der Microagent tun?",
|
||||
"uk": "Що ви хочете, щоб зробив мікроагент?"
|
||||
"uk": "Що в,и хочете, щоб зробив мікроагент?"
|
||||
},
|
||||
"MICROAGENT_MANAGEMENT$DESCRIBE_WHAT_TO_DO": {
|
||||
"en": "Describe what you would like the Microagent to do.",
|
||||
@@ -11374,5 +11374,21 @@
|
||||
"tr": "Organizasyon düzeyinde mikro ajanınız yok",
|
||||
"de": "Sie haben keine Mikroagenten auf Organisationsebene",
|
||||
"uk": "У вас немає мікроагентів на рівні організації"
|
||||
},
|
||||
"COMMON$SEARCH_REPOSITORIES": {
|
||||
"en": "Search repositories",
|
||||
"ja": "リポジトリを検索",
|
||||
"zh-CN": "搜索仓库",
|
||||
"zh-TW": "搜尋存儲庫",
|
||||
"ko-KR": "저장소 검색",
|
||||
"no": "Søk i repositories",
|
||||
"it": "Cerca repository",
|
||||
"pt": "Pesquisar repositórios",
|
||||
"es": "Buscar repositorios",
|
||||
"ar": "البحث في المستودعات",
|
||||
"fr": "Rechercher des dépôts",
|
||||
"tr": "Depo ara",
|
||||
"de": "Repositorys durchsuchen",
|
||||
"uk": "Пошук репозиторіїв"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user