import { useState } from "react" import DOMPurify from "dompurify" import { ChevronDownIcon, EyeIcon, EyeOffIcon, HelpCircleIcon } from "lucide-react" import { marked } from "marked" import { useTranslation } from "react-i18next" import { Button } from "@/components/ui/button" import { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxContent, ComboboxEmpty, ComboboxItem, ComboboxList, ComboboxValue, } from "@/components/ui/combobox" import { useComboboxAnchor } from "@/components/ui/combobox-anchor" import { Field, FieldError, FieldLabel } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { normalizePluginChoices } from "@/components/main/providers/utils" import type { ProviderPluginConfig } from "@/components/main/providers/types" export type SchemaFormValues = Record export type SchemaFieldErrorMap = Record interface SchemaFormFieldsProps { schema: ProviderPluginConfig[] values: SchemaFormValues fieldErrors?: SchemaFieldErrorMap onValueChange: (name: string, value: unknown) => void } interface CheckboxFieldProps { field: ProviderPluginConfig selectedValue: unknown isInvalid: boolean onValueChange: (name: string, value: unknown) => void } function renderSanitizedTips(markdown: string) { const parsed = marked.parse(markdown) const html = typeof parsed === "string" ? parsed : markdown return DOMPurify.sanitize(html) } function CheckboxField({ field, selectedValue, isInvalid, onValueChange, }: CheckboxFieldProps) { const { t } = useTranslation() const anchorRef = useComboboxAnchor() const [open, setOpen] = useState(false) const choices = normalizePluginChoices(field.choices) const optionValueMap = new Map( choices.map((choice) => [String(choice.value), choice.value] as const) ) const options = choices.map((choice) => String(choice.value)) const selectedValues = Array.isArray(selectedValue) ? selectedValue.map((value) => String(value)) : [] const getLabelByValue = (value: string) => { const matched = choices.find((choice) => String(choice.value) === value) return matched?.label ?? value } return ( { const next = Array.isArray(nextValue) ? nextValue.map((value) => optionValueMap.get(String(value)) ?? value) : [] onValueChange(field.name, next) }} open={open} onOpenChange={setOpen} > setOpen(true)} > {(currentValue) => { const selectedItems = Array.isArray(currentValue) ? currentValue.map((value) => String(value)) : [] if (selectedItems.length === 0) { return ( {field.message || field.name} ) } return selectedItems.map((value) => ( {getLabelByValue(value)} )) }} {t("NO_OPTIONS_FOUND")} {(item) => { const value = String(item) return ( {getLabelByValue(value)} ) }} ) } export function SchemaFormFields({ schema, values, fieldErrors = {}, onValueChange, }: SchemaFormFieldsProps) { const [visiblePasswords, setVisiblePasswords] = useState>( {} ) return (
{schema.map((field) => { const value = values[field.name] const choices = normalizePluginChoices(field.choices) const optionValueMap = new Map( choices.map((choice) => [String(choice.value), choice.value] as const) ) const isPasswordVisible = Boolean(visiblePasswords[field.name]) const fieldError = fieldErrors[field.name] const isInvalid = Boolean(fieldError) // For list fields: pre-compute the string-form value and whether it // matches any choice, so the Select renders the placeholder when the // current value is stale (e.g. after a cascade refresh removed the // option). The `choicesKey` is appended to the Select's React key so // the component remounts when the available choices change, dropping // any internal "active item" state that points at a now-removed // SelectItem (which is what blocks the dropdown from opening). const listStringValue = value === null || value === undefined ? "" : String(value) const listValueMatchesChoice = choices.some( (choice) => String(choice.value) === listStringValue ) const listSelectValue = listValueMatchesChoice ? listStringValue : "" const choicesKey = choices .map((choice) => String(choice.value)) .join("|") return ( {field.alias || field.name} {field.required ? * : null} {field.tips ? (
) : null} {field.type === "input" && ( onValueChange(field.name, event.target.value)} /> )} {field.type === "password" && (
onValueChange(field.name, event.target.value)} />
)} {field.type === "list" && ( )} {field.type === "checkbox" && ( )} {field.type === "confirm" && (
{value ? field.confirmText || "yes" : field.cancelText || "no"} onValueChange(field.name, checked)} />
)} {field.type === "editor" && (