Add interactive project picker to issue detail page

The project field in the issue header was a static read-only link (or
invisible when unset), making it impossible to add/edit a project directly
from the /issues/{id} page. Replace it with a searchable popover picker
that always shows, matching the pattern used by StatusIcon and PriorityIcon.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-20 09:01:28 -06:00
parent f766478f5a
commit 32119f5c2f

View File

@@ -4,11 +4,12 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { issuesApi } from "../api/issues";
import { activityApi } from "../api/activity";
import { agentsApi } from "../api/agents";
import { projectsApi } from "../api/projects";
import { useCompany } from "../context/CompanyContext";
import { usePanel } from "../context/PanelContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { relativeTime } from "../lib/utils";
import { relativeTime, cn } from "../lib/utils";
import { InlineEditor } from "../components/InlineEditor";
import { CommentThread } from "../components/CommentThread";
import { IssueProperties } from "../components/IssueProperties";
@@ -20,7 +21,7 @@ import { Identity } from "../components/Identity";
import { Separator } from "@/components/ui/separator";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
import { ChevronRight, MoreHorizontal, EyeOff } from "lucide-react";
import { ChevronRight, MoreHorizontal, EyeOff, Hexagon } from "lucide-react";
import type { ActivityEvent } from "@paperclip/shared";
import type { Agent } from "@paperclip/shared";
@@ -98,6 +99,8 @@ export function IssueDetail() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const [moreOpen, setMoreOpen] = useState(false);
const [projectOpen, setProjectOpen] = useState(false);
const [projectSearch, setProjectSearch] = useState("");
const { data: issue, isLoading, error } = useQuery({
queryKey: queryKeys.issues.detail(issueId!),
@@ -136,6 +139,12 @@ export function IssueDetail() {
enabled: !!selectedCompanyId,
});
const { data: projects } = useQuery({
queryKey: queryKeys.projects.list(selectedCompanyId!),
queryFn: () => projectsApi.list(selectedCompanyId!),
enabled: !!selectedCompanyId,
});
const agentMap = useMemo(() => {
const map = new Map<string, Agent>();
for (const a of agents ?? []) map.set(a.id, a);
@@ -254,6 +263,55 @@ export function IssueDetail() {
/>
<span className="text-xs font-mono text-muted-foreground">{issue.identifier ?? issue.id.slice(0, 8)}</span>
<Popover open={projectOpen} onOpenChange={(open) => { setProjectOpen(open); if (!open) setProjectSearch(""); }}>
<PopoverTrigger asChild>
<button className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors rounded px-1 -mx-1 py-0.5">
<Hexagon className="h-3 w-3 shrink-0" />
{issue.projectId
? ((projects ?? []).find((p) => p.id === issue.projectId)?.name ?? issue.projectId.slice(0, 8))
: <span className="opacity-50">No project</span>
}
</button>
</PopoverTrigger>
<PopoverContent className="w-52 p-1" align="start">
<input
className="w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder="Search projects..."
value={projectSearch}
onChange={(e) => setProjectSearch(e.target.value)}
autoFocus
/>
<button
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
!issue.projectId && "bg-accent"
)}
onClick={() => { updateIssue.mutate({ projectId: null }); setProjectOpen(false); }}
>
No project
</button>
{(projects ?? [])
.filter((p) => {
if (!projectSearch.trim()) return true;
return p.name.toLowerCase().includes(projectSearch.toLowerCase());
})
.map((p) => (
<button
key={p.id}
className={cn(
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
p.id === issue.projectId && "bg-accent"
)}
onClick={() => { updateIssue.mutate({ projectId: p.id }); setProjectOpen(false); }}
>
<Hexagon className="h-3 w-3 text-muted-foreground shrink-0" />
{p.name}
</button>
))
}
</PopoverContent>
</Popover>
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon-xs" className="ml-auto">