refactor(web): reuse repository create dialog

- Extract the repository create dialog into a shared component
- Reuse the same clone/validation flow in project settings and repository management pages
- Keep the shared UI covered with focused tests

Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
This commit is contained in:
2026-05-22 18:43:03 +02:00
parent ebfe991a15
commit 5af4de0d7e
4 changed files with 250 additions and 280 deletions
@@ -1,13 +1,8 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import {
createRepository,
deleteRepository,
listRepositories,
type GitRepository,
type GitRepositoryCreate,
} from "../api/git_repositories";
import { deleteRepository, listRepositories, type GitRepository } from "../api/git_repositories";
import { RepositoryCreateDialog } from "./repository-create-dialog";
import { Icon } from "./icon";
export const RepositoriesSettingsTab: React.FC = () => {
@@ -15,9 +10,6 @@ export const RepositoriesSettingsTab: React.FC = () => {
const [repositories, setRepositories] = useState<GitRepository[]>([]);
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [createMode, setCreateMode] = useState<"clone" | "blank">("clone");
const [formName, setFormName] = useState("");
const [formRemoteUrl, setFormRemoteUrl] = useState("");
const [error, setError] = useState("");
const loadRepositories = useCallback(async () => {
@@ -41,36 +33,6 @@ export const RepositoriesSettingsTab: React.FC = () => {
void loadRepositories();
}, [loadRepositories]);
const handleCreate = async (event: React.FormEvent) => {
event.preventDefault();
setError("");
if (!projectId) return;
if (!formName.trim()) {
setError("Repository name is required");
return;
}
if (createMode === "clone" && !formRemoteUrl.trim()) {
setError("Remote URL is required to clone an existing repository");
return;
}
try {
const input: GitRepositoryCreate = {
name: formName.trim(),
remote_url: formRemoteUrl.trim() || undefined,
};
await createRepository(projectId, input);
setShowCreate(false);
setCreateMode("clone");
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;
@@ -124,62 +86,13 @@ export const RepositoriesSettingsTab: React.FC = () => {
</div>
{showCreate && (
<div className="dialog-overlay" role="dialog" aria-modal="true">
<div className="dialog">
<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">
<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">
Repository name
<input
type="text"
value={formName}
onChange={(event) => setFormName(event.target.value)}
placeholder="repository-name"
/>
</label>
<label className="form-field">
Remote URL {createMode === "clone" ? "(required)" : "(optional)"}
<input
type="text"
value={formRemoteUrl}
onChange={(event) => setFormRemoteUrl(event.target.value)}
placeholder="https://github.com/user/repo.git"
/>
</label>
<div className="dialog-actions">
<button className="secondary-button" onClick={() => setShowCreate(false)} type="button">
Cancel
</button>
<button className="primary-button" type="submit">
{createMode === "clone" ? "Clone Repository" : "Create Blank Repository"}
</button>
</div>
</form>
</div>
</div>
<RepositoryCreateDialog
projectId={projectId!}
open={showCreate}
title="Add Repository"
onClose={() => setShowCreate(false)}
onCreated={loadRepositories}
/>
)}
</div>
);