diff --git a/server/controllers/albums.controller.ts b/server/controllers/albums.controller.ts index 6774d767c..fb5ba8da1 100644 --- a/server/controllers/albums.controller.ts +++ b/server/controllers/albums.controller.ts @@ -26,13 +26,12 @@ const getList = async ( const { serverId } = req.params; const { take, skip, serverUrlId, advancedFilters } = req.query; - const decodedAdvancedFilters = advancedFilters && decodeURI(advancedFilters); - const jsonAdvancedFilters = - decodedAdvancedFilters && JSON.parse(decodedAdvancedFilters); + const decodedAdvancedFilters = + advancedFilters && JSON.parse(decodeURI(advancedFilters)); const albums = await service.albums.findMany({ ...req.query, - advancedFilters: jsonAdvancedFilters, + advancedFilters: decodedAdvancedFilters, serverId, skip: Number(skip), take: Number(take), diff --git a/server/helpers/albums.helpers.ts b/server/helpers/albums.helpers.ts index 1d086d351..8d5bff8ab 100644 --- a/server/helpers/albums.helpers.ts +++ b/server/helpers/albums.helpers.ts @@ -134,8 +134,11 @@ const advancedFilterGroup = ( for (const rule of group.rules) { if (rule.field && rule.operator) { const [table, field, relationField] = rule.field.split('.'); - const condition = rule.operator === '!~' ? 'none' : 'some'; + const condition = + rule.operator === '!~' || rule.operator === '!=' ? 'none' : 'some'; const op = operatorMap[rule.operator as keyof typeof operatorMap]; + const value = + field !== 'releaseDate' ? rule.value : new Date(rule.value); switch (table) { case 'albums': @@ -144,7 +147,7 @@ const advancedFilterGroup = ( [field]: { [condition]: { [relationField]: { - [op]: rule.value, + [op]: value, }, userId: user.id, }, @@ -152,13 +155,24 @@ const advancedFilterGroup = ( }); break; } - + if (field === 'genres') { + query[rootType].push({ + [field]: { + [condition]: { + [relationField]: { + equals: value, + }, + }, + }, + }); + break; + } query[rootType].push({ [field]: { mode: insensitiveFields.includes(field) ? 'insensitive' : undefined, - [op]: rule.value, + [op]: value, }, }); break; @@ -171,7 +185,7 @@ const advancedFilterGroup = ( [field]: { some: { [relationField]: { - [op]: rule.value, + [op]: value, }, userId: user.id, }, @@ -181,13 +195,29 @@ const advancedFilterGroup = ( }); break; } + if (field === 'genres') { + query[rootType].push({ + [table]: { + some: { + [field]: { + [condition]: { + [relationField]: { + equals: value, + }, + }, + }, + }, + }, + }); + break; + } query[rootType].push({ [table]: { [condition]: { [field]: { mode: 'insensitive', - [op]: rule.value, + [op]: value, }, }, }, @@ -218,8 +248,10 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => { for (const rule of filter.rules) { if (rule.field && rule.operator) { let [table, field, relationField] = rule.field.split('.'); - const condition = rule.operator === '!~' ? 'none' : 'some'; + const condition = + rule.operator === '!~' || rule.operator === '!=' ? 'none' : 'some'; const op = operatorMap[rule.operator as keyof typeof operatorMap]; + const value = field !== 'releaseDate' ? rule.value : new Date(rule.value); switch (table) { case 'albums': @@ -228,7 +260,7 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => { [field]: { [condition]: { [relationField]: { - [op]: rule.value, + [op]: value, }, userId: user.id, }, @@ -236,13 +268,24 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => { }); break; } - + if (field === 'genres') { + rootQuery[rootQueryType].push({ + [field]: { + [condition]: { + [relationField]: { + equals: value, + }, + }, + }, + }); + break; + } rootQuery[rootQueryType].push({ [field]: { mode: insensitiveFields.includes(field) ? 'insensitive' : undefined, - [op]: rule.value, + [op]: value, }, }); break; @@ -255,7 +298,7 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => { [field]: { some: { [relationField]: { - [op]: rule.value, + [op]: value, }, userId: user.id, }, @@ -265,13 +308,29 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => { }); break; } + if (field === 'genres') { + rootQuery[rootQueryType].push({ + [table]: { + some: { + [field]: { + [condition]: { + [relationField]: { + equals: value, + }, + }, + }, + }, + }, + }); + break; + } rootQuery[rootQueryType].push({ [table]: { [condition]: { [field]: { mode: 'insensitive', - [op]: rule.value, + [op]: value, }, }, }, diff --git a/src/renderer/features/albums/components/advanced-filters.tsx b/src/renderer/features/albums/components/advanced-filters.tsx index 6500629a7..8cbc5296b 100644 --- a/src/renderer/features/albums/components/advanced-filters.tsx +++ b/src/renderer/features/albums/components/advanced-filters.tsx @@ -1,5 +1,7 @@ +import { useMemo } from 'react'; import { Stack, Group } from '@mantine/core'; import dayjs from 'dayjs'; +import { AnimatePresence, motion } from 'framer-motion'; import get from 'lodash/get'; import set from 'lodash/set'; import { nanoid } from 'nanoid/non-secure'; @@ -12,6 +14,7 @@ import { Select, TextInput, } from '@/renderer/components'; +import { useGenreList } from '@/renderer/features/genres'; export enum FilterGroupType { AND = 'AND', @@ -19,10 +22,10 @@ export enum FilterGroupType { } export type AdvancedFilterRule = { - field: string | null; - operator: string | null; + field?: string | null; + operator?: string | null; uniqueId: string; - value: string | number | Date | undefined | null | any; + value?: string | number | Date | undefined | null | any; }; export type AdvancedFilterGroup = { @@ -58,8 +61,8 @@ const NUMBER_FILTER_OPTIONS_DATA = [ ]; const ID_FILTER_OPTIONS_DATA = [ - { label: 'is', value: 'equals' }, - { label: 'is not', value: 'not' }, + { label: 'is', value: '=' }, + { label: 'is not', value: '!=' }, ]; const FILTER_GROUP_OPTIONS_DATA = [ @@ -75,73 +78,96 @@ const FILTER_GROUP_OPTIONS_DATA = [ const FILTER_OPTIONS_DATA = [ { + default: '~', label: 'Artist Title', value: 'artists.name', }, { + default: '=', label: 'Artist Rating', value: 'artists.ratings.value', }, { + default: '=', label: 'Artist Genre', - value: 'artists.genre', + value: 'artists.genres.id', }, { + default: '~', label: 'Album Artist Title', value: 'albumArtists.name', }, { + default: '=', label: 'Album Artist Rating', value: 'albumArtists.ratings.value', }, { + default: '=', label: 'Album Artist Genre', - value: 'albumArtists.genre', + value: 'albumArtists.genres.id', }, { + default: '~', label: 'Album Title', value: 'albums.name', }, { - label: 'Album Genre', - value: 'albums.genre', - }, - { + default: '=', label: 'Album Rating', value: 'albums.ratings.value', }, { + default: '=', + label: 'Album Genre', + value: 'albums.genres.id', + }, + { + default: '=', label: 'Album Year', value: 'albums.releaseYear', }, { + default: '<', label: 'Album Release Date', value: 'albums.releaseDate', }, { - label: 'Album Plays', + default: '=', + disabled: true, + label: 'Album Play Count', value: 'albums.playCount', }, { + default: '<', label: 'Album Date Added', value: 'albums.dateAdded', }, { + default: '~', label: 'Track Title', value: 'songs.name', }, { - label: 'Track Plays', - value: 'songs.playCount', - }, - { + default: '=', label: 'Track Rating', value: 'songs.ratings.value', }, + { + default: '=', + label: 'Track Genre', + value: 'songs.genres.id', + }, + { + default: '=', + disabled: true, + label: 'Track Play Count', + value: 'songs.playCount', + }, ]; const OPTIONS_MAP = { - 'albumArtists.genre': { + 'albumArtists.genres.id': { type: 'id', }, 'albumArtists.name': { @@ -156,7 +182,7 @@ const OPTIONS_MAP = { 'albums.favorite': { type: 'boolean', }, - 'albums.genre': { + 'albums.genres.id': { type: 'id', }, 'albums.name': { @@ -174,7 +200,7 @@ const OPTIONS_MAP = { 'albums.releaseYear': { type: 'number', }, - 'artists.genre': { + 'artists.genres.id': { type: 'id', }, 'artists.name': { @@ -183,6 +209,9 @@ const OPTIONS_MAP = { 'artists.ratings.value': { type: 'number', }, + 'songs.genres.id': { + type: 'id', + }, 'songs.name': { type: 'string', }, @@ -218,7 +247,7 @@ export const formatAdvancedFiltersGroups = (groups: AdvancedFilterGroup[]) => { }; // Prevent query key from constantly changing due to empty rules or groups -export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => { +export const encodeAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => { const updatedFilter = { ...filter, group: formatAdvancedFiltersGroups(filter.group), @@ -227,7 +256,7 @@ export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => { .map((rule) => ({ ...rule, uniqueId: undefined })), }; - return updatedFilter; + return encodeURI(JSON.stringify(updatedFilter)); }; interface FilterOptionProps { @@ -252,6 +281,48 @@ const FilterOption = ({ onChangeValue, }: FilterOptionProps) => { const { field, operator, uniqueId, value } = data; + const { data: genres } = useGenreList(); + + const genresData = useMemo(() => { + if (!genres?.data) return null; + + const album = []; + const song = []; + const albumArtist = []; + const artist = []; + + for (const genre of genres.data) { + if (genre.albumCount > 0) { + album.push({ + label: `${genre.name} (${genre.albumCount})`, + value: genre.id, + }); + } + + if (genre.songCount > 0) { + song.push({ + label: `${genre.name} (${genre.songCount})`, + value: genre.id, + }); + } + + if (genre.albumArtistCount > 0) { + albumArtist.push({ + label: `${genre.name} (${genre.albumArtistCount})`, + value: genre.id, + }); + } + + if (genre.artistCount > 0) { + artist.push({ + label: `${genre.name} (${genre.artistCount})`, + value: genre.id, + }); + } + } + + return { album, albumArtist, artist, song }; + }, [genres]); const handleDeleteRule = () => { onDeleteRule({ groupIndex, level, uniqueId }); @@ -281,6 +352,17 @@ const FilterOption = ({ }); } + const isDate = e instanceof Date; + + if (isDate) { + return onChangeValue({ + groupIndex, + level, + uniqueId, + value: dayjs(e).format('YYYY-MM-DD'), + }); + } + return onChangeValue({ groupIndex, level, @@ -337,10 +419,10 @@ const FilterOption = ({ }; const filterInputValueMap = { - 'albumArtists.genre': ( + 'albumArtists.genres.id': ( ), - 'artists.genre': ( + 'artists.genres.id': ( + ), 'songs.name': ( - {data.rules.map((rule: AdvancedFilterRule) => ( - - ))} - {data.group && ( - <> - {data.group.map((group: AdvancedFilterGroup, index: number) => ( - + {data.rules.map((rule: AdvancedFilterRule) => ( + + + + ))} + + {data.group && ( + + {data.group.map((group: AdvancedFilterGroup, index: number) => ( + + + ))} - + )} ); @@ -701,10 +810,10 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => { group: [], rules: [ { - field: undefined, - operator: undefined, + field: '', + operator: '', uniqueId: nanoid(), - value: undefined, + value: '', }, ], type: FilterGroupType.AND, @@ -800,11 +909,15 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => { path, get(filtersCopy, path).map((rule: AdvancedFilterRule) => { if (rule.uniqueId !== uniqueId) return rule; + const defaultOperator = FILTER_OPTIONS_DATA.find( + (option) => option.value === value + )?.default; + return { ...rule, field: value, - operator: null, - value: null, + operator: defaultOperator || '', + value: '', }; }) );