diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 1d1e507ef..f32244cb4 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1478,7 +1478,8 @@ "timestamp_style": { "title": "Timestamp Settings" }, - "searchPlaceholder": "Search..." + "searchPlaceholder": "Search...", + "addCustomLabel": "Add custom label..." }, "globalConfig": { "title": "Global Configuration", diff --git a/web/src/components/config-form/theme/widgets/ReviewLabelSwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/ReviewLabelSwitchesWidget.tsx index 66ec79bc2..857450534 100644 --- a/web/src/components/config-form/theme/widgets/ReviewLabelSwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/ReviewLabelSwitchesWidget.tsx @@ -75,6 +75,7 @@ export function ReviewLabelSwitchesWidget(props: WidgetProps) { getEntities: getReviewLabels, getDisplayLabel: getReviewLabelDisplayName, i18nKey: "reviewLabels", + allowCustomEntries: true, listClassName: "relative max-h-none overflow-visible md:max-h-64 md:overflow-y-auto md:overscroll-contain md:scrollbar-container", }} diff --git a/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx b/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx index da8869212..c4b324d6c 100644 --- a/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/SwitchesWidget.tsx @@ -1,6 +1,6 @@ // Generic Switches Widget - Reusable component for selecting from any list of entities import { WidgetProps } from "@rjsf/utils"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -43,6 +43,8 @@ export type SwitchesWidgetOptions = { listClassName?: string; /** Enable search input to filter the list */ enableSearch?: boolean; + /** Allow users to add custom entries not in the predefined list */ + allowCustomEntries?: boolean; }; function normalizeValue(value: unknown): string[] { @@ -122,20 +124,46 @@ export function SwitchesWidget(props: WidgetProps) { [props.options], ); + const allowCustomEntries = useMemo( + () => props.options?.allowCustomEntries as boolean | undefined, + [props.options], + ); + const selectedEntities = useMemo(() => normalizeValue(value), [value]); const [isOpen, setIsOpen] = useState(selectedEntities.length > 0); const [searchTerm, setSearchTerm] = useState(""); + const [customEntries, setCustomEntries] = useState([]); + const [customInput, setCustomInput] = useState(""); + + const allEntities = useMemo(() => { + if (customEntries.length === 0) { + return availableEntities; + } + const merged = new Set([...availableEntities, ...customEntries]); + return [...merged].sort(); + }, [availableEntities, customEntries]); const filteredEntities = useMemo(() => { if (!enableSearch || !searchTerm.trim()) { - return availableEntities; + return allEntities; } const term = searchTerm.toLowerCase(); - return availableEntities.filter((entity) => { + return allEntities.filter((entity) => { const displayLabel = getDisplayLabel(entity, context); return displayLabel.toLowerCase().includes(term); }); - }, [availableEntities, searchTerm, enableSearch, getDisplayLabel, context]); + }, [allEntities, searchTerm, enableSearch, getDisplayLabel, context]); + + const addCustomEntry = useCallback(() => { + const trimmed = customInput.trim().toLowerCase(); + if (!trimmed || allEntities.includes(trimmed)) { + setCustomInput(""); + return; + } + setCustomEntries((prev) => [...prev, trimmed]); + onChange([...selectedEntities, trimmed]); + setCustomInput(""); + }, [customInput, allEntities, selectedEntities, onChange]); const toggleEntity = (entity: string, enabled: boolean) => { if (enabled) { @@ -181,7 +209,7 @@ export function SwitchesWidget(props: WidgetProps) { - {availableEntities.length === 0 ? ( + {allEntities.length === 0 && !allowCustomEntries ? (
{emptyMessage}
) : ( <> @@ -223,6 +251,26 @@ export function SwitchesWidget(props: WidgetProps) { ); })} + {allowCustomEntries && !disabled && !readonly && ( +
+ setCustomInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + addCustomEntry(); + } + }} + onBlur={addCustomEntry} + /> +
+ )} )}