preserve form data when changing cameras

This commit is contained in:
Josh Hawkins 2026-02-08 18:12:46 -06:00
parent a48304d3a2
commit c90a547ec9
2 changed files with 49 additions and 24 deletions

View File

@ -178,8 +178,7 @@ export function ConfigSection({
const [dirtyOverrides, setDirtyOverrides] = useState<JsonValue | undefined>(
undefined,
);
const [baselineFormData, setBaselineFormData] =
useState<ConfigSectionData | null>(null);
const baselineByKeyRef = useRef<Record<string, ConfigSectionData>>({});
const pendingData =
pendingDataBySection !== undefined
@ -202,6 +201,7 @@ export function ConfigSection({
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const isResettingRef = useRef(false);
const isInitializingRef = useRef(true);
const lastPendingDataKeyRef = useRef<string | null>(null);
const updateTopic =
level === "camera" && cameraName
@ -268,6 +268,23 @@ export function ConfigSection({
return sanitizeSectionData(baseData);
}, [rawFormData, modifiedSchema, sanitizeSectionData]);
const baselineSnapshot = useMemo(() => {
if (!pendingData) {
const snapshot = cloneDeep(formData as ConfigSectionData);
baselineByKeyRef.current[pendingDataKey] = snapshot;
return snapshot;
}
const cached = baselineByKeyRef.current[pendingDataKey];
if (cached) {
return cached;
}
const snapshot = cloneDeep(formData as ConfigSectionData);
baselineByKeyRef.current[pendingDataKey] = snapshot;
return snapshot;
}, [formData, pendingData, pendingDataKey]);
const schemaDefaults = useMemo(() => {
if (!modifiedSchema) {
return {};
@ -297,21 +314,29 @@ export function ConfigSection({
// This prevents RJSF's initial onChange call from being treated as a user edit
// Only clear if pendingData is managed locally (not by parent)
useEffect(() => {
if (!pendingData) {
const pendingKeyChanged = lastPendingDataKeyRef.current !== pendingDataKey;
if (pendingKeyChanged) {
lastPendingDataKeyRef.current = pendingDataKey;
isInitializingRef.current = true;
setPendingOverrides(undefined);
setDirtyOverrides(undefined);
} else if (!pendingData) {
isInitializingRef.current = true;
setPendingOverrides(undefined);
setDirtyOverrides(undefined);
setBaselineFormData(cloneDeep(formData as ConfigSectionData));
}
if (onPendingDataChange === undefined) {
setPendingData(null);
}
}, [
formData,
pendingData,
setPendingData,
setBaselineFormData,
onPendingDataChange,
pendingData,
pendingDataKey,
setPendingData,
setDirtyOverrides,
setPendingOverrides,
]);
useEffect(() => {
@ -344,7 +369,7 @@ export function ConfigSection({
return;
}
const sanitizedData = sanitizeSectionData(data as ConfigSectionData);
let nextBaselineFormData = baselineFormData ?? formData;
const nextBaselineFormData = baselineSnapshot;
const overrides = buildOverrides(
sanitizedData,
compareBaseData,
@ -353,16 +378,6 @@ export function ConfigSection({
setPendingOverrides(overrides as JsonValue | undefined);
if (isInitializingRef.current && !pendingData) {
isInitializingRef.current = false;
if (!baselineFormData) {
// Always use formData (server data + schema defaults) for the
// baseline snapshot, NOT sanitizedData from the onChange callback.
// If a custom component (e.g., zone checkboxes) triggers onChange
// before RJSF's initial onChange, sanitizedData would include the
// user's modification, corrupting the baseline.
const baselineSnapshot = cloneDeep(formData as ConfigSectionData);
setBaselineFormData(baselineSnapshot);
nextBaselineFormData = baselineSnapshot;
}
if (overrides === undefined) {
setPendingData(null);
setPendingOverrides(undefined);
@ -392,14 +407,12 @@ export function ConfigSection({
setPendingData,
setPendingOverrides,
setDirtyOverrides,
baselineFormData,
setBaselineFormData,
formData,
baselineSnapshot,
],
);
const currentFormData = pendingData || formData;
const effectiveBaselineFormData = baselineFormData ?? formData;
const effectiveBaselineFormData = baselineSnapshot;
const currentOverrides = useMemo(() => {
if (!currentFormData || typeof currentFormData !== "object") {

View File

@ -6,7 +6,7 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Children, useState, useEffect } from "react";
import { Children, useState, useEffect, useRef } from "react";
import type { ReactNode } from "react";
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
import { useTranslation } from "react-i18next";
@ -142,6 +142,10 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
const hasModifiedDescendants = checkSubtreeModified(fieldPath);
const [isOpen, setIsOpen] = useState(hasModifiedDescendants);
const resetKey = `${formContext?.level ?? "global"}::${
formContext?.cameraName ?? "global"
}`;
const lastResetKeyRef = useRef<string | null>(null);
// Auto-expand collapsible when modifications are detected
useEffect(() => {
@ -191,6 +195,14 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
setShowAdvanced(true);
}
}, [hasModifiedAdvanced]);
useEffect(() => {
if (lastResetKeyRef.current !== resetKey) {
lastResetKeyRef.current = resetKey;
setIsOpen(hasModifiedDescendants);
setShowAdvanced(hasModifiedAdvanced);
}
}, [resetKey, hasModifiedDescendants, hasModifiedAdvanced]);
const { children } = props as ObjectFieldTemplateProps & {
children?: ReactNode;
};