1import { Settings2 } from "lucide-react";
2
3import { useBugChangeLabelsMutation, BugDetailDocument } from "@/__generated__/graphql";
4import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
5import { useAuth } from "@/lib/auth";
6
7import { LabelBadge } from "./LabelBadge";
8
9interface LabelEditorProps {
10 bugPrefix: string;
11 currentLabels: Array<{ name: string; color: { R: number; G: number; B: number } }>;
12 /** Current repo slug, passed as `ref` in refetch query variables. */
13 ref_?: string | null;
14 /** Pre-fetched valid labels for the repository. */
15 validLabels: Array<{ name: string; color: { R: number; G: number; B: number } }>;
16}
17
18// Gear-icon popover in the BugDetailPage sidebar for adding/removing labels.
19// Loads all valid labels from the repo and toggles them via bugChangeLabels.
20// Hidden in read-only mode.
21export function LabelEditor({ bugPrefix, currentLabels, ref_, validLabels }: LabelEditorProps) {
22 const { user } = useAuth();
23 const [changeLabels] = useBugChangeLabelsMutation({
24 refetchQueries: [{ query: BugDetailDocument, variables: { ref: ref_, prefix: bugPrefix } }],
25 });
26
27 const currentNames = new Set(currentLabels.map((l) => l.name));
28
29 async function toggleLabel(name: string) {
30 const isSet = currentNames.has(name);
31 await changeLabels({
32 variables: {
33 input: {
34 prefix: bugPrefix,
35 added: isSet ? [] : [name],
36 Removed: isSet ? [name] : [],
37 },
38 },
39 });
40 }
41
42 return (
43 <div>
44 <div className="mb-2 flex items-center justify-between">
45 <h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
46 Labels
47 </h3>
48 {user && validLabels.length > 0 && (
49 <Popover>
50 <PopoverTrigger asChild>
51 <button className="text-muted-foreground hover:text-foreground">
52 <Settings2 className="size-3.5" />
53 </button>
54 </PopoverTrigger>
55 <PopoverContent align="end" className="w-56 p-2">
56 <p className="text-muted-foreground mb-2 px-2 text-xs font-medium">Apply labels</p>
57 <div className="space-y-1">
58 {validLabels.map((label) => {
59 const active = currentNames.has(label.name);
60 return (
61 <button
62 key={label.name}
63 onClick={() => {
64 void toggleLabel(label.name);
65 }}
66 className="hover:bg-muted flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm"
67 >
68 <span
69 className={`size-2 rounded-full border-2 transition-colors ${
70 active
71 ? "border-transparent"
72 : "border-muted-foreground/40 bg-transparent"
73 }`}
74 style={
75 active
76 ? {
77 backgroundColor: `rgb(${label.color.R},${label.color.G},${label.color.B})`,
78 }
79 : {}
80 }
81 />
82 <LabelBadge name={label.name} color={label.color} />
83 </button>
84 );
85 })}
86 </div>
87 </PopoverContent>
88 </Popover>
89 )}
90 </div>
91
92 {currentLabels.length === 0 ? (
93 <p className="text-muted-foreground text-sm">None yet</p>
94 ) : (
95 <div className="flex flex-wrap gap-1">
96 {currentLabels.map((label) => (
97 <LabelBadge key={label.name} name={label.name} color={label.color} />
98 ))}
99 </div>
100 )}
101 </div>
102 );
103}