From c353bceb971a2cf0e383172370c5def347b058fe Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 9 Jun 2026 09:22:36 +0000 Subject: [PATCH] feat: unify repository creation flow for desktop and mobile - ProjectCard.tsx: add onAddRepository prop, show 'Add Repository' button in expanded view - ProjectCard.tsx: add showBackButton/onBack props for mobile detail reuse - ProjectsPage.tsx (desktop): wire onAddRepository to open RepositoryCreateDialog - ProjectsPage.tsx (mobile): reuse ProjectCard for detail view instead of inline duplication - pages/projects.css: add .project-add-repo style Both platforms now use the same ProjectCard component and RepositoryCreateDialog for adding repositories to projects. Quality gates: tsc --noEmit pass, npm run build pass, 80/80 tests pass --- .gitignore | 1 + .../features/project/ProjectCard.tsx | 69 ++++-- apps/web/src/pages/ProjectsPage.tsx | 217 +++++------------- apps/web/src/styles/pages/projects.css | 8 + 4 files changed, 121 insertions(+), 174 deletions(-) diff --git a/.gitignore b/.gitignore index d10ca6a..2809674 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ Thumbs.db .atl/ .sisyphus/ .pi-lens/ +minerv3/ diff --git a/apps/web/src/components/features/project/ProjectCard.tsx b/apps/web/src/components/features/project/ProjectCard.tsx index 1f38570..65e7c89 100644 --- a/apps/web/src/components/features/project/ProjectCard.tsx +++ b/apps/web/src/components/features/project/ProjectCard.tsx @@ -8,7 +8,10 @@ interface Props { deleteConfirm: boolean; workspaceLoading: string | null; showCreateForm: string | null; - onToggle: () => void; + /** Mobile detail mode: shows back button instead of chevron toggle. */ + showBackButton?: boolean; + onToggle?: () => void; + onBack?: () => void; onEdit: () => void; onDelete: () => void; onConfirmDelete: () => void; @@ -19,6 +22,7 @@ interface Props { workspace: WorkspaceSummary, action: "sync" | "delete", ) => void; + onAddRepository?: () => void; onCancelCreate: () => void; onCreated: () => void; } @@ -29,39 +33,53 @@ export const ProjectCard = ({ deleteConfirm, workspaceLoading, showCreateForm, + showBackButton = false, onToggle, + onBack, onEdit, onDelete, onConfirmDelete, onCancelDelete, onCreateWorkspace, onWorkspaceAction, + onAddRepository, onCancelCreate, onCreated, }: Props) => { return (
- + ) : ( + + )} +
+

{project.name}

+ {project.description && ( +

{project.description}

)} - +
+ {!showBackButton && project.repositories?.length > 0 && ( + + {project.repositories.length} repo + {project.repositories.length > 1 ? "s" : ""} + + )}
)} + {onAddRepository && ( +
+ +
+ )}
)}
diff --git a/apps/web/src/pages/ProjectsPage.tsx b/apps/web/src/pages/ProjectsPage.tsx index 4cffa81..15898e5 100644 --- a/apps/web/src/pages/ProjectsPage.tsx +++ b/apps/web/src/pages/ProjectsPage.tsx @@ -1,5 +1,9 @@ import { useState } from "react"; -import { EmptyState, ErrorState, LoadingState } from "../components/data-states"; +import { + EmptyState, + ErrorState, + LoadingState, +} from "../components/data-states"; import { Icon } from "../components/icon"; import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { ProjectCard } from "../components/features/project/ProjectCard"; @@ -15,7 +19,8 @@ type MobileView = "list" | "detail" | "create-project" | "create-repo"; export const ProjectsPage = () => { const isMobile = useMobileViewport(); const [mobileView, setMobileView] = useState("list"); - const [selectedProject, setSelectedProject] = useState(null); + const [selectedProject, setSelectedProject] = + useState(null); const { projects, @@ -85,157 +90,59 @@ export const ProjectsPage = () => { } if (mobileView === "detail" && selectedProject) { - const project = selectedProject; return (
-
- -
-

{project.name}

- {project.description && ( -

{project.description}

- )} -
-
- -
-
- -
- {project.repositories.length === 0 ? ( -

No repositories yet.

- ) : ( -
- {project.repositories.map((repo) => ( -
-
-

{repo.name}

- -
- {/* Inline workspace create */} - {creatingWorkspace?.projectId === project.id && - creatingWorkspace.repoId === repo.id && ( -
-

Workspace creation form would appear here.

- -
- )} - {repo.workspaces.length === 0 ? ( -

No workspaces.

- ) : ( -
- {repo.workspaces.map((ws) => ( -
- {ws.name} - - {ws.branch} - - {ws.instance_count > 0 && ( - - {ws.instance_count} tool - {ws.instance_count > 1 ? "s" : ""} - - )} -
- - -
-
- ))} -
- )} -
- ))} -
- )} -
-
-
- -
- - - -
+ { + setMobileView("list"); + setSelectedProject(null); + }} + onEdit={() => { + setFormName(selectedProject.name); + setFormDescription(selectedProject.description ?? ""); + setMobileView("create-project"); + }} + onDelete={() => setDeleteConfirmId(selectedProject.id)} + onConfirmDelete={() => { + void handleDelete(selectedProject.id); + setMobileView("list"); + setSelectedProject(null); + }} + onCancelDelete={() => setDeleteConfirmId(null)} + onCreateWorkspace={(repoId) => + setCreatingWorkspace({ + projectId: selectedProject.id, + repoId, + }) + } + onWorkspaceAction={(repoId, workspace, action) => { + if (action === "sync") { + void handleSyncWorkspace( + selectedProject.id, + repoId, + workspace, + ); + } else if (action === "delete") { + void handleDeleteWorkspace(workspace); + } + }} + onAddRepository={() => setMobileView("create-repo")} + onCancelCreate={() => setCreatingWorkspace(null)} + onCreated={() => { + setCreatingWorkspace(null); + reload(); + }} + />
); } @@ -250,9 +157,7 @@ export const ProjectsPage = () => { - {status === "loading" && ( - - )} + {status === "loading" && } {status === "error" && ( @@ -346,6 +251,10 @@ export const ProjectsPage = () => { } }} onCancelCreate={() => setCreatingWorkspace(null)} + onAddRepository={() => { + setSelectedProject(project); + setMobileView("create-repo"); + }} onCreated={() => { setCreatingWorkspace(null); reload(); diff --git a/apps/web/src/styles/pages/projects.css b/apps/web/src/styles/pages/projects.css index 0e7c53f..fa51841 100644 --- a/apps/web/src/styles/pages/projects.css +++ b/apps/web/src/styles/pages/projects.css @@ -130,3 +130,11 @@ color: var(--danger); } +.project-add-repo { + margin-top: var(--space-4); + padding-top: var(--space-4); + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-start; +} +