LabelEditor.tsx

  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}