diff --git a/ui/src/components/IssueProperties.tsx b/ui/src/components/IssueProperties.tsx index ffc3d7d6..04d6cd72 100644 --- a/ui/src/components/IssueProperties.tsx +++ b/ui/src/components/IssueProperties.tsx @@ -1,8 +1,9 @@ import { useState } from "react"; import { Link } from "react-router-dom"; import type { Issue } from "@paperclip/shared"; -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { agentsApi } from "../api/agents"; +import { issuesApi } from "../api/issues"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { queryKeys } from "../lib/queryKeys"; @@ -13,7 +14,7 @@ import { formatDate, cn } from "../lib/utils"; import { timeAgo } from "../lib/timeAgo"; import { Separator } from "@/components/ui/separator"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { User, Hexagon, ArrowUpRight } from "lucide-react"; +import { User, Hexagon, ArrowUpRight, Tag, Plus, Trash2 } from "lucide-react"; import { AgentIcon } from "./AgentIconPicker"; interface IssuePropertiesProps { @@ -32,23 +33,61 @@ function PropertyRow({ label, children }: { label: string; children: React.React export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) { const { selectedCompanyId } = useCompany(); + const queryClient = useQueryClient(); + const companyId = issue.companyId ?? selectedCompanyId; const [assigneeOpen, setAssigneeOpen] = useState(false); const [assigneeSearch, setAssigneeSearch] = useState(""); const [projectOpen, setProjectOpen] = useState(false); const [projectSearch, setProjectSearch] = useState(""); + const [labelsOpen, setLabelsOpen] = useState(false); + const [labelSearch, setLabelSearch] = useState(""); + const [newLabelName, setNewLabelName] = useState(""); + const [newLabelColor, setNewLabelColor] = useState("#6366f1"); const { data: agents } = useQuery({ - queryKey: queryKeys.agents.list(selectedCompanyId!), - queryFn: () => agentsApi.list(selectedCompanyId!), - enabled: !!selectedCompanyId, + queryKey: queryKeys.agents.list(companyId!), + queryFn: () => agentsApi.list(companyId!), + enabled: !!companyId, }); const { data: projects } = useQuery({ - queryKey: queryKeys.projects.list(selectedCompanyId!), - queryFn: () => projectsApi.list(selectedCompanyId!), - enabled: !!selectedCompanyId, + queryKey: queryKeys.projects.list(companyId!), + queryFn: () => projectsApi.list(companyId!), + enabled: !!companyId, }); + const { data: labels } = useQuery({ + queryKey: queryKeys.issues.labels(companyId!), + queryFn: () => issuesApi.listLabels(companyId!), + enabled: !!companyId, + }); + + const createLabel = useMutation({ + mutationFn: (data: { name: string; color: string }) => issuesApi.createLabel(companyId!, data), + onSuccess: async (created) => { + await queryClient.invalidateQueries({ queryKey: queryKeys.issues.labels(companyId!) }); + onUpdate({ labelIds: [...(issue.labelIds ?? []), created.id] }); + setNewLabelName(""); + }, + }); + + const deleteLabel = useMutation({ + mutationFn: (labelId: string) => issuesApi.deleteLabel(labelId), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.labels(companyId!) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId!) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issue.id) }); + }, + }); + + const toggleLabel = (labelId: string) => { + const ids = issue.labelIds ?? []; + const next = ids.includes(labelId) + ? ids.filter((id) => id !== labelId) + : [...ids, labelId]; + onUpdate({ labelIds: next }); + }; + const agentName = (id: string | null) => { if (!id || !agents) return null; const agent = agents.find((a) => a.id === id); @@ -84,6 +123,110 @@ export function IssueProperties({ issue, onUpdate }: IssuePropertiesProps) { /> + + { setLabelsOpen(open); if (!open) setLabelSearch(""); }}> + + + + + setLabelSearch(e.target.value)} + autoFocus + /> +
+ {(labels ?? []) + .filter((label) => { + if (!labelSearch.trim()) return true; + return label.name.toLowerCase().includes(labelSearch.toLowerCase()); + }) + .map((label) => { + const selected = (issue.labelIds ?? []).includes(label.id); + return ( +
+ + +
+ ); + })} +
+
+
+ setNewLabelColor(e.target.value)} + /> + setNewLabelName(e.target.value)} + /> +
+ +
+
+
+
+ { setAssigneeOpen(open); if (!open) setAssigneeSearch(""); }}>