Files
headquarter/apps/web/src/pages/RepoWorkspacePage.tsx
T
Developer 5ed5e1c84b fix: resolve four frontend/backend issues
- 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
2026-06-03 08:30:28 +00:00

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>
);
};