import { useState, type ChangeEvent, type FocusEvent } from "react"; import { PlusIcon } from "lucide-react"; import { Input } from "../components/input"; import { Badge } from "../components/badge"; import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList, CommandSeparator, } from "../components/command"; import { Popover, PopoverAnchor, PopoverContent } from "../components/popover"; import { useScopeStack } from "../api/scope-context"; export interface SecretPickerSecret { readonly id: string; readonly scopeId: string; readonly name: string; readonly provider?: string; } const PROVIDER_LABELS: Record = { keychain: "Keychain", file: "Local", memory: "Memory", onepassword: "1Password", }; const providerLabel = (key: string | undefined): string => { if (!key) return "Local"; return PROVIDER_LABELS[key] ?? key; }; export function SecretPicker(props: { readonly value: string | null; readonly valueScopeId?: string; readonly onSelect: (secretId: string, scopeId: string) => void; readonly secrets: readonly SecretPickerSecret[]; readonly placeholder?: string; /** When provided, renders a "+ New secret" row at the top of the dropdown. */ readonly onCreateNew?: () => void; }) { const { value, valueScopeId, onSelect, secrets, placeholder = "Search secrets…", onCreateNew, } = props; const [open, setOpen] = useState(false); const [query, setQuery] = useState(""); const scopeStack = useScopeStack(); const scopeLabel = (scopeId: string): string => { const index = scopeStack.findIndex((entry) => String(entry.id) === scopeId); if (index === 0) return "Personal"; if (index > 0) return scopeStack[index]?.name || "Organization"; return "Scoped"; }; const selected = secrets.find( (secret) => secret.id === value && (valueScopeId === undefined || secret.scopeId === valueScopeId), ) ?? secrets.find((secret) => secret.id === value) ?? null; const grouped = new Map(); for (const secret of secrets) { const key = providerLabel(secret.provider); const group = grouped.get(key); if (group) { group.push(secret); } else { grouped.set(key, [secret]); } } const groups: [string, SecretPickerSecret[]][] = [...grouped.entries()] .map(([label, items]): [string, SecretPickerSecret[]] => [ label, [...items].sort((a, b) => a.name.localeCompare(b.name)), ]) .sort(([a], [b]) => a.localeCompare(b)); const showGroupHeadings = groups.length > 1; return (
) => { setQuery(event.target.value); if (!open) setOpen(true); }} onFocus={() => setOpen(true)} onBlur={(event: FocusEvent) => { const related = event.relatedTarget as HTMLElement | null; if (related?.closest("[data-slot=popover-content]")) return; setOpen(false); }} placeholder={placeholder} className="text-sm" /> event.preventDefault()} onCloseAutoFocus={(event: Event) => event.preventDefault()} onInteractOutside={(event: Event) => { const target = event.target as HTMLElement | null; if (target?.closest("[data-slot=popover-anchor]")) { event.preventDefault(); } }} > No secrets found {onCreateNew && ( <> { onCreateNew(); setOpen(false); setQuery(""); }} className="text-muted-foreground data-[selected=true]:text-foreground" > New secret {secrets.length > 0 && } )} {groups.map(([label, items]) => { const lowerQuery = query.toLowerCase(); const filtered = lowerQuery ? items.filter( (secret) => secret.name.toLowerCase().includes(lowerQuery) || secret.id.toLowerCase().includes(lowerQuery), ) : items; if (filtered.length === 0) return null; return ( {filtered.map((secret) => ( { onSelect(secret.id, secret.scopeId); setOpen(false); setQuery(""); }} > {secret.name} {scopeLabel(secret.scopeId)} ))} ); })}
); }