mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 14:47:40 +03:00
reuse widget and refactor for multiline
This commit is contained in:
parent
b1732a1767
commit
0bd2bdc1c1
@ -29,9 +29,10 @@ const review: SectionConfigOverrides = {
|
|||||||
},
|
},
|
||||||
genai: {
|
genai: {
|
||||||
additional_concerns: {
|
additional_concerns: {
|
||||||
"ui:widget": "textareaArray",
|
"ui:widget": "ArrayAsTextWidget",
|
||||||
"ui:options": {
|
"ui:options": {
|
||||||
size: "full",
|
size: "full",
|
||||||
|
multiline: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
activity_context_prompt: {
|
activity_context_prompt: {
|
||||||
|
|||||||
@ -30,7 +30,6 @@ import { CameraPathWidget } from "./widgets/CameraPathWidget";
|
|||||||
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget";
|
||||||
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
import { SemanticSearchModelWidget } from "./widgets/SemanticSearchModelWidget";
|
||||||
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
import { OnvifProfileWidget } from "./widgets/OnvifProfileWidget";
|
||||||
import { TextareaArrayWidget } from "./widgets/TextareaArrayWidget";
|
|
||||||
|
|
||||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||||
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
||||||
@ -82,7 +81,6 @@ export const frigateTheme: FrigateTheme = {
|
|||||||
optionalField: OptionalFieldWidget,
|
optionalField: OptionalFieldWidget,
|
||||||
semanticSearchModel: SemanticSearchModelWidget,
|
semanticSearchModel: SemanticSearchModelWidget,
|
||||||
onvifProfile: OnvifProfileWidget,
|
onvifProfile: OnvifProfileWidget,
|
||||||
textareaArray: TextareaArrayWidget,
|
|
||||||
},
|
},
|
||||||
templates: {
|
templates: {
|
||||||
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
||||||
|
|||||||
@ -1,33 +1,101 @@
|
|||||||
// Widget that displays an array as a concatenated text string
|
// Widget that displays an array as editable text.
|
||||||
|
// Single-line mode (default): space-separated in an Input.
|
||||||
|
// Multiline mode (options.multiline): one item per line in a Textarea.
|
||||||
import type { WidgetProps } from "@rjsf/utils";
|
import type { WidgetProps } from "@rjsf/utils";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { useCallback } from "react";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getSizedFieldClassName } from "../utils";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
function arrayToText(value: unknown, multiline: boolean): string {
|
||||||
|
const sep = multiline ? "\n" : " ";
|
||||||
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
|
return value.join(sep);
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function textToArray(text: string, multiline: boolean): string[] {
|
||||||
|
if (text.trim() === "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return multiline
|
||||||
|
? text.split("\n").filter((line) => line.trim() !== "")
|
||||||
|
: text.trim().split(/\s+/);
|
||||||
|
}
|
||||||
|
|
||||||
export function ArrayAsTextWidget(props: WidgetProps) {
|
export function ArrayAsTextWidget(props: WidgetProps) {
|
||||||
const { value, onChange, disabled, readonly, placeholder } = props;
|
const {
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
readonly,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
placeholder,
|
||||||
|
schema,
|
||||||
|
options,
|
||||||
|
} = props;
|
||||||
|
|
||||||
// Convert array or string to text
|
const multiline = !!(options.multiline as boolean);
|
||||||
let textValue = "";
|
|
||||||
if (typeof value === "string" && value.length > 0) {
|
|
||||||
textValue = value;
|
|
||||||
} else if (Array.isArray(value) && value.length > 0) {
|
|
||||||
textValue = value.join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
// Local state keeps raw text so newlines aren't stripped mid-typing
|
||||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
const [text, setText] = useState(() => arrayToText(value, multiline));
|
||||||
const newText = event.target.value;
|
|
||||||
// Convert space-separated string back to array
|
useEffect(() => {
|
||||||
const newArray = newText.trim() ? newText.trim().split(/\s+/) : [];
|
setText(arrayToText(value, multiline));
|
||||||
onChange(newArray);
|
}, [value, multiline]);
|
||||||
|
|
||||||
|
const fieldClassName = multiline
|
||||||
|
? getSizedFieldClassName(options, "md")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
setText(raw);
|
||||||
|
onChange(textToArray(raw, multiline));
|
||||||
},
|
},
|
||||||
[onChange],
|
[onChange, multiline],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(
|
||||||
|
(e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
// Clean up: strip empty entries and sync
|
||||||
|
const cleaned = textToArray(e.target.value, multiline);
|
||||||
|
onChange(cleaned);
|
||||||
|
setText(arrayToText(cleaned, multiline));
|
||||||
|
onBlur?.(id, e.target.value);
|
||||||
|
},
|
||||||
|
[id, onChange, onBlur, multiline],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (multiline) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
id={id}
|
||||||
|
className={cn("text-md", fieldClassName)}
|
||||||
|
value={text}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
rows={(options.rows as number) || 3}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={(e) => onFocus?.(id, e.target.value)}
|
||||||
|
aria-label={schema.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
value={textValue}
|
value={text}
|
||||||
onChange={handleChange}
|
onChange={handleInputChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readOnly={readonly}
|
readOnly={readonly}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@ -1,50 +0,0 @@
|
|||||||
// Textarea Array Widget - displays an array of strings as a multiline textarea
|
|
||||||
// Each line in the textarea corresponds to one item in the array.
|
|
||||||
import type { WidgetProps } from "@rjsf/utils";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { getSizedFieldClassName } from "../utils";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
export function TextareaArrayWidget(props: WidgetProps) {
|
|
||||||
const { id, value, disabled, readonly, onChange, onBlur, onFocus, schema, options } =
|
|
||||||
props;
|
|
||||||
|
|
||||||
// Convert array to newline-separated text for display
|
|
||||||
let textValue = "";
|
|
||||||
if (Array.isArray(value) && value.length > 0) {
|
|
||||||
textValue = value.join("\n");
|
|
||||||
} else if (typeof value === "string") {
|
|
||||||
textValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldClassName = getSizedFieldClassName(options, "md");
|
|
||||||
|
|
||||||
const handleChange = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
const text = e.target.value;
|
|
||||||
if (text === "") {
|
|
||||||
onChange([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Split by newlines and filter out empty lines
|
|
||||||
const items = text.split("\n").filter((line) => line.trim() !== "");
|
|
||||||
onChange(items);
|
|
||||||
},
|
|
||||||
[onChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Textarea
|
|
||||||
id={id}
|
|
||||||
className={cn("text-md", fieldClassName)}
|
|
||||||
value={textValue}
|
|
||||||
disabled={disabled || readonly}
|
|
||||||
rows={(options.rows as number) || 3}
|
|
||||||
onChange={handleChange}
|
|
||||||
onBlur={(e) => onBlur(id, e.target.value)}
|
|
||||||
onFocus={(e) => onFocus(id, e.target.value)}
|
|
||||||
aria-label={schema.title}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user