070e960c05
- Extract use-projects hook for state management - Extract ProjectCard and ProjectDialog components - Slim ProjectsPage from 433 to ~100 lines Quality gates: tsc --noEmit passes, npm run build passes
121 lines
4.7 KiB
TypeScript
121 lines
4.7 KiB
TypeScript
import { Icon } from "../../icon";
|
|
import { WorkspaceCreateForm } from "../workspace/workspace-create-form";
|
|
import type { ProjectWithRepos, WorkspaceSummary } from "../../../types";
|
|
|
|
interface Props {
|
|
project: ProjectWithRepos;
|
|
expanded: boolean;
|
|
deleteConfirm: boolean;
|
|
workspaceLoading: string | null;
|
|
showCreateForm: string | null;
|
|
onToggle: () => void;
|
|
onEdit: () => void;
|
|
onDelete: () => void;
|
|
onConfirmDelete: () => void;
|
|
onCancelDelete: () => void;
|
|
onCreateWorkspace: (repoId: string) => void;
|
|
onWorkspaceAction: (repoId: string, workspace: WorkspaceSummary, action: "sync" | "delete") => void;
|
|
onCancelCreate: () => void;
|
|
onCreated: () => void;
|
|
}
|
|
|
|
export const ProjectCard = ({
|
|
project,
|
|
expanded,
|
|
deleteConfirm,
|
|
workspaceLoading,
|
|
showCreateForm,
|
|
onToggle,
|
|
onEdit,
|
|
onDelete,
|
|
onConfirmDelete,
|
|
onCancelDelete,
|
|
onCreateWorkspace,
|
|
onWorkspaceAction,
|
|
onCancelCreate,
|
|
onCreated,
|
|
}: Props) => {
|
|
return (
|
|
<article className="card project-card">
|
|
<div className="project-info-row">
|
|
<button className="project-toggle" onClick={onToggle} type="button" aria-expanded={expanded}>
|
|
<Icon name={expanded ? "chevron-down" : "chevron-right"} size="sm" />
|
|
<h3>{project.name}</h3>
|
|
{project.repositories.length > 0 && (
|
|
<span className="repo-count">
|
|
{project.repositories.length} repo{project.repositories.length > 1 ? "s" : ""}
|
|
</span>
|
|
)}
|
|
</button>
|
|
<div className="project-actions">
|
|
<button className="ghost-button" onClick={onEdit} type="button">
|
|
<Icon name="edit" size="sm" /> Edit
|
|
</button>
|
|
{deleteConfirm ? (
|
|
<div className="delete-confirm">
|
|
<span>Are you sure?</span>
|
|
<button className="danger-button" onClick={onConfirmDelete} type="button">
|
|
<Icon name="delete" size="sm" /> Delete
|
|
</button>
|
|
<button className="ghost-button" onClick={onCancelDelete} type="button">
|
|
<Icon name="cancel" size="sm" /> Cancel
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button className="ghost-button danger-text" onClick={onDelete} type="button">
|
|
<Icon name="delete" size="sm" /> Delete
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{expanded && (
|
|
<div className="project-detail">
|
|
{project.repositories.length === 0 ? (
|
|
<p className="muted">No repositories yet.</p>
|
|
) : (
|
|
<div className="repo-list">
|
|
{project.repositories.map((repo) => (
|
|
<div key={repo.id} className="repo-block">
|
|
<div className="repo-header">
|
|
<h4>{repo.name}</h4>
|
|
<button className="btn btn-sm btn-primary" onClick={() => onCreateWorkspace(repo.id)} type="button">
|
|
<Icon name="add" size="sm" /> New Workspace
|
|
</button>
|
|
</div>
|
|
{showCreateForm === repo.id && (
|
|
<WorkspaceCreateForm defaultProjectId={project.id} defaultRepoId={repo.id} onSubmit={onCreated} onCancel={onCancelCreate} />
|
|
)}
|
|
{repo.workspaces.length === 0 ? (
|
|
<p className="muted">No workspaces.</p>
|
|
) : (
|
|
<div className="workspace-grid">
|
|
{repo.workspaces.map((ws) => (
|
|
<div key={ws.id} className={`workspace-chip ${ws.status}`}>
|
|
<a href={`/workspaces/${ws.id}`}>{ws.name}</a>
|
|
<span className="ws-branch"><Icon name="branch" size="sm" /> {ws.branch}</span>
|
|
{ws.instance_count > 0 && (
|
|
<span className="ws-instances">{ws.instance_count} tool{ws.instance_count > 1 ? "s" : ""}</span>
|
|
)}
|
|
<div className="ws-actions">
|
|
<button type="button" disabled={workspaceLoading === ws.id} onClick={() => onWorkspaceAction(repo.id, ws, "sync")}>
|
|
<Icon name="refresh" size="sm" />
|
|
</button>
|
|
<button type="button" className="danger-text" disabled={workspaceLoading === ws.id} onClick={() => onWorkspaceAction(repo.id, ws, "delete")}>
|
|
<Icon name="delete" size="sm" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</article>
|
|
);
|
|
};
|