mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-03 13:54:55 +03:00
ability to add custom labels to review
This commit is contained in:
parent
8f5b334aea
commit
c7054b7211
@ -1478,7 +1478,8 @@
|
|||||||
"timestamp_style": {
|
"timestamp_style": {
|
||||||
"title": "Timestamp Settings"
|
"title": "Timestamp Settings"
|
||||||
},
|
},
|
||||||
"searchPlaceholder": "Search..."
|
"searchPlaceholder": "Search...",
|
||||||
|
"addCustomLabel": "Add custom label..."
|
||||||
},
|
},
|
||||||
"globalConfig": {
|
"globalConfig": {
|
||||||
"title": "Global Configuration",
|
"title": "Global Configuration",
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export function ReviewLabelSwitchesWidget(props: WidgetProps) {
|
|||||||
getEntities: getReviewLabels,
|
getEntities: getReviewLabels,
|
||||||
getDisplayLabel: getReviewLabelDisplayName,
|
getDisplayLabel: getReviewLabelDisplayName,
|
||||||
i18nKey: "reviewLabels",
|
i18nKey: "reviewLabels",
|
||||||
|
allowCustomEntries: true,
|
||||||
listClassName:
|
listClassName:
|
||||||
"relative max-h-none overflow-visible md:max-h-64 md:overflow-y-auto md:overscroll-contain md:scrollbar-container",
|
"relative max-h-none overflow-visible md:max-h-64 md:overflow-y-auto md:overscroll-contain md:scrollbar-container",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// Generic Switches Widget - Reusable component for selecting from any list of entities
|
// Generic Switches Widget - Reusable component for selecting from any list of entities
|
||||||
import { WidgetProps } from "@rjsf/utils";
|
import { WidgetProps } from "@rjsf/utils";
|
||||||
import { useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@ -43,6 +43,8 @@ export type SwitchesWidgetOptions = {
|
|||||||
listClassName?: string;
|
listClassName?: string;
|
||||||
/** Enable search input to filter the list */
|
/** Enable search input to filter the list */
|
||||||
enableSearch?: boolean;
|
enableSearch?: boolean;
|
||||||
|
/** Allow users to add custom entries not in the predefined list */
|
||||||
|
allowCustomEntries?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeValue(value: unknown): string[] {
|
function normalizeValue(value: unknown): string[] {
|
||||||
@ -122,20 +124,46 @@ export function SwitchesWidget(props: WidgetProps) {
|
|||||||
[props.options],
|
[props.options],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allowCustomEntries = useMemo(
|
||||||
|
() => props.options?.allowCustomEntries as boolean | undefined,
|
||||||
|
[props.options],
|
||||||
|
);
|
||||||
|
|
||||||
const selectedEntities = useMemo(() => normalizeValue(value), [value]);
|
const selectedEntities = useMemo(() => normalizeValue(value), [value]);
|
||||||
const [isOpen, setIsOpen] = useState(selectedEntities.length > 0);
|
const [isOpen, setIsOpen] = useState(selectedEntities.length > 0);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [customEntries, setCustomEntries] = useState<string[]>([]);
|
||||||
|
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(() => {
|
const filteredEntities = useMemo(() => {
|
||||||
if (!enableSearch || !searchTerm.trim()) {
|
if (!enableSearch || !searchTerm.trim()) {
|
||||||
return availableEntities;
|
return allEntities;
|
||||||
}
|
}
|
||||||
const term = searchTerm.toLowerCase();
|
const term = searchTerm.toLowerCase();
|
||||||
return availableEntities.filter((entity) => {
|
return allEntities.filter((entity) => {
|
||||||
const displayLabel = getDisplayLabel(entity, context);
|
const displayLabel = getDisplayLabel(entity, context);
|
||||||
return displayLabel.toLowerCase().includes(term);
|
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) => {
|
const toggleEntity = (entity: string, enabled: boolean) => {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
@ -181,7 +209,7 @@ export function SwitchesWidget(props: WidgetProps) {
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
<CollapsibleContent className="rounded-lg border border-input bg-secondary pb-1 pr-0 pt-2 md:max-w-md">
|
<CollapsibleContent className="rounded-lg border border-input bg-secondary pb-1 pr-0 pt-2 md:max-w-md">
|
||||||
{availableEntities.length === 0 ? (
|
{allEntities.length === 0 && !allowCustomEntries ? (
|
||||||
<div className="text-sm text-muted-foreground">{emptyMessage}</div>
|
<div className="text-sm text-muted-foreground">{emptyMessage}</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -223,6 +251,26 @@ export function SwitchesWidget(props: WidgetProps) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{allowCustomEntries && !disabled && !readonly && (
|
||||||
|
<div className="mx-2 mt-2 pb-1">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t?.("configForm.addCustomLabel", {
|
||||||
|
ns: "views/settings",
|
||||||
|
defaultValue: "Add custom label...",
|
||||||
|
})}
|
||||||
|
value={customInput}
|
||||||
|
onChange={(e) => setCustomInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
addCustomEntry();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={addCustomEntry}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user