From 78e19d95e622769bf0cdeedea4695dadaf7a1f5b Mon Sep 17 00:00:00 2001 From: Stephen Boettcher Date: Mon, 9 Mar 2026 04:15:45 -0400 Subject: [PATCH 1/2] Add 'Is Empty' and 'Is Not Empty' operators to filters. specifically to allow a user to filter movies that are part of a collection or not a part of a collection --- .../Filter/Builder/FilterBuilderRow.js | 21 +++++++++++----- .../src/Helpers/Props/filterBuilderTypes.js | 8 ++++++ .../src/Helpers/Props/filterTypePredicates.js | 25 +++++++++++++++++++ frontend/src/Helpers/Props/filterTypes.js | 6 ++++- .../createClientSideCollectionSelector.js | 2 +- src/NzbDrone.Core/Localization/Core/en.json | 2 ++ 6 files changed, 56 insertions(+), 8 deletions(-) diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 9fd450d2ef..c8218cc1b9 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; +import * as filterTypes from 'Helpers/Props/filterTypes'; import sortByProp from 'Utilities/Array/sortByProp'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; @@ -226,6 +227,13 @@ class FilterBuilderRow extends Component { const selectedFilterBuilderProp = this.selectedFilterBuilderProp; + const operatorsWithoutValue = [ + filterTypes.IS_EMPTY, + filterTypes.IS_NOT_EMPTY + ]; + + const requiresValue = !operatorsWithoutValue.includes(filterType); + const keyOptions = filterBuilderProps.map((availablePropFilter) => { const { name, label } = availablePropFilter; @@ -262,10 +270,11 @@ class FilterBuilderRow extends Component { /> } - -
- { - filterValue != null && !!selectedFilterBuilderProp && + { + requiresValue && + filterValue != null && + !!selectedFilterBuilderProp && +
- } -
+
+ }
translate('FilterDoesNotContain') + }, + { + key: filterTypes.IS_EMPTY, + value: () => translate('FilterIsEmpty') + }, + { + key: filterTypes.IS_NOT_EMPTY, + value: () => translate('FilterIsNotEmpty') } ], diff --git a/frontend/src/Helpers/Props/filterTypePredicates.js b/frontend/src/Helpers/Props/filterTypePredicates.js index d07059c02a..d63841bd58 100644 --- a/frontend/src/Helpers/Props/filterTypePredicates.js +++ b/frontend/src/Helpers/Props/filterTypePredicates.js @@ -55,6 +55,31 @@ const filterTypePredicates = { [filterTypes.NOT_ENDS_WITH]: function(itemValue, filterValue) { return !itemValue.toLowerCase().endsWith(filterValue.toLowerCase()); + }, + + [filterTypes.IS_EMPTY]: function(itemValue, filterValue) { + console.log('isEmpty', { itemValue }); + if (itemValue === null || itemValue === undefined) { + return true; + } + + if (Array.isArray(itemValue) || typeof itemValue === 'string') { + return itemValue.length === 0; + } + + return false; + }, + + [filterTypes.IS_NOT_EMPTY]: function(itemValue, filterValue) { + if (itemValue === null || itemValue === undefined) { + return false; + } + + if (Array.isArray(itemValue) || typeof itemValue === 'string') { + return itemValue.length > 0; + } + + return true; } }; diff --git a/frontend/src/Helpers/Props/filterTypes.js b/frontend/src/Helpers/Props/filterTypes.js index 239a4e7e94..c47f8b29f9 100644 --- a/frontend/src/Helpers/Props/filterTypes.js +++ b/frontend/src/Helpers/Props/filterTypes.js @@ -14,6 +14,8 @@ export const STARTS_WITH = 'startsWith'; export const NOT_STARTS_WITH = 'notStartsWith'; export const ENDS_WITH = 'endsWith'; export const NOT_ENDS_WITH = 'notEndsWith'; +export const IS_EMPTY = 'isEmpty'; +export const IS_NOT_EMPTY = 'isNotEmpty'; export const all = [ CONTAINS, @@ -31,5 +33,7 @@ export const all = [ STARTS_WITH, NOT_STARTS_WITH, ENDS_WITH, - NOT_ENDS_WITH + NOT_ENDS_WITH, + IS_EMPTY, + IS_NOT_EMPTY ]; diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js index 1bac14f087..a87e354a11 100644 --- a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -43,7 +43,7 @@ function filter(items, state) { if (filterPredicates && filterPredicates.hasOwnProperty(key)) { const predicate = filterPredicates[key]; - if (Array.isArray(value)) { + if (Array.isArray(value) && value.length > 0) { if ( type === filterTypes.NOT_CONTAINS || type === filterTypes.NOT_EQUAL diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 23a3b71ab8..10c1377c81 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -697,6 +697,8 @@ "FilterNotInLast": "not in the last", "FilterNotInNext": "not in the next", "FilterStartsWith": "starts with", + "FilterIsEmpty": "is empty", + "FilterIsNotEmpty": "is NOT empty", "Filters": "Filters", "FilterMoviePropertiesOnlyNotFileWarning": "Filters are available only for the properties of a movie, they are not available for properties of the file(s) you may have for that movie.", "FirstDayOfWeek": "First Day of Week", From d69a36e1baf973687289658c0d8551f8e0a980b5 Mon Sep 17 00:00:00 2001 From: Stephen Boettcher Date: Mon, 9 Mar 2026 10:10:08 -0400 Subject: [PATCH 2/2] removing a console.log that I left behind --- frontend/src/Helpers/Props/filterTypePredicates.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/Helpers/Props/filterTypePredicates.js b/frontend/src/Helpers/Props/filterTypePredicates.js index d63841bd58..007533ece2 100644 --- a/frontend/src/Helpers/Props/filterTypePredicates.js +++ b/frontend/src/Helpers/Props/filterTypePredicates.js @@ -58,7 +58,6 @@ const filterTypePredicates = { }, [filterTypes.IS_EMPTY]: function(itemValue, filterValue) { - console.log('isEmpty', { itemValue }); if (itemValue === null || itemValue === undefined) { return true; }