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:
@@ -4,11 +4,12 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|||||||
import { issuesApi } from "../api/issues";
|
import { issuesApi } from "../api/issues";
|
||||||
import { activityApi } from "../api/activity";
|
import { activityApi } from "../api/activity";
|
||||||
import { agentsApi } from "../api/agents";
|
import { agentsApi } from "../api/agents";
|
||||||
|
import { projectsApi } from "../api/projects";
|
||||||
import { useCompany } from "../context/CompanyContext";
|
import { useCompany } from "../context/CompanyContext";
|
||||||
import { usePanel } from "../context/PanelContext";
|
import { usePanel } from "../context/PanelContext";
|
||||||
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
||||||
import { queryKeys } from "../lib/queryKeys";
|
import { queryKeys } from "../lib/queryKeys";
|
||||||
import { relativeTime } from "../lib/utils";
|
import { relativeTime, cn } from "../lib/utils";
|
||||||
import { InlineEditor } from "../components/InlineEditor";
|
import { InlineEditor } from "../components/InlineEditor";
|
||||||
import { CommentThread } from "../components/CommentThread";
|
import { CommentThread } from "../components/CommentThread";
|
||||||
import { IssueProperties } from "../components/IssueProperties";
|
import { IssueProperties } from "../components/IssueProperties";
|
||||||
@@ -20,7 +21,7 @@ import { Identity } from "../components/Identity";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { ActivityEvent } from "@paperclip/shared";
|
||||||
import type { Agent } from "@paperclip/shared";
|
import type { Agent } from "@paperclip/shared";
|
||||||
|
|
||||||
@@ -98,6 +99,8 @@ export function IssueDetail() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [moreOpen, setMoreOpen] = useState(false);
|
const [moreOpen, setMoreOpen] = useState(false);
|
||||||
|
const [projectOpen, setProjectOpen] = useState(false);
|
||||||
|
const [projectSearch, setProjectSearch] = useState("");
|
||||||
|
|
||||||
const { data: issue, isLoading, error } = useQuery({
|
const { data: issue, isLoading, error } = useQuery({
|
||||||
queryKey: queryKeys.issues.detail(issueId!),
|
queryKey: queryKeys.issues.detail(issueId!),
|
||||||
@@ -136,6 +139,12 @@ export function IssueDetail() {
|
|||||||
enabled: !!selectedCompanyId,
|
enabled: !!selectedCompanyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: projects } = useQuery({
|
||||||
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
||||||
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
||||||
|
enabled: !!selectedCompanyId,
|
||||||
|
});
|
||||||
|
|
||||||
const agentMap = useMemo(() => {
|
const agentMap = useMemo(() => {
|
||||||
const map = new Map<string, Agent>();
|
const map = new Map<string, Agent>();
|
||||||
for (const a of agents ?? []) map.set(a.id, a);
|
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>
|
<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}>
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="icon-xs" className="ml-auto">
|
<Button variant="ghost" size="icon-xs" className="ml-auto">
|
||||||
|
|||||||
Reference in New Issue
Block a user