153 lines
4.0 KiB
TypeScript
153 lines
4.0 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
|
|
import type { GitRepository } from "../types/git-repository";
|
|
import { deleteRepository, listRepositories } from "../api/git-repositories";
|
|
import { Icon } from "../components/ui/Icon";
|
|
import { RepositoryCreateDialog } from "../components/features/project/RepositoryCreateDialog";
|
|
|
|
type RepoStatus = "loading" | "ready" | "error";
|
|
|
|
export const GitRepositoriesPage = () => {
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
const navigate = useNavigate();
|
|
const [status, setStatus] = useState<RepoStatus>("loading");
|
|
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
|
|
|
const loadRepositories = useCallback(async () => {
|
|
if (!projectId) return;
|
|
setStatus("loading");
|
|
try {
|
|
const data = await listRepositories(projectId);
|
|
setRepositories(data);
|
|
setStatus("ready");
|
|
} catch {
|
|
setRepositories([]);
|
|
setStatus("error");
|
|
}
|
|
}, [projectId]);
|
|
|
|
useEffect(() => {
|
|
void loadRepositories();
|
|
}, [loadRepositories]);
|
|
|
|
const handleDelete = async (repoId: string) => {
|
|
if (!projectId) return;
|
|
try {
|
|
await deleteRepository(projectId, repoId);
|
|
setDeleteConfirmId(null);
|
|
await loadRepositories();
|
|
} catch {
|
|
setDeleteConfirmId(null);
|
|
}
|
|
};
|
|
|
|
const isEmpty = status === "ready" && repositories.length === 0;
|
|
|
|
return (
|
|
<section className="stack">
|
|
<div className="page-header">
|
|
<h1>Repositories</h1>
|
|
<button
|
|
className="primary-button"
|
|
onClick={() => setShowCreate(true)}
|
|
type="button"
|
|
>
|
|
<Icon name="add" size="sm" />
|
|
New Repository
|
|
</button>
|
|
</div>
|
|
|
|
{status === "loading" && <p className="muted">Loading repositories...</p>}
|
|
|
|
{status === "error" && (
|
|
<div className="card stack">
|
|
<p>Failed to load repositories</p>
|
|
<button
|
|
className="secondary-button"
|
|
onClick={() => void loadRepositories()}
|
|
type="button"
|
|
>
|
|
<Icon name="refresh" size="sm" />
|
|
Retry
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{isEmpty && (
|
|
<p className="muted">
|
|
No repositories yet. Create your first repository above.
|
|
</p>
|
|
)}
|
|
|
|
{status === "ready" && repositories.length > 0 && (
|
|
<div className="repository-list">
|
|
{repositories.map((repo) => (
|
|
<article className="card repository-card" key={repo.id}>
|
|
<div className="repository-info">
|
|
<h3>{repo.name}</h3>
|
|
{repo.is_mirror && repo.remote_url && (
|
|
<p className="muted">Mirror of {repo.remote_url}</p>
|
|
)}
|
|
<p className="muted">{repo.path}</p>
|
|
</div>
|
|
<div className="repository-actions">
|
|
<button
|
|
className="secondary-button small"
|
|
onClick={() =>
|
|
navigate(
|
|
`/projects/${projectId}/repositories/${repo.id}/history`,
|
|
)
|
|
}
|
|
type="button"
|
|
>
|
|
History
|
|
</button>
|
|
{deleteConfirmId === repo.id ? (
|
|
<div className="delete-confirm">
|
|
<span>Are you sure?</span>
|
|
<button
|
|
className="danger-button"
|
|
onClick={() => void handleDelete(repo.id)}
|
|
type="button"
|
|
>
|
|
Delete
|
|
</button>
|
|
<button
|
|
className="ghost-button"
|
|
onClick={() => setDeleteConfirmId(null)}
|
|
type="button"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button
|
|
className="ghost-button danger-text"
|
|
onClick={() => setDeleteConfirmId(repo.id)}
|
|
type="button"
|
|
>
|
|
Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{showCreate && (
|
|
<RepositoryCreateDialog
|
|
projectId={projectId!}
|
|
open={showCreate}
|
|
title="Create Repository"
|
|
onClose={() => setShowCreate(false)}
|
|
onCreated={loadRepositories}
|
|
/>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|