feat(overlays): add exists (null) boolean condition

fix #285
This commit is contained in:
Tom Wheeler 2026-01-09 01:50:23 +13:00
parent d32c041faf
commit 15d343e8ee
4 changed files with 28 additions and 6 deletions

View File

@ -197,7 +197,8 @@ export interface ConditionRule {
| 'contains' // string contains
| 'regex' // regex match
| 'begins' // string begins with
| 'ends'; // string ends with
| 'ends' // string ends with
| 'exists'; // field exists (has non-null/undefined value)
value: string | number | boolean | (string | number)[];
}

View File

@ -185,6 +185,12 @@ function evaluateRule(
if (rule.operator === 'neq') {
return conditionValue !== undefined && conditionValue !== null;
}
// For 'exists', we need to evaluate based on the presence/absence of value
if (rule.operator === 'exists') {
// value is null/undefined, so field does NOT exist
// Return true if conditionValue is false (checking for non-existence)
return conditionValue === false;
}
// For all other operators (eq, gt, gte, lt, lte, contains, in, etc.)
// undefined/null means the condition can't be evaluated, so false
return false;
@ -292,6 +298,14 @@ function evaluateRule(
typeof conditionValue === 'string' &&
value.toLowerCase().endsWith(conditionValue.toLowerCase())
);
case 'exists':
// Check if field has a non-null/undefined value
// conditionValue should be boolean: true = exists, false = not exists
if (typeof conditionValue === 'boolean') {
const hasValue = value !== undefined && value !== null;
return conditionValue ? hasValue : !hasValue;
}
return false;
default:
return false;
}

View File

@ -56,6 +56,7 @@ const messages = defineMessages({
opBegins: 'begins with',
opEnds: 'ends with',
opIn: 'in',
opExists: 'exists',
and: 'AND',
or: 'OR',
});
@ -143,6 +144,7 @@ const RuleItem: React.FC<RuleItemProps> = ({
const isRadarrTags = field === 'radarrTags';
const isSonarrTags = field === 'sonarrTags';
const isTagField = isRadarrTags || isSonarrTags;
const isExistsOperator = operator === 'exists';
// Fetch all tags from all Radarr instances
const { data: radarrTags } = useSWR<ArrTag[]>(
@ -218,14 +220,14 @@ const RuleItem: React.FC<RuleItemProps> = ({
const numericOnlyOperators = ['gt', 'gte', 'lt', 'lte'];
const isCurrentOperatorInvalid =
(!isNewFieldNumeric && numericOnlyOperators.includes(operator)) ||
(isNewFieldBoolean && !['eq', 'neq'].includes(operator));
(isNewFieldBoolean && !['eq', 'neq', 'exists'].includes(operator));
// Reset to appropriate defaults when changing field
onChange({
...rule,
field: newField,
operator: isCurrentOperatorInvalid ? 'eq' : rule.operator,
value: isNewFieldBoolean ? true : '',
value: operator === 'exists' || isNewFieldBoolean ? true : '',
});
}}
className="flex-1 select-none rounded border border-stone-600 bg-stone-700 px-2 py-1 text-sm text-white"
@ -247,9 +249,12 @@ const RuleItem: React.FC<RuleItemProps> = ({
<select
value={operator}
onChange={(e) => {
const newOperator = e.target.value as ConditionRule['operator'];
onChange({
...rule,
operator: e.target.value as ConditionRule['operator'],
operator: newOperator,
// Set value to true when switching to 'exists' operator
value: newOperator === 'exists' ? true : rule.value,
});
}}
className="w-32 rounded border border-stone-600 bg-stone-700 px-2 py-1 text-sm text-white"
@ -287,10 +292,11 @@ const RuleItem: React.FC<RuleItemProps> = ({
<option value="ends">{intl.formatMessage(messages.opEnds)}</option>
</>
)}
<option value="exists">{intl.formatMessage(messages.opExists)}</option>
</select>
{/* Value Input */}
{isBoolean ? (
{isBoolean || isExistsOperator ? (
<select
value={String(value)}
onChange={(e) => {

View File

@ -139,7 +139,8 @@ export interface ConditionRule {
| 'contains' // string contains
| 'regex' // regex match
| 'begins' // string begins with
| 'ends'; // string ends with
| 'ends' // string ends with
| 'exists'; // field exists (has non-null/undefined value)
value: string | number | boolean | (string | number)[];
}