5ed5e1c84b
- Fix ProjectsPage tests by wrapping renders in MemoryRouter (9 passing) - Improve session auto-naming to 'project / repo / tool' format - Add missing /users/me/sessions endpoint for sidebar session loading - Handle git history 500s: catch RuntimeError in endpoints, graceful empty repo handling - Add git status badge and discard-changes button to FileEditor toolbar Quality gates: tsc pass, build pass, Python syntax pass
210 lines
5.9 KiB
TypeScript
210 lines
5.9 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { Link, useParams, useSearchParams } from "react-router-dom";
|
|
|
|
import { apiClient } from "../api/client";
|
|
import type { GitRepository } from "../types/git-repository";
|
|
import type { ToolType } from "../types/tool-type";
|
|
import {
|
|
getRepositoryStatus,
|
|
listRepositories,
|
|
type GitStatus,
|
|
} from "../api/git-repositories";
|
|
import { WorkspaceSidebar } from "../components/features/git";
|
|
import { FileEditor } from "../components/features/git/FileEditor";
|
|
import { WorkspaceHeader } from "../components/features/workspace/WorkspaceHeader";
|
|
import { GitToolbar } from "../components/features/git/GitToolbar";
|
|
import { LoadingState, ErrorState } from "../components/ui";
|
|
import { listToolTypes } from "../api/tool-types";
|
|
|
|
type WorkspaceStatus = "loading" | "ready" | "error" | "empty";
|
|
|
|
interface Project {
|
|
id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
}
|
|
|
|
export const RepoWorkspace = () => {
|
|
const { projectId } = useParams<{ projectId: string }>();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
const [status, setStatus] = useState<WorkspaceStatus>("loading");
|
|
const [project, setProject] = useState<Project | null>(null);
|
|
const [repositories, setRepositories] = useState<GitRepository[]>([]);
|
|
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(
|
|
searchParams.get("repo"),
|
|
);
|
|
const [branches, setBranches] = useState<string[]>([]);
|
|
const [currentBranch, setCurrentBranch] = useState<string>("main");
|
|
const [gitStatus, setGitStatus] = useState<GitStatus | null>(null);
|
|
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
|
|
|
|
const loadProject = useCallback(async () => {
|
|
if (!projectId) return;
|
|
try {
|
|
const response = await apiClient.get(`/projects/${projectId}`);
|
|
setProject(response.data);
|
|
} catch {
|
|
setProject(null);
|
|
}
|
|
}, [projectId]);
|
|
|
|
const loadRepositories = useCallback(async () => {
|
|
if (!projectId) return;
|
|
setStatus("loading");
|
|
try {
|
|
const data = await listRepositories(projectId);
|
|
setRepositories(data);
|
|
if (data.length === 0) {
|
|
setStatus("empty");
|
|
} else {
|
|
setStatus("ready");
|
|
if (!selectedRepoId) {
|
|
setSelectedRepoId(data[0].id);
|
|
const newParams = new URLSearchParams(searchParams);
|
|
newParams.set("repo", data[0].id);
|
|
setSearchParams(newParams, { replace: true });
|
|
}
|
|
}
|
|
} catch {
|
|
setRepositories([]);
|
|
setStatus("error");
|
|
}
|
|
}, [projectId, selectedRepoId, searchParams, setSearchParams]);
|
|
|
|
const loadBranches = useCallback(async () => {
|
|
if (!projectId || !selectedRepoId) return;
|
|
try {
|
|
const response = await apiClient.get(
|
|
`/projects/${projectId}/repositories/${selectedRepoId}/branches`,
|
|
);
|
|
const branchList = response.data.branches.map(
|
|
(b: { name: string }) => b.name,
|
|
);
|
|
setBranches(branchList);
|
|
const defaultBranch = response.data.default_branch;
|
|
if (defaultBranch) setCurrentBranch(defaultBranch);
|
|
} catch {
|
|
setBranches([]);
|
|
}
|
|
}, [projectId, selectedRepoId]);
|
|
|
|
const loadGitStatus = useCallback(async () => {
|
|
if (!projectId || !selectedRepoId) return;
|
|
try {
|
|
const data = await getRepositoryStatus(projectId, selectedRepoId);
|
|
setGitStatus(data);
|
|
} catch {
|
|
setGitStatus(null);
|
|
}
|
|
}, [projectId, selectedRepoId]);
|
|
|
|
const loadToolTypes = useCallback(async () => {
|
|
try {
|
|
const data = await listToolTypes();
|
|
setToolTypes(data);
|
|
} catch {
|
|
setToolTypes([]);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void loadProject();
|
|
void loadRepositories();
|
|
void loadToolTypes();
|
|
}, [loadProject, loadRepositories, loadToolTypes]);
|
|
|
|
useEffect(() => {
|
|
void loadBranches();
|
|
void loadGitStatus();
|
|
}, [loadBranches, loadGitStatus]);
|
|
|
|
const handleRepoChange = (repoId: string) => {
|
|
setSelectedRepoId(repoId);
|
|
const newParams = new URLSearchParams(searchParams);
|
|
newParams.set("repo", repoId);
|
|
newParams.delete("branch");
|
|
newParams.delete("path");
|
|
setSearchParams(newParams);
|
|
};
|
|
|
|
const selectedRepo = repositories.find((r) => r.id === selectedRepoId);
|
|
|
|
return (
|
|
<section className="repo-workspace">
|
|
{project && (
|
|
<WorkspaceHeader project={project} currentRepo={selectedRepo || null} />
|
|
)}
|
|
|
|
{status === "loading" && (
|
|
<LoadingState message="Loading repositories..." />
|
|
)}
|
|
|
|
{status === "error" && (
|
|
<ErrorState
|
|
message="Failed to load repositories"
|
|
onRetry={() => void loadRepositories()}
|
|
/>
|
|
)}
|
|
|
|
{status === "empty" && (
|
|
<div className="card stack">
|
|
<p>No repositories in this project yet.</p>
|
|
<Link
|
|
className="primary-button"
|
|
to={`/projects/${projectId}/settings/repositories`}
|
|
>
|
|
Manage Repositories
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{status === "ready" && repositories.length > 0 && (
|
|
<>
|
|
{selectedRepoId && (
|
|
<GitToolbar
|
|
projectId={projectId!}
|
|
repoId={selectedRepoId}
|
|
currentBranch={currentBranch}
|
|
branches={branches}
|
|
hasRemote={Boolean(selectedRepo?.remote_url)}
|
|
onBranchChange={(branch) => {
|
|
setCurrentBranch(branch);
|
|
const newParams = new URLSearchParams(searchParams);
|
|
newParams.set("branch", branch);
|
|
setSearchParams(newParams);
|
|
}}
|
|
onRefresh={() => {
|
|
void loadBranches();
|
|
void loadGitStatus();
|
|
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
|
}}
|
|
/>
|
|
)}
|
|
<div className="workspace-layout">
|
|
{selectedRepoId && (
|
|
<WorkspaceSidebar
|
|
projectId={projectId!}
|
|
repoId={selectedRepoId}
|
|
repositories={repositories}
|
|
gitStatus={gitStatus}
|
|
toolTypes={toolTypes}
|
|
onRepoChange={handleRepoChange}
|
|
onCommit={() => {
|
|
void loadGitStatus();
|
|
window.dispatchEvent(new CustomEvent("refresh-file-tree"));
|
|
}}
|
|
/>
|
|
)}
|
|
<main className="workspace-main">
|
|
{selectedRepoId && (
|
|
<FileEditor projectId={projectId!} repoId={selectedRepoId} gitStatus={gitStatus} />
|
|
)}
|
|
</main>
|
|
</div>
|
|
</>
|
|
)}
|
|
</section>
|
|
);
|
|
};
|