merge: align dev branch with main

This commit is contained in:
Developer
2026-06-03 08:51:02 +00:00
parent 51a399c775
commit b39d6ce5f4
319 changed files with 30221 additions and 9221 deletions
+152
View File
@@ -0,0 +1,152 @@
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>
);
};