From 9ad0acf5557038e8029d05eb456b2f9c8aacc953 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Thu, 25 Jun 2026 14:01:28 -0400 Subject: [PATCH] Refine project UI surfaces Co-authored-by: Thomas Petersen Signed-off-by: Thomas Petersen --- desktop/src/features/projects/hooks.ts | 18 +- .../projects/ui/ProjectDetailScreen.tsx | 12 +- .../src/features/projects/ui/ProjectsView.tsx | 253 +++++++++++------- 3 files changed, 158 insertions(+), 125 deletions(-) diff --git a/desktop/src/features/projects/hooks.ts b/desktop/src/features/projects/hooks.ts index 819c46019..02d4f1a33 100644 --- a/desktop/src/features/projects/hooks.ts +++ b/desktop/src/features/projects/hooks.ts @@ -95,19 +95,6 @@ function readHiddenProjectCards(): string[] { } } -function hideProjectCard(project: Project): void { - if (typeof window === "undefined") { - return; - } - - const hidden = new Set(readHiddenProjectCards()); - hidden.add(projectCoordinate(project)); - window.localStorage.setItem( - HIDDEN_PROJECT_CARDS_KEY, - JSON.stringify([...hidden]), - ); -} - function isHiddenLocally(project: Project): boolean { return readHiddenProjectCards().includes(projectCoordinate(project)); } @@ -306,13 +293,12 @@ async function fetchProjectActivitySummaries( async function deleteProject(project: Project): Promise { const identity = await getIdentity(); if (identity.pubkey.toLowerCase() !== project.owner.toLowerCase()) { - hideProjectCard(project); - return; + throw new Error("Only branch owners can delete branches."); } const event = await signRelayEvent({ kind: KIND_DELETION, - content: `Delete project ${project.name}`, + content: `Delete branch ${project.name}`, tags: [["a", project.repoAddress]], }); diff --git a/desktop/src/features/projects/ui/ProjectDetailScreen.tsx b/desktop/src/features/projects/ui/ProjectDetailScreen.tsx index 8cb6c95ce..2db1b4794 100644 --- a/desktop/src/features/projects/ui/ProjectDetailScreen.tsx +++ b/desktop/src/features/projects/ui/ProjectDetailScreen.tsx @@ -282,7 +282,7 @@ function LatestCommitPanel({ return (
-
+

@@ -395,8 +395,8 @@ function IssuesPanel({ return (

{issues.slice(0, 10).map((issue) => ( -
@@ -418,14 +418,14 @@ function IssuesPanel({ Updated {compactDate(issue.updatedAt)} {issue.labels.map((label) => ( {label} ))}
-
+ ))}
); @@ -646,7 +646,7 @@ export function ProjectDetailScreen({ projectId }: ProjectDetailScreenProps) {
-
+
diff --git a/desktop/src/features/projects/ui/ProjectsView.tsx b/desktop/src/features/projects/ui/ProjectsView.tsx index 86d19634c..e7a874881 100644 --- a/desktop/src/features/projects/ui/ProjectsView.tsx +++ b/desktop/src/features/projects/ui/ProjectsView.tsx @@ -7,6 +7,7 @@ import { LayoutGrid, List, MessageSquare, + MoreHorizontal, Trash2, Users, } from "lucide-react"; @@ -30,8 +31,24 @@ import { useIdentityQuery } from "@/shared/api/hooks"; import { topChromeInset } from "@/shared/layout/chromeLayout"; import { cn } from "@/shared/lib/cn"; import { normalizePubkey } from "@/shared/lib/pubkey"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/shared/ui/alert-dialog"; import { Button } from "@/shared/ui/button"; import { Card } from "@/shared/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/shared/ui/dropdown-menu"; import { UserAvatar } from "@/shared/ui/UserAvatar"; type ProjectsViewMode = "grid" | "list"; @@ -161,6 +178,15 @@ function isProjectMine(project: Project, currentPubkey: string | undefined) { ); } +function isProjectOwnedByCurrentUser( + project: Project, + currentPubkey: string | undefined, +) { + return currentPubkey + ? normalizePubkey(project.owner) === normalizePubkey(currentPubkey) + : false; +} + function projectHasAgent( project: Project, people: string[], @@ -179,31 +205,6 @@ function projectOwnerIsUser( return profiles?.[normalizePubkey(project.owner)]?.isAgent !== true; } -function WorkOwnerBadge({ - avatarUrl, - isAgent, - label, -}: { - avatarUrl: string | null; - isAgent: boolean; - label: string; -}) { - return ( - - - - {isAgent ? "Agent" : "Work by"}: {label} - - - ); -} - function ProjectPeopleStack({ pubkeys, profiles, @@ -248,8 +249,12 @@ function ProjectPeopleStack({ } function StatusPill({ status }: { status: string }) { + if (status === "active") { + return null; + } + return ( - + {status} ); @@ -447,30 +452,86 @@ function ProjectCardButton({ ); } -function ProjectDeleteButton({ +function ProjectActionsMenu({ project, + canDelete, disabled, onDelete, }: { project: Project; + canDelete: boolean; disabled: boolean; - onDelete: (project: Project) => void; + onDelete: (project: Project) => Promise | void; }) { + const [confirmOpen, setConfirmOpen] = React.useState(false); + return ( - + + + + + + + { + event.preventDefault(); + event.stopPropagation(); + if (canDelete && !disabled) { + setConfirmOpen(true); + } + }} + > + + Delete branch + + + + + + Delete branch? + + Delete {project.name} from Projects for everyone. This can only be + done for branches you own and cannot be undone. + + + + + + + + + + + + ); } @@ -479,67 +540,69 @@ function ProjectGridCard({ people, profiles, summary, + canDelete, + deleteDisabled, onDelete, onOpen, - deleteDisabled, }: { project: Project; people: string[]; profiles?: UserProfileLookup; summary: ProjectActivitySummary | undefined; - onDelete: (project: Project) => void; - onOpen: (project: Project) => void; + canDelete: boolean; deleteDisabled: boolean; + onDelete: (project: Project) => Promise | void; + onOpen: (project: Project) => void; }) { - const ownerProfile = profiles?.[normalizePubkey(project.owner)]; - const ownerLabel = resolveUserLabel({ pubkey: project.owner, profiles }); - return (
-
+
- - - {project.name} + + +
+ + {project.name} + +

+ {project.dtag} +

+
-

- {project.dtag} -

- +
+ + +
- - -

+

{project.description || "A shared space for internal git work."}

-
+
{project.defaultBranch} {pluralize(people.length, "person", "people")} - - {getDiscussionLabel(project)} - {formatCreatedDate(project.createdAt)}
-
+

{getActivityLabel(summary)} @@ -550,11 +613,6 @@ function ProjectGridCard({ pubkeys={people} workOwnerPubkey={project.owner} /> -

@@ -572,24 +630,23 @@ function ProjectListRow({ people, profiles, summary, + canDelete, + deleteDisabled, onDelete, onOpen, - deleteDisabled, }: { project: Project; people: string[]; profiles?: UserProfileLookup; summary: ProjectActivitySummary | undefined; - onDelete: (project: Project) => void; - onOpen: (project: Project) => void; + canDelete: boolean; deleteDisabled: boolean; + onDelete: (project: Project) => Promise | void; + onOpen: (project: Project) => void; }) { - const ownerProfile = profiles?.[normalizePubkey(project.owner)]; - const ownerLabel = resolveUserLabel({ pubkey: project.owner, profiles }); - return ( @@ -605,11 +662,6 @@ function ProjectListRow({

{project.description || "A shared space for internal git work."}

-
@@ -642,7 +694,8 @@ function ProjectListRow({ pubkeys={people} workOwnerPubkey={project.owner} /> - { @@ -707,14 +761,14 @@ export function ProjectsView() { }, []); const visibleProjects = React.useMemo(() => { - const currentPubkey = identityQuery.data?.pubkey; return projects .filter((project) => { const summary = activitySummariesQuery.data?.[project.repoAddress]; const people = projectPeople(project, summary); if (filter === "mine") return isProjectMine(project, currentPubkey); - if (filter === "agents") + if (filter === "agents") { return projectHasAgent(project, people, profiles); + } if (filter === "users") return projectOwnerIsUser(project, profiles); return true; }) @@ -734,8 +788,8 @@ export function ProjectsView() { }); }, [ activitySummariesQuery.data, + currentPubkey, filter, - identityQuery.data?.pubkey, profiles, projects, sort, @@ -750,17 +804,12 @@ export function ProjectsView() { const handleDeleteProject = React.useCallback( async (project: Project) => { - const confirmed = window.confirm(`Delete ${project.name}?`); - if (!confirmed) return; - try { await deleteProjectMutation.mutateAsync(project); - toast.success("Project card deleted"); + toast.success("Branch deleted"); } catch (error) { toast.error( - error instanceof Error - ? error.message - : "Failed to delete project card", + error instanceof Error ? error.message : "Failed to delete branch", ); } }, @@ -816,11 +865,10 @@ export function ProjectsView() { const summary = activitySummariesQuery.data?.[project.repoAddress]; return ( - void handleDeleteProject(nextProject) - } + onDelete={handleDeleteProject} onOpen={handleOpenProject} people={projectPeople(project, summary)} profiles={profiles} @@ -836,11 +884,10 @@ export function ProjectsView() { const summary = activitySummariesQuery.data?.[project.repoAddress]; return ( - void handleDeleteProject(nextProject) - } + onDelete={handleDeleteProject} onOpen={handleOpenProject} people={projectPeople(project, summary)} profiles={profiles}