From 16e00a0f9f982dbfcf31afc3c8c10e9a0eace4a5 Mon Sep 17 00:00:00 2001
From: Bram Johnson <89540826+bramjohnson@users.noreply.github.com>
Date: Thu, 18 Jun 2026 12:44:58 -0400
Subject: [PATCH] feat(playlists): add `isMissing` and `isPresent` operators to
Navidrome smart playlist form (#2149)
---
src/i18n/locales/en.json | 2 ++
.../query-builder/query-builder-option.tsx | 33 ++++++++++++-------
src/shared/api/navidrome/navidrome-types.ts | 15 +++++++++
3 files changed, 38 insertions(+), 12 deletions(-)
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 4e96df9ac..a14a1780d 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -314,6 +314,8 @@
"inTheRangeDate": "Is in the range (date)",
"is": "Is",
"isNot": "Is not",
+ "isMissing": "Is missing",
+ "isPresent": "Is present",
"isGreaterThan": "Is greater than",
"isLessThan": "Is less than",
"matchesRegex": "Matches regex",
diff --git a/src/renderer/components/query-builder/query-builder-option.tsx b/src/renderer/components/query-builder/query-builder-option.tsx
index 5865b2192..56896f811 100644
--- a/src/renderer/components/query-builder/query-builder-option.tsx
+++ b/src/renderer/components/query-builder/query-builder-option.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { Filters } from '/@/renderer/components/query-builder';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
@@ -102,19 +102,28 @@ const QueryValueInput = ({
const isDatePickerOperator =
operator === 'beforeDate' || operator === 'afterDate' || operator === 'inTheRangeDate';
+ const BooleanSelectComponent = useMemo(
+ () => (
+
+ ),
+ [onChange, props, value],
+ );
+
+ if (operator === 'isMissing' || operator === 'isPresent') {
+ return BooleanSelectComponent;
+ }
+
switch (type) {
case 'boolean':
- return (
-
- );
+ return BooleanSelectComponent;
case 'date':
if (isDatePickerOperator && operator !== 'inTheRangeDate') {
const dateValue = value ? parseDateValue(value) : null;
diff --git a/src/shared/api/navidrome/navidrome-types.ts b/src/shared/api/navidrome/navidrome-types.ts
index 2ae2084ad..bde37acbd 100644
--- a/src/shared/api/navidrome/navidrome-types.ts
+++ b/src/shared/api/navidrome/navidrome-types.ts
@@ -216,6 +216,17 @@ export const NDSongQueryPlaylistOperators = [
},
];
+const NDPresenceOperators = [
+ {
+ label: i18n.t('filterOperator.isMissing'),
+ value: 'isMissing',
+ },
+ {
+ label: i18n.t('filterOperator.isPresent'),
+ value: 'isPresent',
+ },
+];
+
export const NDSongQueryDateOperators = [
{
label: i18n.t('filterOperator.is'),
@@ -225,6 +236,7 @@ export const NDSongQueryDateOperators = [
label: i18n.t('filterOperator.isNot'),
value: 'isNot',
},
+ ...NDPresenceOperators,
{
label: i18n.t('filterOperator.before'),
value: 'before',
@@ -268,6 +280,7 @@ export const NDSongQueryStringOperators = [
label: i18n.t('filterOperator.isNot'),
value: 'isNot',
},
+ ...NDPresenceOperators,
{
label: i18n.t('filterOperator.contains'),
value: 'contains',
@@ -295,6 +308,7 @@ export const NDSongQueryBooleanOperators = [
label: i18n.t('filterOperator.isNot'),
value: 'isNot',
},
+ ...NDPresenceOperators,
];
export const NDSongQueryNumberOperators = [
@@ -306,6 +320,7 @@ export const NDSongQueryNumberOperators = [
label: i18n.t('filterOperator.isNot'),
value: 'isNot',
},
+ ...NDPresenceOperators,
{
label: i18n.t('filterOperator.contains'),
value: 'contains',