LabelEditor.tsx

  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}