improve error messages when mixing/matching detectors

This commit is contained in:
Josh Hawkins 2026-04-01 07:22:33 -05:00
parent b839f501da
commit 6bcdbd7232
2 changed files with 72 additions and 2 deletions

View File

@ -1409,7 +1409,8 @@
"keyDuplicate": "Detector name already exists.", "keyDuplicate": "Detector name already exists.",
"noSchema": "No detector schemas are available.", "noSchema": "No detector schemas are available.",
"none": "No detector instances configured.", "none": "No detector instances configured.",
"add": "Add detector" "add": "Add detector",
"addCustomKey": "Add custom key"
}, },
"record": { "record": {
"title": "Recording Settings" "title": "Recording Settings"
@ -1637,6 +1638,10 @@
}, },
"snapshots": { "snapshots": {
"detectDisabled": "Object detection is disabled. Snapshots are generated from tracked objects and will not be created." "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}}."
} }
} }
} }

View File

@ -374,6 +374,18 @@ export function DetectorHardwareField(props: FieldProps) {
[detectors], [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(() => { const handleAdd = useCallback(() => {
if (!addType) { if (!addType) {
setAddError( setAddError(
@ -400,6 +412,28 @@ export function DetectorHardwareField(props: FieldProps) {
return; 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; const baseKey = addType;
let nextKey = baseKey; let nextKey = baseKey;
let index = 2; let index = 2;
@ -427,8 +461,10 @@ export function DetectorHardwareField(props: FieldProps) {
configNamespace, configNamespace,
detectors, detectors,
getDetectorDefaults, getDetectorDefaults,
getExistingType,
getTypeLabel, getTypeLabel,
isSingleInstanceType, isSingleInstanceType,
multiInstanceSet,
resolveDuplicateType, resolveDuplicateType,
updateDetectors, updateDetectors,
]); ]);
@ -523,6 +559,29 @@ export function DetectorHardwareField(props: FieldProps) {
return; 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) => { setTypeErrors((prev) => {
const { [key]: _, ...rest } = prev; const { [key]: _, ...rest } = prev;
return rest; return rest;
@ -538,8 +597,10 @@ export function DetectorHardwareField(props: FieldProps) {
[ [
detectors, detectors,
getDetectorDefaults, getDetectorDefaults,
getExistingType,
getTypeLabel, getTypeLabel,
isSingleInstanceType, isSingleInstanceType,
multiInstanceSet,
resolveDuplicateType, resolveDuplicateType,
t, t,
updateDetectors, updateDetectors,
@ -556,6 +617,10 @@ export function DetectorHardwareField(props: FieldProps) {
const nestedOverrides = { const nestedOverrides = {
"ui:options": { "ui:options": {
disableNestedCard: true, disableNestedCard: true,
addButtonText: t("configForm.detectors.addCustomKey", {
ns: "views/settings",
defaultValue: "Add custom key",
}),
}, },
} as UiSchema; } as UiSchema;
@ -567,7 +632,7 @@ export function DetectorHardwareField(props: FieldProps) {
); );
return mergeUiSchema(withTypeHiddenAndOptions, nestedOverrides); return mergeUiSchema(withTypeHiddenAndOptions, nestedOverrides);
}, },
[globalHiddenFields, hiddenByType, uiSchema?.additionalProperties], [globalHiddenFields, hiddenByType, t, uiSchema?.additionalProperties],
); );
const renderInstanceForm = useCallback( const renderInstanceForm = useCallback(