Files
tool-evaluator/artifacts/toolrate/src/components/category-combobox.tsx
T
opencode db397a14bc
Build & Push Docker Image / build (push) Successful in 4m32s
fix: security hardening, validation, cache and analytics fixes
Backend security:
- Admin-gate /admin/redundancy (GET+POST) with zod validation and tool existence checks
- Restrict CORS to same-origin (plus CORS_ORIGIN allowlist) and SameSite=Lax cookie
- Validate returnTo to prevent open redirect in the OIDC flow
- Validate/coerce relations body, reject self-relations and non-admin 'recommended'
- Add central JSON error middleware (no more Express HTML 500s)
- Fail fast at startup when SESSION_SECRET/VOTER_SECRET missing in production

Backend correctness:
- Stop leaking voterToken in the create-rating response
- Allow clearing websiteUrl/iconUrl (nullable in UpdateToolBody, frontend sends null)
- Regenerate session after login/callback (session fixation) and add OIDC state check
- Block self-demotion and last-admin demotion in user PATCH
- Set created_by to NULL on user delete (FK-safe)
- Validate cost create/update bodies with zod
- Unique index (tool_id, voter_token) + 409 on race duplicate ratings
- Clamp audit limit, escape ilike wildcards in search, O(N) analytics queries

Frontend:
- tools-browse reads and syncs URL query params (fixes home 'View all' links)
- Invalidate analytics/top-tools/categories/features caches after mutations
- Sync category combobox input when the value changes externally
- Hide Write a Review for anonymous users, drop unreachable rating guard
2026-08-01 19:11:00 +02:00

124 lines
3.7 KiB
TypeScript

import { useState, useEffect } from "react";
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 = "Select or type a category..." }: CategoryComboboxProps) {
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()
);
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 || placeholder}
</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="Search or enter new category..."
value={inputValue}
onValueChange={(v) => {
setInputValue(v);
onChange(v);
}}
data-testid="input-category-search"
/>
<CommandList>
{filtered.length === 0 && !showCreateOption && (
<CommandEmpty>No categories found.</CommandEmpty>
)}
{filtered.length > 0 && (
<CommandGroup heading="Known categories">
{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="Create new">
<CommandItem
value={inputValue}
onSelect={() => select(inputValue.trim())}
data-testid="item-category-create-new"
>
<span className="text-primary font-medium">+ Create</span>
<span className="ml-2 text-muted-foreground">&ldquo;{inputValue.trim()}&rdquo;</span>
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}