Files
tool-evaluator/artifacts/toolrate/src/components/category-combobox.tsx
T
opencode 3de59ccaaf
Build & Push Docker Image / build (push) Successful in 2m33s
feat(i18n): full app + docs localization (de/en)
- translate remaining pages/components (admin, analytics, redundancy,
  trash, compare, browse, watchlist, admin tools tab, category combobox,
  theme toggle, tool preview card, tool-new/edit) to t() calls
- locales: 453 keys each, parity verified
- docs: bilingual handbook + release notes via *.en.md variants,
  language-aware docs.tsx (markdown paths, nav titles, search index),
  LanguageSwitcher in docs header
- generate-docs: emit per-locale handbook/release/search output,
  localized index.json fields (fileEn/titleEn), en-aware snapshots
2026-08-04 08:35:05 +02:00

128 lines
3.9 KiB
TypeScript

import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { useListCategories, getListCategoriesQueryKey } from "@workspace/api-client-react";
interface CategoryComboboxProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function CategoryCombobox({ value, onChange, placeholder }: CategoryComboboxProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [inputValue, setInputValue] = useState(value);
useEffect(() => {
setInputValue(value);
}, [value]);
const categories = useListCategories({
query: {
queryKey: getListCategoriesQueryKey(),
refetchOnMount: "always",
staleTime: 0,
},
});
const known: string[] = categories.data ?? [];
const filtered = inputValue.trim()
? known.filter((c) => c.toLowerCase().includes(inputValue.toLowerCase()))
: known;
const showCreateOption = inputValue.trim() !== "" && !known.some(
(c) => c.toLowerCase() === inputValue.toLowerCase()
);
const resolvedPlaceholder = placeholder ?? t("category.placeholder");
function select(val: string) {
onChange(val);
setInputValue(val);
setOpen(false);
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between font-normal h-9"
data-testid="button-category-combobox"
>
<span className={cn(!value && "text-muted-foreground")}>
{value || resolvedPlaceholder}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command shouldFilter={false}>
<CommandInput
placeholder={t("category.searchPlaceholder")}
value={inputValue}
onValueChange={(v) => {
setInputValue(v);
onChange(v);
}}
data-testid="input-category-search"
/>
<CommandList>
{filtered.length === 0 && !showCreateOption && (
<CommandEmpty>{t("category.noCategories")}</CommandEmpty>
)}
{filtered.length > 0 && (
<CommandGroup heading={t("category.knownCategories")}>
{filtered.map((cat) => (
<CommandItem
key={cat}
value={cat}
onSelect={() => select(cat)}
data-testid={`item-category-${cat}`}
>
<Check
className={cn("mr-2 h-4 w-4", value === cat ? "opacity-100" : "opacity-0")}
/>
{cat}
</CommandItem>
))}
</CommandGroup>
)}
{showCreateOption && (
<CommandGroup heading={t("category.createNew")}>
<CommandItem
value={inputValue}
onSelect={() => select(inputValue.trim())}
data-testid="item-category-create-new"
>
<span className="text-primary font-medium">{t("category.create")}</span>
<span className="ml-2 text-muted-foreground">&ldquo;{inputValue.trim()}&rdquo;</span>
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}