diff --git a/core/src/db/filter/smart_filter.rs b/core/src/db/filter/smart_filter.rs
index e515d51fe..6a79d9dd9 100644
--- a/core/src/db/filter/smart_filter.rs
+++ b/core/src/db/filter/smart_filter.rs
@@ -236,25 +236,45 @@ pub enum LibrarySmartFilter {
#[serde(untagged)]
#[prisma_table("series_metadata")]
pub enum SeriesMetadataSmartFilter {
+ #[is_optional]
+ AgeRating {
+ age_rating: i32,
+ },
MetaType {
meta_type: String,
},
#[is_optional]
+ Title {
+ title: String,
+ },
+ #[is_optional]
+ Summary {
+ summary: String,
+ },
+ #[is_optional]
Publisher {
publisher: String,
},
#[is_optional]
- Status {
- status: String,
+ Imprint {
+ imprint: String,
},
#[is_optional]
- AgeRating {
- age_rating: i32,
+ ComicId {
+ comicid: i32,
+ },
+ #[is_optional]
+ BookType {
+ booktype: String,
},
#[is_optional]
Volume {
volume: i32,
},
+ #[is_optional]
+ Status {
+ status: String,
+ },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Type, ToSchema)]
diff --git a/packages/browser/src/components/smartList/createOrUpdate/__tests__/schema.test.ts b/packages/browser/src/components/smartList/createOrUpdate/__tests__/schema.test.ts
index aa572dd9c..9e9aacb3d 100644
--- a/packages/browser/src/components/smartList/createOrUpdate/__tests__/schema.test.ts
+++ b/packages/browser/src/components/smartList/createOrUpdate/__tests__/schema.test.ts
@@ -116,9 +116,40 @@ describe('schema', () => {
value: Object.values(filter)[0],
})
}
+ })
+
+ it('should convert smart filter with series meta into form filter', () => {
+ for (const filter of stringFilters) {
+ expect(
+ intoFormFilter({
+ series: {
+ metadata: {
+ title: filter,
+ },
+ },
+ } satisfies MediaSmartFilter),
+ ).toEqual({
+ field: 'title',
+ operation: Object.keys(filter)[0],
+ source: 'series_meta',
+ value: Object.values(filter)[0],
+ })
+ }
- // TODO: support series metadata
- // TODO: add numeric filters for series?
+ for (const filter of numericFilters) {
+ const operation = 'from' in filter ? 'range' : Object.keys(filter)[0]
+ const value = 'from' in filter ? filter : Object.values(filter)[0]
+ expect(
+ intoFormFilter({
+ series: { metadata: { age_rating: filter } },
+ } satisfies MediaSmartFilter),
+ ).toEqual({
+ field: 'age_rating',
+ operation,
+ source: 'series_meta',
+ value,
+ })
+ }
})
it('should convert smart filter with library into form filter', () => {
@@ -304,6 +335,68 @@ describe('schema', () => {
})
})
+ it('should convert smart filter form with series meta into API filter', () => {
+ // String filter
+ expect(
+ intoAPIFilter({
+ field: 'title',
+ operation: 'any',
+ source: 'series_meta',
+ value: ['foo', 'shmoo'],
+ }),
+ ).toEqual({
+ series: {
+ metadata: {
+ title: {
+ any: ['foo', 'shmoo'],
+ },
+ },
+ },
+ })
+
+ // Numeric filter (basic)
+ expect(
+ intoAPIFilter({
+ field: 'age_rating',
+ operation: 'gte',
+ source: 'series_meta',
+ value: 42,
+ }),
+ ).toEqual({
+ series: {
+ metadata: {
+ age_rating: {
+ gte: 42,
+ },
+ },
+ },
+ })
+
+ // Numeric filter (complex)
+ expect(
+ intoAPIFilter({
+ field: 'age_rating',
+ operation: 'range',
+ source: 'series_meta',
+ value: {
+ from: 42,
+ inclusive: true,
+ to: 69,
+ },
+ }),
+ ).toEqual({
+ series: {
+ metadata: {
+ age_rating: {
+ from: 42,
+ inclusive: true,
+ to: 69,
+ },
+ },
+ },
+ })
+ })
+
it('should convert smart filter form with library into API filter', () => {
// String filter
expect(
diff --git a/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/FieldSelector.tsx b/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/FieldSelector.tsx
index 7dee603a0..05e0c1fdd 100644
--- a/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/FieldSelector.tsx
+++ b/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/FieldSelector.tsx
@@ -68,6 +68,15 @@ export function FieldSelector({ idx }: Props) {
{t(getSourceKey('series', 'label'))}
+
+ setSource('series_meta')}
+ className="flex items-center justify-between"
+ >
+ {t(getSourceKey('series_meta', 'label'))}
+
+
+
setSource('library')}
className="flex items-center justify-between"
@@ -189,6 +198,18 @@ const sourceOptions: Record = {
],
library: [{ value: 'name' }, { value: 'path' }],
series: [{ value: 'name' }, { value: 'path' }],
+ series_meta: [
+ { value: 'age_rating' },
+ { value: 'meta_type' },
+ { value: 'title' },
+ { value: 'summary' },
+ { value: 'publisher' },
+ { value: 'imprint' },
+ { value: 'comicid' },
+ { value: 'booktype' },
+ { value: 'status' },
+ { value: 'volume' },
+ ],
}
// TODO: series_meta: [meta_type, publisher, status, age_rating, volume]
diff --git a/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/OperatorSelect.tsx b/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/OperatorSelect.tsx
index eb5f55ae3..387bf8ccf 100644
--- a/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/OperatorSelect.tsx
+++ b/packages/browser/src/components/smartList/createOrUpdate/queryBuilder/filterGroup/OperatorSelect.tsx
@@ -59,7 +59,7 @@ export default function OperatorSelect({ idx }: Props) {
(field) => isNumberField(field) || isDateField(field),
() => ['gt', 'gte', 'lt', 'lte', 'not', 'equals', 'range'] as NumberOperation[],
)
- .otherwise(() => [] as Operation[]),
+ .otherwise(() => ['not', 'equals'] as Operation[]),
[fieldDef],
)
@@ -79,7 +79,7 @@ export default function OperatorSelect({ idx }: Props) {
},
]
: []),
- ]
+ ].filter(({ operators }) => operators.length)
}, [operators, fieldDef])
useEffect(() => {
diff --git a/packages/browser/src/components/smartList/createOrUpdate/schema.ts b/packages/browser/src/components/smartList/createOrUpdate/schema.ts
index 24b0ad63b..adbc9aa7c 100644
--- a/packages/browser/src/components/smartList/createOrUpdate/schema.ts
+++ b/packages/browser/src/components/smartList/createOrUpdate/schema.ts
@@ -4,6 +4,7 @@ import {
LibrarySmartFilter,
MediaMetadataSmartFilter,
MediaSmartFilter,
+ SeriesMetadataSmartFilter,
SeriesSmartFilter,
SmartFilter,
SmartList,
@@ -56,6 +57,10 @@ export const stringField = z.enum([
'links',
'characters',
'teams',
+ 'comicid',
+ 'booktype',
+ 'status',
+ 'meta_type',
])
export type StringField = z.infer
export const isStringField = (field: string): field is StringField =>
@@ -69,6 +74,7 @@ export const numberField = z.enum([
'pages',
'page_count',
'size',
+ 'volume',
])
export type NumberField = z.infer
export const isNumberField = (field: string): field is NumberField =>
@@ -82,7 +88,7 @@ export const filter = z
.object({
field: z.string(),
operation,
- source: z.enum(['book', 'book_meta', 'series', 'library']),
+ source: z.enum(['book', 'book_meta', 'series', 'series_meta', 'library']),
value: z.union([
z.string(),
z.string().array(),
@@ -129,6 +135,13 @@ export const intoAPIFilter = (input: z.infer): MediaSmartFilter =
[input.field]: fieldValue,
} as SeriesSmartFilter,
}))
+ .with('series_meta', () => ({
+ series: {
+ metadata: {
+ [input.field]: fieldValue,
+ },
+ } as SeriesSmartFilter,
+ }))
.with('library', () => ({
series: {
library: {
@@ -152,6 +165,10 @@ export const intoFormFilter = (input: MediaSmartFilter): z.infer
(x) => 'series' in x && 'library' in x.series,
() => 'library' as const,
)
+ .when(
+ (x) => 'series' in x && 'metadata' in x.series,
+ () => 'series_meta' as const,
+ )
.when(
(x) => 'series' in x,
() => 'series' as const,
@@ -165,6 +182,13 @@ export const intoFormFilter = (input: MediaSmartFilter): z.infer
() => Object.keys((input as { metadata: MediaMetadataSmartFilter }).metadata)[0],
)
.with('series', () => Object.keys((input as { series: SeriesSmartFilter }).series)[0])
+ .with(
+ 'series_meta',
+ () =>
+ Object.keys(
+ (input as { series: { metadata: SeriesMetadataSmartFilter } }).series.metadata,
+ )[0],
+ )
.with(
'library',
() => Object.keys((input as { series: { library: LibrarySmartFilter } }).series.library)[0],
@@ -217,6 +241,21 @@ export const intoFormFilter = (input: MediaSmartFilter): z.infer
value,
}
})
+ .with('series_meta', () => {
+ const castedInput = input as { series: { metadata: SeriesMetadataSmartFilter } } // { series: { metadata: { [field]: { [operation]: value } } } }
+ const filterValue = getProperty(castedInput.series.metadata, field || '') // { [operation]: value }
+ const operation = 'from' in filterValue ? 'range' : Object.keys(filterValue || {})[0]
+ const value = match(operation)
+ .with('range', () => filterValue)
+ .otherwise(() => getProperty(filterValue, operation || ''))
+
+ return {
+ field,
+ operation,
+ source,
+ value,
+ }
+ })
.with('library', () => {
const castedInput = input as { series: { library: LibrarySmartFilter } } // { series: { library: { [field]: { [operation]: value } } } }
const filterValue = getProperty(castedInput.series.library, field || '') // { [operation]: value }
@@ -361,8 +400,6 @@ export const intoAPI = ({
export const intoAPIFilters = ({
groups,
- joiner,
}: Pick['filters']): SmartFilter => ({
groups: groups.map(intoAPIGroup),
- joiner: joiner.toUpperCase() as 'AND' | 'OR',
})
diff --git a/packages/browser/src/components/smartList/createOrUpdate/sections/FilterConfigJSON.tsx b/packages/browser/src/components/smartList/createOrUpdate/sections/FilterConfigJSON.tsx
index 7b91566d1..2116d2bc5 100644
--- a/packages/browser/src/components/smartList/createOrUpdate/sections/FilterConfigJSON.tsx
+++ b/packages/browser/src/components/smartList/createOrUpdate/sections/FilterConfigJSON.tsx
@@ -11,12 +11,11 @@ export default function FilterConfigJSON() {
const [joiner] = form.watch(['filters.joiner'])
const groups = useMemo(() => (filters?.groups ?? []) as FilterGroupSchema[], [filters?.groups])
- // FIXME: this errors lol
const apiFilters = useMemo(
() =>
intoAPIFilters({
groups,
- joiner: joiner ?? 'AND',
+ joiner,
}),
[groups, joiner],
)
diff --git a/packages/components/src/select/ComboBox.tsx b/packages/components/src/select/ComboBox.tsx
index 2759c9f51..60fe0406d 100644
--- a/packages/components/src/select/ComboBox.tsx
+++ b/packages/components/src/select/ComboBox.tsx
@@ -136,8 +136,8 @@ export function ComboBox({