Miscellaneous fixes (0.18 beta) (#23892)
CI / AMD64 Build (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s

* update network requirements docs for keras weights download

* fix manual PTZ relative moves permanently stopping object detection

* document available camera set features and link profiles docs to the API

* fix stale stream name field when switching cameras

The live streams and known plates fields rendered the map key as an uncontrolled input, so switching cameras left the previous camera's stream name on screen and would rename the wrong key if that stale text was committed. Both now use a shared MapKeyInput that resyncs with the form data and commits per keystroke, except while the typed name belongs to another entry, so the section is marked modified without waiting for blur.
This commit is contained in:
Josh Hawkins
2026-08-03 08:18:28 -05:00
committed by GitHub
parent 4f2a297745
commit 3b14ec0c87
13 changed files with 544 additions and 32 deletions
@@ -5,6 +5,7 @@
import { canExpand } from "@rjsf/utils";
import type { RJSFSchema, UiSchema } from "@rjsf/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { LuPlus, LuChevronDown, LuChevronRight } from "react-icons/lu";
import { useTranslation } from "react-i18next";
import {
@@ -12,7 +13,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import type { ReactNode } from "react";
import { useEffect, useState, type ReactNode } from "react";
interface AddPropertyButtonProps {
/** Callback fired when the add button is clicked */
@@ -67,6 +68,72 @@ export function AddPropertyButton({
);
}
interface MapKeyInputProps {
/** DOM id used for label association */
id: string;
/** The committed key as it exists in the form data */
value: string;
/** Placeholder shown when the input is empty */
placeholder?: string;
/** Whether the input is disabled */
disabled?: boolean;
/** Additional class names */
className?: string;
/** Called with the edited key when it is safe to commit */
onCommit: (next: string) => void;
/** Whether another entry already uses this key, which defers the commit */
isKeyTaken?: (next: string) => boolean;
}
/**
* Text input for the key of a map entry (e.g. a live stream name).
*
* The edit is kept in local state so that the draft can be re-synced whenever
* the committed key changes underneath the input, which is what happens when
* the selected camera changes while the field stays mounted.
*
* Each keystroke is committed so the section is marked as modified right away,
* except while the typed key belongs to another entry: renaming onto an
* existing key merges the two entries, so a name typed through a neighbor's
* name would silently drop it. Those keystrokes stay local until the key is
* free again or the input is blurred.
*/
export function MapKeyInput({
id,
value,
placeholder,
disabled,
className,
onCommit,
isKeyTaken,
}: MapKeyInputProps) {
const [draft, setDraft] = useState(value);
useEffect(() => {
setDraft(value);
}, [value]);
const handleChange = (next: string) => {
setDraft(next);
if (!isKeyTaken?.(next)) {
onCommit(next);
}
};
return (
<Input
id={id}
value={draft}
placeholder={placeholder}
disabled={disabled}
className={className}
onChange={(e) => handleChange(e.target.value)}
onBlur={() => onCommit(draft)}
/>
);
}
interface AdvancedCollapsibleProps {
/** Number of advanced fields */
count: number;
@@ -19,6 +19,7 @@ import {
import type { ConfigFormContext } from "@/types/configForm";
import get from "lodash/get";
import { isSubtreeModified } from "../utils";
import { MapKeyInput } from "../components";
type KnownPlatesData = Record<string, string[]>;
@@ -194,12 +195,16 @@ export function KnownPlatesField(props: FieldProps) {
className="space-y-2 rounded-md border p-3"
>
<div className="flex items-center gap-2">
<Input
<MapKeyInput
id={`${entryId}-key`}
defaultValue={key}
value={key}
placeholder={namePlaceholder}
disabled={disabled || readonly}
onBlur={(e) => handleRenameKey(key, e.target.value)}
onCommit={(next) => handleRenameKey(key, next)}
isKeyTaken={(next) =>
next !== key &&
Object.prototype.hasOwnProperty.call(data, next)
}
className="flex-1"
/>
<Button
@@ -3,7 +3,6 @@ import { useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Command,
@@ -20,6 +19,7 @@ import {
import { cn } from "@/lib/utils";
import { Check, ChevronsUpDown, Plus } from "lucide-react";
import { LuPlus, LuTrash2 } from "react-icons/lu";
import { MapKeyInput } from "../components";
import type { ConfigFormContext } from "@/types/configForm";
import get from "lodash/get";
import { isSubtreeModified } from "../utils";
@@ -288,12 +288,16 @@ export function LiveStreamsField(props: FieldProps) {
>
<div className="col-span-12 space-y-2 md:col-span-5">
<Label htmlFor={`${entryId}-key`}>{streamNameLabel}</Label>
<Input
<MapKeyInput
id={`${entryId}-key`}
defaultValue={key}
value={key}
placeholder={streamNamePlaceholder}
disabled={disabled || readonly}
onBlur={(e) => handleRenameKey(key, e.target.value)}
onCommit={(next) => handleRenameKey(key, next)}
isKeyTaken={(next) =>
next !== key &&
Object.prototype.hasOwnProperty.call(data, next)
}
/>
</div>
<div className="col-span-10 space-y-2 md:col-span-6">