ab79080f0b
Frontend: - Create reusable DataStates components (LoadingState, ErrorState, EmptyState) - Refactor 12 pages to use shared state components instead of inline JSX - Extract useInstanceActions hook to eliminate session action duplication - Update dashboard and sessions pages to use shared hook OpenSpec: - Archive completed mobile-app-usability change (44/44 tasks) - Archive completed add-config-profiles change (15/15 tasks) Quality: TypeScript check passes, production build succeeds
122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
import { useState } from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
|
|
import {
|
|
deleteRepository,
|
|
listRepositories,
|
|
} from "../api/git_repositories";
|
|
import type { GitRepository } from "../api/git_repositories";
|
|
import { EmptyState, ErrorState, LoadingState } from "../components/data-states";
|
|
import { Icon } from "../components/icon";
|
|
import { RepositoryCreateDialog } from "../components/repository-create-dialog";
|
|
import { useAsyncData } from "../hooks/use-async-data";
|
|
|
|
export const GitRepositoriesPage = () => {
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
const navigate = useNavigate();
|
|
const [showCreate, setShowCreate] = useState(false);
|
|
const [deleteConfirmId, setDeleteConfirmId] = useState<string | null>(null);
|
|
|
|
const { data: repositories, status, reload } = useAsyncData<GitRepository[]>(
|
|
async () => {
|
|
if (!projectId) return [];
|
|
return await listRepositories(projectId);
|
|
},
|
|
[projectId]
|
|
);
|
|
|
|
const handleDelete = async (repoId: string) => {
|
|
if (!projectId) return;
|
|
try {
|
|
await deleteRepository(projectId, repoId);
|
|
setDeleteConfirmId(null);
|
|
reload();
|
|
} catch {
|
|
setDeleteConfirmId(null);
|
|
}
|
|
};
|
|
|
|
const safeRepositories = repositories ?? [];
|
|
const isEmpty = status === "ready" && safeRepositories.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" && <LoadingState message="Loading repositories..." />}
|
|
|
|
{status === "error" && <ErrorState message="Failed to load repositories" onRetry={reload} />}
|
|
|
|
{isEmpty && <EmptyState message="No repositories yet. Create your first repository above." />}
|
|
|
|
{status === "ready" && safeRepositories.length > 0 && (
|
|
<div className="repository-list">
|
|
{safeRepositories.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={reload}
|
|
/>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|