cc1507a33e
- Add repository creation dialog to the project settings repositories tab - Reuse shared repository API for list/create/delete operations - Add coverage for the create flow in the settings tab Quality gates: tsc --noEmit, vitest run src/components/repositories-settings-tab.test.tsx
154 lines
4.5 KiB
TypeScript
154 lines
4.5 KiB
TypeScript
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 { Icon } from "./icon";
|
|
|
|
export const RepositoriesSettingsTab: React.FC = () => {
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [formName, setFormName] = useState("");
|
|
const [formRemoteUrl, setFormRemoteUrl] = useState("");
|
|
const [error, setError] = useState("");
|
|
|
|
const loadRepositories = useCallback(async () => {
|
|
if (!projectId) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
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 deleteRepository(projectId, repoId);
|
|
setRepositories((current) => current.filter((r) => r.id !== repoId));
|
|
} catch {
|
|
setError("Failed to delete repository");
|
|
}
|
|
};
|
|
|
|
if (loading) return <div>Loading...</div>;
|
|
|
|
return (
|
|
<div className="repositories-settings-tab">
|
|
<div className="page-header">
|
|
<h2>Repositories</h2>
|
|
<button className="primary-button" onClick={() => setShowCreate(true)} type="button">
|
|
<Icon name="add" size="sm" />
|
|
Add Repository
|
|
</button>
|
|
</div>
|
|
{error && <div className="error-message">{error}</div>}
|
|
|
|
<div className="repositories-list">
|
|
{repositories.length === 0 ? (
|
|
<p>No repositories yet.</p>
|
|
) : (
|
|
repositories.map((repo) => (
|
|
<div key={repo.id} className="repository-card">
|
|
<div className="repository-info">
|
|
<h3>{repo.name}</h3>
|
|
<p>{repo.remote_url}</p>
|
|
<span className="repo-type">
|
|
{repo.is_mirror ? "Mirror" : "Clone"}
|
|
</span>
|
|
</div>
|
|
<button
|
|
onClick={() => handleDelete(repo.id)}
|
|
className="btn-danger"
|
|
>
|
|
Delete
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{showCreate && (
|
|
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
|
<div className="dialog">
|
|
<h3>Add Repository</h3>
|
|
<form onSubmit={handleCreate} className="stack">
|
|
<label className="form-field">
|
|
Name
|
|
<input
|
|
type="text"
|
|
value={formName}
|
|
onChange={(event) => setFormName(event.target.value)}
|
|
placeholder="repository-name"
|
|
/>
|
|
</label>
|
|
<label className="form-field">
|
|
Remote URL (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">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|