refactor: extract ProjectsPage components
- 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
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Icon } from "../../icon";
|
||||
|
||||
interface Props {
|
||||
mode: "create" | "edit";
|
||||
name: string;
|
||||
description: string;
|
||||
error: string | null;
|
||||
onNameChange: (name: string) => void;
|
||||
onDescriptionChange: (desc: string) => void;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const ProjectDialog = ({
|
||||
mode,
|
||||
name,
|
||||
description,
|
||||
error,
|
||||
onNameChange,
|
||||
onDescriptionChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="dialog-overlay" role="dialog" aria-modal="true">
|
||||
<div className="dialog">
|
||||
<h2>{mode === "create" ? "Create Project" : "Edit Project"}</h2>
|
||||
<form onSubmit={onSubmit} className="stack">
|
||||
<label className="form-field">
|
||||
Name
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => onNameChange(e.target.value)}
|
||||
placeholder="Project name"
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
Description
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
||||
placeholder="Optional description"
|
||||
rows={3}
|
||||
/>
|
||||
</label>
|
||||
{error && <p className="error-text">{error}</p>}
|
||||
<div className="dialog-actions">
|
||||
<button className="secondary-button" onClick={onCancel} type="button">
|
||||
<Icon name="cancel" size="sm" />
|
||||
Cancel
|
||||
</button>
|
||||
<button className="primary-button" type="submit">
|
||||
{mode === "create" ? (
|
||||
<>
|
||||
<Icon name="add" size="sm" />
|
||||
Create
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Icon name="save" size="sm" />
|
||||
Save
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user