/** Projects page with inline repositories and workspaces. */ import { useState } from "react"; import { createProject, deleteProject, listProjects, updateProject, type ProjectCreateInput, type ProjectUpdateInput, } from "../api/projects"; import { createWorkspace, deleteWorkspace, syncWorkspace, } from "../api/workspaces"; import { EmptyState, ErrorState, LoadingState } from "../components/data-states"; import { Icon } from "../components/icon"; import { WorkspaceCreateForm } from "../components/workspace-create-form"; import { useAsyncData } from "../hooks/use-async-data"; import type { ProjectWithRepos, WorkspaceSummary } from "../types"; type DialogMode = "none" | "create" | "edit"; export const ProjectsPage = () => { const { data: projects, status, reload } = useAsyncData( listProjects, [], ); const [dialogMode, setDialogMode] = useState("none"); const [editingProject, setEditingProject] = useState( null, ); const [formName, setFormName] = useState(""); const [formDescription, setFormDescription] = useState(""); const [formError, setFormError] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [expandedProject, setExpandedProject] = useState(null); const [creatingWorkspace, setCreatingWorkspace] = useState<{ projectId: string; repoId: string; } | null>(null); const [workspaceLoading, setWorkspaceLoading] = useState(null); const safeProjects = projects ?? []; const openCreate = () => { setFormName(""); setFormDescription(""); setFormError(null); setEditingProject(null); setDialogMode("create"); }; const openEdit = (project: ProjectWithRepos) => { setFormName(project.name); setFormDescription(project.description ?? ""); setFormError(null); setEditingProject(project); setDialogMode("edit"); }; const closeDialog = () => { setDialogMode("none"); setEditingProject(null); setFormError(null); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setFormError(null); if (!formName.trim()) { setFormError("Project name is required"); return; } try { if (dialogMode === "create") { const input: ProjectCreateInput = { name: formName.trim(), description: formDescription.trim() || null, }; await createProject(input); } else if (dialogMode === "edit" && editingProject) { const input: ProjectUpdateInput = { name: formName.trim(), description: formDescription.trim() || null, }; await updateProject(editingProject.id, input); } closeDialog(); reload(); } catch { setFormError("Failed to save project"); } }; const handleDelete = async (projectId: string) => { try { await deleteProject(projectId); setDeleteConfirmId(null); reload(); } catch { setDeleteConfirmId(null); } }; const handleCreateWorkspace = async ( projectId: string, repoId: string, data: { name: string; branch: string }, ) => { setWorkspaceLoading(repoId); try { await createWorkspace(projectId, repoId, data); setCreatingWorkspace(null); reload(); } catch (err) { alert(err instanceof Error ? err.message : "Failed to create workspace"); } finally { setWorkspaceLoading(null); } }; const handleSyncWorkspace = async ( projectId: string, repoId: string, workspace: WorkspaceSummary, ) => { setWorkspaceLoading(workspace.id); try { await syncWorkspace(projectId, repoId, workspace.id); reload(); } catch (err) { alert(err instanceof Error ? err.message : "Failed to sync workspace"); } finally { setWorkspaceLoading(null); } }; const handleDeleteWorkspace = async ( projectId: string, repoId: string, workspace: WorkspaceSummary, ) => { if (!confirm(`Delete workspace "${workspace.name}"?`)) return; setWorkspaceLoading(workspace.id); try { await deleteWorkspace(projectId, repoId, workspace.id); reload(); } catch (err) { alert(err instanceof Error ? err.message : "Failed to delete workspace"); } finally { setWorkspaceLoading(null); } }; const isEmpty = status === "ready" && safeProjects.length === 0; return (

Projects

{status === "loading" && } {status === "error" && ( )} {isEmpty && ( )} {status === "ready" && safeProjects.length > 0 && (
{safeProjects.map((project) => ( setExpandedProject( expandedProject === project.id ? null : project.id, ) } onEdit={() => openEdit(project)} onDelete={() => setDeleteConfirmId(project.id)} deleteConfirm={deleteConfirmId === project.id} onConfirmDelete={() => void handleDelete(project.id)} onCancelDelete={() => setDeleteConfirmId(null)} onCreateWorkspace={(repoId) => setCreatingWorkspace({ projectId: project.id, repoId }) } onWorkspaceAction={(repoId, workspace, action) => { if (action === "sync") { void handleSyncWorkspace(project.id, repoId, workspace); } else if (action === "delete") { void handleDeleteWorkspace( project.id, repoId, workspace, ); } }} workspaceLoading={workspaceLoading} showCreateForm={ creatingWorkspace?.projectId === project.id ? creatingWorkspace.repoId : null } onCancelCreate={() => setCreatingWorkspace(null)} onSubmitCreate={async (repoId, data) => await handleCreateWorkspace(project.id, repoId, data) } /> ))}
)} {dialogMode !== "none" && (

{dialogMode === "create" ? "Create Project" : "Edit Project"}