From c6b355bd7cfbf8eeb30424ef9771d2a35f28bffa Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 12 May 2026 15:40:13 -0500 Subject: [PATCH] fix profile array overrides not replacing base arrays don't use lodash merge(), it does positional merging and an empty source array doesn't override the destination, and shorter arrays leak destination elements through. backend is unaffected, so the saved config and actual backend functionality was right --- .../config-form/sections/BaseSection.tsx | 7 +++- web/src/utils/configUtil.ts | 40 +++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 6eb7640453..7727b92201 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -41,7 +41,6 @@ import Heading from "@/components/ui/heading"; import get from "lodash/get"; import cloneDeep from "lodash/cloneDeep"; import isEqual from "lodash/isEqual"; -import merge from "lodash/merge"; import { Collapsible, CollapsibleContent, @@ -73,6 +72,7 @@ import { buildConfigDataForPath, flattenOverrides, getBaseCameraSectionValue, + mergeProfileOverrides, resolveHiddenFieldEntries, sanitizeSectionData as sharedSanitizeSectionData, requiresRestartForOverrides as sharedRequiresRestartForOverrides, @@ -353,7 +353,10 @@ export function ConfigSection({ `profiles.${profileName}.${sectionPath}`, ); if (profileOverrides && typeof profileOverrides === "object") { - return merge(cloneDeep(baseValue ?? {}), cloneDeep(profileOverrides)); + return mergeProfileOverrides( + (baseValue as object) ?? {}, + profileOverrides as object, + ); } return baseValue; } diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index ee33e59b7b..54de9d9ec9 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -6,7 +6,6 @@ import get from "lodash/get"; import cloneDeep from "lodash/cloneDeep"; -import merge from "lodash/merge"; import unset from "lodash/unset"; import isEqual from "lodash/isEqual"; import mergeWith from "lodash/mergeWith"; @@ -92,6 +91,32 @@ export function getBaseCameraSectionValue( return base !== undefined ? base : get(cam, sectionPath); } +// mergeWith customizer that replaces arrays wholesale instead of merging them +// positionally by index. Used when the source value is meant to fully replace +// the destination (e.g. profile overrides, section config overrides), so an +// empty source array correctly clears the destination array. +const replaceArraysCustomizer = (objValue: unknown, srcValue: unknown) => { + if (Array.isArray(objValue) || Array.isArray(srcValue)) { + return srcValue !== undefined ? srcValue : objValue; + } + return undefined; +}; + +// Merge profile overrides on top of base config values. Matches the backend's +// deep_merge(overrides, base_data) semantics: arrays are replaced wholesale by +// the profile's value rather than merged positionally, so an empty array in a +// profile clears the base array instead of leaving stale entries behind. +export function mergeProfileOverrides( + baseValue: T, + profileOverrides: object, +): T { + return mergeWith( + cloneDeep(baseValue), + cloneDeep(profileOverrides), + replaceArraysCustomizer, + ) as T; +} + /** Sections that can appear inside a camera profile definition. */ export const PROFILE_ELIGIBLE_SECTIONS = new Set([ "audio", @@ -564,9 +589,9 @@ export function prepareSectionSavePayload(opts: { baseValue && typeof baseValue === "object" ) { - rawSectionValue = merge( - cloneDeep(baseValue), - cloneDeep(profileOverrides), + rawSectionValue = mergeProfileOverrides( + baseValue as object, + profileOverrides as object, ); } else { rawSectionValue = baseValue; @@ -675,13 +700,12 @@ const mergeSectionConfig = ( overrides: Partial | undefined, ): SectionConfig => mergeWith({}, base ?? {}, overrides ?? {}, (objValue, srcValue, key) => { - if (Array.isArray(objValue) || Array.isArray(srcValue)) { - return srcValue ?? objValue; - } + const arrayResult = replaceArraysCustomizer(objValue, srcValue); + if (arrayResult !== undefined) return arrayResult; if (key === "uiSchema") { if (objValue && srcValue) { - return merge({}, objValue, srcValue); + return mergeWith({}, objValue, srcValue, replaceArraysCustomizer); } return srcValue ?? objValue; }