From 5f61079f811fa63848cd33a219fac3095130a26d Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:35:03 -0500 Subject: [PATCH] fix save button race conditions, add reset spinner, and fix enrichments profile leak - Disable both Save and SaveAll buttons while either operation is in progress so users cannot trigger concurrent saves - Show activity indicator on Reset to Default/Global button during the API call - Enrichments panes (semantic search, genai, face recognition) now always show base config fields regardless of profile selection in the header dropdown --- .../config-form/sections/BaseSection.tsx | 24 +++++++++++++++++-- web/src/pages/Settings.tsx | 11 +++++++-- web/src/views/settings/SingleSectionPage.tsx | 15 +++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 8ff5daa4f..9a2f710f7 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -152,6 +152,10 @@ export interface BaseSectionProps { profileBorderColor?: string; /** Callback to delete the current profile's overrides for this section */ onDeleteProfileSection?: () => void; + /** Whether a SaveAll operation is in progress (disables individual Save) */ + isSavingAll?: boolean; + /** Callback when this section's saving state changes */ + onSavingChange?: (isSaving: boolean) => void; } export interface CreateSectionOptions { @@ -186,6 +190,8 @@ export function ConfigSection({ profileFriendlyName, profileBorderColor, onDeleteProfileSection, + isSavingAll = false, + onSavingChange, }: ConfigSectionProps) { // For replay level, treat as camera-level config access const effectiveLevel = level === "replay" ? "camera" : level; @@ -246,6 +252,7 @@ export function ConfigSection({ [onPendingDataChange, effectiveSectionPath, cameraName], ); const [isSaving, setIsSaving] = useState(false); + const [isResettingToDefault, setIsResettingToDefault] = useState(false); const [hasValidationErrors, setHasValidationErrors] = useState(false); const [extraHasChanges, setExtraHasChanges] = useState(false); const [formKey, setFormKey] = useState(0); @@ -577,6 +584,7 @@ export function ConfigSection({ if (!pendingData) return; setIsSaving(true); + onSavingChange?.(true); try { const basePath = effectiveLevel === "camera" && cameraName @@ -699,6 +707,7 @@ export function ConfigSection({ } } finally { setIsSaving(false); + onSavingChange?.(false); } }, [ sectionPath, @@ -718,12 +727,14 @@ export function ConfigSection({ setPendingData, requiresRestartForOverrides, skipSave, + onSavingChange, ]); // Handle reset to global/defaults - removes camera-level override or resets global to defaults const handleResetToGlobal = useCallback(async () => { if (effectiveLevel === "camera" && !cameraName) return; + setIsResettingToDefault(true); try { const basePath = effectiveLevel === "camera" && cameraName @@ -758,6 +769,8 @@ export function ConfigSection({ defaultValue: "Failed to reset settings", }), ); + } finally { + setIsResettingToDefault(false); } }, [ effectiveSectionPath, @@ -945,9 +958,12 @@ export function ConfigSection({