From 6bcdbd72324c29dae88b9c49c70f1841b2913858 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:22:33 -0500 Subject: [PATCH] improve error messages when mixing/matching detectors --- web/public/locales/en/views/settings.json | 7 +- .../theme/fields/DetectorHardwareField.tsx | 67 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index e075c7e0eb..18c277cfce 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1409,7 +1409,8 @@ "keyDuplicate": "Detector name already exists.", "noSchema": "No detector schemas are available.", "none": "No detector instances configured.", - "add": "Add detector" + "add": "Add detector", + "addCustomKey": "Add custom key" }, "record": { "title": "Recording Settings" @@ -1637,6 +1638,10 @@ }, "snapshots": { "detectDisabled": "Object detection is disabled. Snapshots are generated from tracked objects and will not be created." + }, + "detectors": { + "mixedTypes": "All detectors must use the same type. Remove existing detectors to use a different type.", + "mixedTypesSuggestion": "All detectors must use the same type. Remove existing detectors or select {{type}}." } } } diff --git a/web/src/components/config-form/theme/fields/DetectorHardwareField.tsx b/web/src/components/config-form/theme/fields/DetectorHardwareField.tsx index 871131111a..b9e15ced2f 100644 --- a/web/src/components/config-form/theme/fields/DetectorHardwareField.tsx +++ b/web/src/components/config-form/theme/fields/DetectorHardwareField.tsx @@ -374,6 +374,18 @@ export function DetectorHardwareField(props: FieldProps) { [detectors], ); + const getExistingType = useCallback( + (excludeKey?: string): string | undefined => { + for (const [key, value] of Object.entries(detectors)) { + if (excludeKey && key === excludeKey) continue; + const type = getInstanceType(value); + if (type) return type; + } + return undefined; + }, + [detectors], + ); + const handleAdd = useCallback(() => { if (!addType) { setAddError( @@ -400,6 +412,28 @@ export function DetectorHardwareField(props: FieldProps) { return; } + const existingType = getExistingType(); + if (existingType && existingType !== addType) { + const canAddExisting = + multiInstanceSet.has(existingType) || + !resolveDuplicateType(existingType); + setAddError( + canAddExisting + ? t("configMessages.detectors.mixedTypesSuggestion", { + ns: "views/settings", + defaultValue: + "All detectors must use the same type. Remove existing detectors or select {{type}}.", + type: getTypeLabel(existingType), + }) + : t("configMessages.detectors.mixedTypes", { + ns: "views/settings", + defaultValue: + "All detectors must use the same type. Remove existing detectors to use a different type.", + }), + ); + return; + } + const baseKey = addType; let nextKey = baseKey; let index = 2; @@ -427,8 +461,10 @@ export function DetectorHardwareField(props: FieldProps) { configNamespace, detectors, getDetectorDefaults, + getExistingType, getTypeLabel, isSingleInstanceType, + multiInstanceSet, resolveDuplicateType, updateDetectors, ]); @@ -523,6 +559,29 @@ export function DetectorHardwareField(props: FieldProps) { return; } + const existingType = getExistingType(key); + if (existingType && existingType !== nextType) { + const canAddExisting = + multiInstanceSet.has(existingType) || + !resolveDuplicateType(existingType, key); + setTypeErrors((prev) => ({ + ...prev, + [key]: canAddExisting + ? t("configMessages.detectors.mixedTypesSuggestion", { + ns: "views/settings", + defaultValue: + "All detectors must use the same type. Remove existing detectors or select {{type}}.", + type: getTypeLabel(existingType), + }) + : t("configMessages.detectors.mixedTypes", { + ns: "views/settings", + defaultValue: + "All detectors must use the same type. Remove existing detectors to use a different type.", + }), + })); + return; + } + setTypeErrors((prev) => { const { [key]: _, ...rest } = prev; return rest; @@ -538,8 +597,10 @@ export function DetectorHardwareField(props: FieldProps) { [ detectors, getDetectorDefaults, + getExistingType, getTypeLabel, isSingleInstanceType, + multiInstanceSet, resolveDuplicateType, t, updateDetectors, @@ -556,6 +617,10 @@ export function DetectorHardwareField(props: FieldProps) { const nestedOverrides = { "ui:options": { disableNestedCard: true, + addButtonText: t("configForm.detectors.addCustomKey", { + ns: "views/settings", + defaultValue: "Add custom key", + }), }, } as UiSchema; @@ -567,7 +632,7 @@ export function DetectorHardwareField(props: FieldProps) { ); return mergeUiSchema(withTypeHiddenAndOptions, nestedOverrides); }, - [globalHiddenFields, hiddenByType, uiSchema?.additionalProperties], + [globalHiddenFields, hiddenByType, t, uiSchema?.additionalProperties], ); const renderInstanceForm = useCallback(