diff --git a/apps/web/components/task-create-dialog-extra-repos.test.tsx b/apps/web/components/task-create-dialog-extra-repos.test.tsx new file mode 100644 index 0000000000..20630c0a65 --- /dev/null +++ b/apps/web/components/task-create-dialog-extra-repos.test.tsx @@ -0,0 +1,154 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { render, screen, fireEvent, cleanup } from "@testing-library/react"; + +afterEach(cleanup); +import type { Repository } from "@/lib/types/http"; +import type { DialogFormState, ExtraRepositoryRow } from "./task-create-dialog-types"; + +// Mock branches hook so the BranchSelector inside the row doesn't blow up. +vi.mock("@/hooks/domains/workspace/use-repository-branches", () => ({ + useRepositoryBranches: () => ({ branches: [], isLoading: false }), +})); + +// Defer the import until after mocks are set up. +async function loadComponent() { + return (await import("./task-create-dialog-extra-repos")).ExtraRepositoryRows; +} + +const REPO_FRONT_ID = "repo-front"; +const REPO_BACK_ID = "repo-back"; + +function makeRepo(id: string, name: string): Repository { + return { + id, + workspace_id: "ws-1", + name, + source_type: "local", + local_path: `/repos/${name}`, + default_branch: "main", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + } as Repository; +} + +function makeFs(overrides: Partial): DialogFormState { + // Only the fields ExtraRepositoryRows actually reads need to be real. + return { + repositoryId: REPO_FRONT_ID, + extraRepositories: [] as ExtraRepositoryRow[], + addExtraRepository: vi.fn(), + removeExtraRepository: vi.fn(), + updateExtraRepository: vi.fn(), + ...overrides, + } as unknown as DialogFormState; +} + +describe("ExtraRepositoryRows", () => { + it("disables the Add button until a primary repo is selected", async () => { + const ExtraRepositoryRows = await loadComponent(); + render( + , + ); + const btn = screen.getByTestId("add-extra-repository") as HTMLButtonElement; + expect(btn.disabled).toBe(true); + }); + + it("calls addExtraRepository when the Add button is clicked", async () => { + const ExtraRepositoryRows = await loadComponent(); + const fs = makeFs({}); + render( + , + ); + fireEvent.click(screen.getByTestId("add-extra-repository")); + expect(fs.addExtraRepository).toHaveBeenCalledOnce(); + }); + + it("renders one editable row per extra repository", async () => { + const ExtraRepositoryRows = await loadComponent(); + render( + , + ); + expect(screen.getAllByTestId("extra-repository-row")).toHaveLength(2); + }); + + it("hides the section when the task is already started", async () => { + const ExtraRepositoryRows = await loadComponent(); + const { container } = render( + , + ); + expect(container.querySelector("[data-testid='extra-repositories']")).toBeNull(); + }); + + it("opening a row's dropdown shows the unused workspace repos", async () => { + const ExtraRepositoryRows = await loadComponent(); + render( + , + ); + // Open the row's repository combobox. + const triggers = screen.getAllByTestId("repository-selector"); + fireEvent.click(triggers[0]); + // The primary repo (frontend) is excluded; the other two should be offered. + expect(screen.queryByText("backend")).not.toBeNull(); + expect(screen.queryByText("shared")).not.toBeNull(); + expect(screen.queryByText("frontend")).toBeNull(); + }); + + it("disables the Add button when no other workspace repos are available", async () => { + const ExtraRepositoryRows = await loadComponent(); + render( + , + ); + const btn = screen.getByTestId("add-extra-repository") as HTMLButtonElement; + expect(btn.disabled).toBe(true); + }); +}); diff --git a/apps/web/components/task-create-dialog-extra-repos.tsx b/apps/web/components/task-create-dialog-extra-repos.tsx index eab7b8e524..bbcc480d58 100644 --- a/apps/web/components/task-create-dialog-extra-repos.tsx +++ b/apps/web/components/task-create-dialog-extra-repos.tsx @@ -41,6 +41,14 @@ export function ExtraRepositoryRows({ // Hide the section in started/edit modes where the repo set is fixed. if (isTaskStarted) return null; + // Count workspace repositories the user could still add: total minus primary + // minus all already-chosen extras. When zero, we disable the Add button so + // the user gets a clear signal instead of a row with an empty dropdown. + const usedIds = collectUsedRepoIds(fs, ""); + const remainingCount = repositories.filter((r) => !usedIds.has(r.id)).length; + const canAddMore = primarySelected && remainingCount > 0; + const addButtonHint = computeAddButtonHint(primarySelected, remainingCount); + return (
{fs.extraRepositories.map((row) => ( @@ -60,7 +68,8 @@ export function ExtraRepositoryRows({ variant="ghost" size="sm" onClick={fs.addExtraRepository} - disabled={!primarySelected} + disabled={!canAddMore} + title={addButtonHint} className="text-xs cursor-pointer text-muted-foreground hover:text-foreground" data-testid="add-extra-repository" > @@ -71,6 +80,15 @@ export function ExtraRepositoryRows({ ); } +function computeAddButtonHint( + primarySelected: boolean, + remainingCount: number, +): string | undefined { + if (!primarySelected) return "Select the primary repository first"; + if (remainingCount === 0) return "All workspace repositories are already added"; + return undefined; +} + function computeBranchPlaceholder( repoId: string, branchesLoading: boolean,