fix(web): support cloning existing repositories

- Make the add repository dialog explicitly support cloning from a remote git server
- Keep blank repo creation as a fallback option
- Add validation and tests for the clone-first flow

Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
This commit is contained in:
2026-05-22 18:38:21 +02:00
parent cc1507a33e
commit ebfe991a15
2 changed files with 60 additions and 7 deletions
@@ -32,7 +32,7 @@ afterEach(() => {
}); });
describe("RepositoriesSettingsTab", () => { describe("RepositoriesSettingsTab", () => {
it("opens create dialog and submits a repository", async () => { it("opens create dialog and clones an existing repository", async () => {
const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]); const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]);
@@ -46,14 +46,34 @@ describe("RepositoriesSettingsTab", () => {
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), { fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
target: { value: "New Repo" }, target: { value: "New Repo" },
}); });
fireEvent.click(screen.getByRole("button", { name: /create/i })); fireEvent.change(screen.getByPlaceholderText(/github.com\/user\/repo.git/i), {
target: { value: "https://github.com/user/repo.git" },
});
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
await waitFor(() => { await waitFor(() => {
expect(createMock).toHaveBeenCalledWith("proj-1", { expect(createMock).toHaveBeenCalledWith("proj-1", {
name: "New Repo", name: "New Repo",
remote_url: undefined, remote_url: "https://github.com/user/repo.git",
}); });
}); });
expect(listMock).toHaveBeenCalledTimes(2); expect(listMock).toHaveBeenCalledTimes(2);
}); });
it("shows validation when cloning without a remote url", async () => {
vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories);
render(<RepositoriesSettingsTab />);
await waitFor(() => {
expect(screen.getByText("Main Repo")).toBeInTheDocument();
});
fireEvent.click(screen.getByRole("button", { name: /add repository/i }));
fireEvent.change(screen.getByPlaceholderText(/repository-name/i), {
target: { value: "New Repo" },
});
fireEvent.click(screen.getByRole("button", { name: /clone repository/i }));
expect(screen.getByText(/remote url is required/i)).toBeInTheDocument();
});
}); });
@@ -15,6 +15,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
const [repositories, setRepositories] = useState<GitRepository[]>([]); const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false); const [showCreate, setShowCreate] = useState(false);
const [createMode, setCreateMode] = useState<"clone" | "blank">("clone");
const [formName, setFormName] = useState(""); const [formName, setFormName] = useState("");
const [formRemoteUrl, setFormRemoteUrl] = useState(""); const [formRemoteUrl, setFormRemoteUrl] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -49,6 +50,10 @@ export const RepositoriesSettingsTab: React.FC = () => {
setError("Repository name is required"); setError("Repository name is required");
return; return;
} }
if (createMode === "clone" && !formRemoteUrl.trim()) {
setError("Remote URL is required to clone an existing repository");
return;
}
try { try {
const input: GitRepositoryCreate = { const input: GitRepositoryCreate = {
@@ -57,6 +62,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
}; };
await createRepository(projectId, input); await createRepository(projectId, input);
setShowCreate(false); setShowCreate(false);
setCreateMode("clone");
setFormName(""); setFormName("");
setFormRemoteUrl(""); setFormRemoteUrl("");
await loadRepositories(); await loadRepositories();
@@ -82,7 +88,11 @@ export const RepositoriesSettingsTab: React.FC = () => {
<div className="repositories-settings-tab"> <div className="repositories-settings-tab">
<div className="page-header"> <div className="page-header">
<h2>Repositories</h2> <h2>Repositories</h2>
<button className="primary-button" onClick={() => setShowCreate(true)} type="button"> <button
className="primary-button"
onClick={() => setShowCreate(true)}
type="button"
>
<Icon name="add" size="sm" /> <Icon name="add" size="sm" />
Add Repository Add Repository
</button> </button>
@@ -117,9 +127,32 @@ export const RepositoriesSettingsTab: React.FC = () => {
<div className="dialog-overlay" role="dialog" aria-modal="true"> <div className="dialog-overlay" role="dialog" aria-modal="true">
<div className="dialog"> <div className="dialog">
<h3>Add Repository</h3> <h3>Add Repository</h3>
<p className="muted">
Clone an existing repository from a git server, or create a blank bare repo here.
</p>
<form onSubmit={handleCreate} className="stack"> <form onSubmit={handleCreate} className="stack">
<div className="form-field">
<label>
<input
type="radio"
name="repository-mode"
checked={createMode === "clone"}
onChange={() => setCreateMode("clone")}
/>
Clone existing repository
</label>
<label>
<input
type="radio"
name="repository-mode"
checked={createMode === "blank"}
onChange={() => setCreateMode("blank")}
/>
Create blank repository
</label>
</div>
<label className="form-field"> <label className="form-field">
Name Repository name
<input <input
type="text" type="text"
value={formName} value={formName}
@@ -128,7 +161,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
/> />
</label> </label>
<label className="form-field"> <label className="form-field">
Remote URL (optional) Remote URL {createMode === "clone" ? "(required)" : "(optional)"}
<input <input
type="text" type="text"
value={formRemoteUrl} value={formRemoteUrl}
@@ -141,7 +174,7 @@ export const RepositoriesSettingsTab: React.FC = () => {
Cancel Cancel
</button> </button>
<button className="primary-button" type="submit"> <button className="primary-button" type="submit">
Create {createMode === "clone" ? "Clone Repository" : "Create Blank Repository"}
</button> </button>
</div> </div>
</form> </form>