diff --git a/apps/web/src/components/repositories-settings-tab.test.tsx b/apps/web/src/components/repositories-settings-tab.test.tsx new file mode 100644 index 0000000..c223e4b --- /dev/null +++ b/apps/web/src/components/repositories-settings-tab.test.tsx @@ -0,0 +1,59 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { RepositoriesSettingsTab } from "./repositories-settings-tab"; +import * as gitRepositoriesApi from "../api/git_repositories"; + +const mockRepositories = [ + { + id: "repo-1", + name: "Main Repo", + path: "/repos/main", + project_id: "proj-1", + owner_id: "user-1", + is_mirror: false, + remote_url: null, + last_push: null, + created_at: null, + }, +]; + +vi.mock("react-router-dom", async () => { + const actual = await vi.importActual("react-router-dom"); + return { + ...actual, + useParams: () => ({ projectId: "proj-1" }), + }; +}); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe("RepositoriesSettingsTab", () => { + it("opens create dialog and submits a repository", async () => { + const listMock = vi.spyOn(gitRepositoriesApi, "listRepositories").mockResolvedValue(mockRepositories); + const createMock = vi.spyOn(gitRepositoriesApi, "createRepository").mockResolvedValue(mockRepositories[0]); + + render(); + + 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: /create/i })); + + await waitFor(() => { + expect(createMock).toHaveBeenCalledWith("proj-1", { + name: "New Repo", + remote_url: undefined, + }); + }); + expect(listMock).toHaveBeenCalledTimes(2); + }); +}); diff --git a/apps/web/src/components/repositories-settings-tab.tsx b/apps/web/src/components/repositories-settings-tab.tsx index 3b928de..7ff9a62 100644 --- a/apps/web/src/components/repositories-settings-tab.tsx +++ b/apps/web/src/components/repositories-settings-tab.tsx @@ -1,37 +1,77 @@ -import React, { useState, useEffect } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useParams } from "react-router-dom"; -import { apiClient } from "../api/client"; -import { GitRepository } from "../api/git_repositories"; + +import { + createRepository, + deleteRepository, + listRepositories, + type GitRepository, + type GitRepositoryCreate, +} from "../api/git_repositories"; +import { Icon } from "./icon"; export const RepositoriesSettingsTab: React.FC = () => { const { projectId } = useParams<{ projectId: string }>(); const [repositories, setRepositories] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(">"); + const [showCreate, setShowCreate] = useState(false); + const [formName, setFormName] = useState(""); + const [formRemoteUrl, setFormRemoteUrl] = useState(""); + const [error, setError] = useState(""); - useEffect(() => { - const fetchRepositories = async () => { - try { - const response = await apiClient.get( - `/projects/${projectId}/repositories` - ); - setRepositories(response.data); - } catch (err) { - setError("Failed to load repositories"); - } finally { - setLoading(false); - } - }; + const loadRepositories = useCallback(async () => { + if (!projectId) { + setLoading(false); + return; + } - fetchRepositories(); + setLoading(true); + try { + const data = await listRepositories(projectId); + setRepositories(data); + } catch { + setError("Failed to load repositories"); + } finally { + setLoading(false); + } }, [projectId]); + useEffect(() => { + void loadRepositories(); + }, [loadRepositories]); + + const handleCreate = async (event: React.FormEvent) => { + event.preventDefault(); + setError(""); + + if (!projectId) return; + if (!formName.trim()) { + setError("Repository name is required"); + return; + } + + try { + const input: GitRepositoryCreate = { + name: formName.trim(), + remote_url: formRemoteUrl.trim() || undefined, + }; + await createRepository(projectId, input); + setShowCreate(false); + setFormName(""); + setFormRemoteUrl(""); + await loadRepositories(); + } catch { + setError("Failed to create repository"); + } + }; + const handleDelete = async (repoId: string) => { + if (!projectId) return; if (!window.confirm("Are you sure you want to delete this repository?")) return; try { - await apiClient.delete(`/projects/${projectId}/repositories/${repoId}`); - setRepositories(repositories.filter((r) => r.id !== repoId)); - } catch (err) { + await deleteRepository(projectId, repoId); + setRepositories((current) => current.filter((r) => r.id !== repoId)); + } catch { setError("Failed to delete repository"); } }; @@ -40,7 +80,13 @@ export const RepositoriesSettingsTab: React.FC = () => { return (
-

Repositories

+
+

Repositories

+ +
{error &&
{error}
}
@@ -66,6 +112,42 @@ export const RepositoriesSettingsTab: React.FC = () => { )) )}
+ + {showCreate && ( +
+
+

Add Repository

+
+ + +
+ + +
+
+
+
+ )}
); };