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