import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { orgQueries } from "@app/lib/queries"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import { useQuery } from "@tanstack/react-query"; import type { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; import { useActionState, useMemo, useRef, useState } from "react"; import { useDebounce } from "use-debounce"; import { Button } from "./ui/button"; import { Checkbox } from "./ui/checkbox"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/command"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; export type SelectedLabel = { name: string; color: string; labelId: number; }; export type LabelsSelectorProps = { orgId: string; selectedLabels: SelectedLabel[]; toggleLabel: (newlabel: SelectedLabel, action: "detach" | "attach") => void; }; export const LABEL_COLORS = { red: "#ff6467", green: "#05df72", blue: "#51a2ff", yellow: "#fdc744", orange: "#ff8905", purple: "#a684ff", gray: "#b4b4b4" }; export function LabelsSelector({ orgId, selectedLabels, toggleLabel }: LabelsSelectorProps) { const t = useTranslations(); const [labelSearchQuery, setlabelsSearchQuery] = useState(""); const [debouncedQuery] = useDebounce(labelSearchQuery, 150); const api = createApiClient(useEnvContext()); const { data: labels = [] } = useQuery( orgQueries.labels({ orgId, query: debouncedQuery, perPage: 10 }) ); const labelsShown = useMemo(() => { const base = [...labels]; if (debouncedQuery.trim().length === 0 && selectedLabels.length > 0) { const selectedNotInBase = selectedLabels.filter( (sel) => !base.some((s) => s.labelId === sel.labelId) ); return [...selectedNotInBase, ...base]; } return base; }, [debouncedQuery, labels, selectedLabels]); const selectedIds = useMemo( () => new Set(selectedLabels.map((s) => s.labelId)), [selectedLabels] ); const colorValues = Object.values(LABEL_COLORS); const randomColor = colorValues[Math.floor(Math.random() * colorValues.length)]; const [, action, isPending] = useActionState(createLabel, null); const createFormRef = useRef(null); const trimmedQuery = labelSearchQuery.trim(); const canCreateLabel = trimmedQuery.length > 0 && labelsShown.length === 0 && !isPending; async function createLabel(_: any, formData: FormData) { const name = formData.get("name")?.toString(); const color = formData.get("color")?.toString(); try { const res = await api.post< AxiosResponse >(`/org/${orgId}/labels`, { name, color }); const { label } = res.data.data; toggleLabel( { labelId: label.labelId, name: label.name, color: label.color }, "attach" ); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e, t("errorOccurred")), variant: "destructive" }); } setlabelsSearchQuery(""); } return ( { if (e.key === "Enter" && canCreateLabel) { e.preventDefault(); createFormRef.current?.requestSubmit(); } }} /> {labelSearchQuery.trim().length > 0 ? (
{t("createNewLabel", { label: labelSearchQuery.trim() })}
) : (
{t("labelsNotFound")} {t("labelsEmptyCreateHint")}
)}
{labelsShown.map((label) => ( { toggleLabel( label, selectedIds.has(label.labelId) ? "detach" : "attach" ); }} > { e.stopPropagation(); }} onPointerDown={(e) => { e.stopPropagation(); }} onCheckedChange={(checked) => { toggleLabel( label, checked ? "attach" : "detach" ); }} aria-hidden tabIndex={-1} />
{label.name}
))}
); }