LabelEditor.tsx

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