Miscellaneous fixes (#23186)

* improve scroll handling for non-modal DropdownMenu in classification and face selection dialogs

* clean up

* fix incorrect key capitalization

* 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

* only show audio debug tab when audio is enabled in config

* move apple_compatibility out of advanced

* remove retry_interval from UI

99% of users should never be changing this

* hide switch in optionalfieldwidget if editing a profile

* add override badges for cameras and profiles

collect shared functions into the config util and separate hooks

* Use new models endpoint info to determine modalities

* clarify language

* fix linter

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins
2026-05-13 11:04:11 -05:00
committed by GitHub
co-authored by Nicolas Mowen
parent bd1fc1cc72
commit ca75f06456
19 changed files with 783 additions and 379 deletions
+55 -8
View File
@@ -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<T extends object>(
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<SectionConfig> | 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;
}
@@ -739,3 +763,26 @@ export function resolveHiddenFieldEntries(
}
return result;
}
/**
* Match a delta path against a hidden-field pattern. Supports literal prefixes
* (so a hidden field "streams" also hides "streams.foo.bar") and `*` wildcards
* matching exactly one path segment (e.g. "filters.*.mask").
*/
export function pathMatchesHiddenPattern(
path: string,
pattern: string,
): boolean {
if (!pattern) return false;
if (!pattern.includes("*")) {
return path === pattern || path.startsWith(`${pattern}.`);
}
const patternSegments = pattern.split(".");
const pathSegments = path.split(".");
if (pathSegments.length < patternSegments.length) return false;
for (let i = 0; i < patternSegments.length; i += 1) {
if (patternSegments[i] === "*") continue;
if (patternSegments[i] !== pathSegments[i]) return false;
}
return true;
}