Add default ops, handle releaseDate

This commit is contained in:
jeffvli
2022-11-03 14:45:00 -07:00
parent 5908554f38
commit f284b29052
3 changed files with 246 additions and 75 deletions
+3 -4
View File
@@ -26,13 +26,12 @@ const getList = async (
const { serverId } = req.params; const { serverId } = req.params;
const { take, skip, serverUrlId, advancedFilters } = req.query; const { take, skip, serverUrlId, advancedFilters } = req.query;
const decodedAdvancedFilters = advancedFilters && decodeURI(advancedFilters); const decodedAdvancedFilters =
const jsonAdvancedFilters = advancedFilters && JSON.parse(decodeURI(advancedFilters));
decodedAdvancedFilters && JSON.parse(decodedAdvancedFilters);
const albums = await service.albums.findMany({ const albums = await service.albums.findMany({
...req.query, ...req.query,
advancedFilters: jsonAdvancedFilters, advancedFilters: decodedAdvancedFilters,
serverId, serverId,
skip: Number(skip), skip: Number(skip),
take: Number(take), take: Number(take),
+71 -12
View File
@@ -134,8 +134,11 @@ const advancedFilterGroup = (
for (const rule of group.rules) { for (const rule of group.rules) {
if (rule.field && rule.operator) { if (rule.field && rule.operator) {
const [table, field, relationField] = rule.field.split('.'); 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 op = operatorMap[rule.operator as keyof typeof operatorMap];
const value =
field !== 'releaseDate' ? rule.value : new Date(rule.value);
switch (table) { switch (table) {
case 'albums': case 'albums':
@@ -144,7 +147,7 @@ const advancedFilterGroup = (
[field]: { [field]: {
[condition]: { [condition]: {
[relationField]: { [relationField]: {
[op]: rule.value, [op]: value,
}, },
userId: user.id, userId: user.id,
}, },
@@ -152,13 +155,24 @@ const advancedFilterGroup = (
}); });
break; break;
} }
if (field === 'genres') {
query[rootType].push({
[field]: {
[condition]: {
[relationField]: {
equals: value,
},
},
},
});
break;
}
query[rootType].push({ query[rootType].push({
[field]: { [field]: {
mode: insensitiveFields.includes(field) mode: insensitiveFields.includes(field)
? 'insensitive' ? 'insensitive'
: undefined, : undefined,
[op]: rule.value, [op]: value,
}, },
}); });
break; break;
@@ -171,7 +185,7 @@ const advancedFilterGroup = (
[field]: { [field]: {
some: { some: {
[relationField]: { [relationField]: {
[op]: rule.value, [op]: value,
}, },
userId: user.id, userId: user.id,
}, },
@@ -181,13 +195,29 @@ const advancedFilterGroup = (
}); });
break; break;
} }
if (field === 'genres') {
query[rootType].push({
[table]: {
some: {
[field]: {
[condition]: {
[relationField]: {
equals: value,
},
},
},
},
},
});
break;
}
query[rootType].push({ query[rootType].push({
[table]: { [table]: {
[condition]: { [condition]: {
[field]: { [field]: {
mode: 'insensitive', mode: 'insensitive',
[op]: rule.value, [op]: value,
}, },
}, },
}, },
@@ -218,8 +248,10 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
for (const rule of filter.rules) { for (const rule of filter.rules) {
if (rule.field && rule.operator) { if (rule.field && rule.operator) {
let [table, field, relationField] = rule.field.split('.'); 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 op = operatorMap[rule.operator as keyof typeof operatorMap];
const value = field !== 'releaseDate' ? rule.value : new Date(rule.value);
switch (table) { switch (table) {
case 'albums': case 'albums':
@@ -228,7 +260,7 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
[field]: { [field]: {
[condition]: { [condition]: {
[relationField]: { [relationField]: {
[op]: rule.value, [op]: value,
}, },
userId: user.id, userId: user.id,
}, },
@@ -236,13 +268,24 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
}); });
break; break;
} }
if (field === 'genres') {
rootQuery[rootQueryType].push({
[field]: {
[condition]: {
[relationField]: {
equals: value,
},
},
},
});
break;
}
rootQuery[rootQueryType].push({ rootQuery[rootQueryType].push({
[field]: { [field]: {
mode: insensitiveFields.includes(field) mode: insensitiveFields.includes(field)
? 'insensitive' ? 'insensitive'
: undefined, : undefined,
[op]: rule.value, [op]: value,
}, },
}); });
break; break;
@@ -255,7 +298,7 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
[field]: { [field]: {
some: { some: {
[relationField]: { [relationField]: {
[op]: rule.value, [op]: value,
}, },
userId: user.id, userId: user.id,
}, },
@@ -265,13 +308,29 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
}); });
break; break;
} }
if (field === 'genres') {
rootQuery[rootQueryType].push({
[table]: {
some: {
[field]: {
[condition]: {
[relationField]: {
equals: value,
},
},
},
},
},
});
break;
}
rootQuery[rootQueryType].push({ rootQuery[rootQueryType].push({
[table]: { [table]: {
[condition]: { [condition]: {
[field]: { [field]: {
mode: 'insensitive', mode: 'insensitive',
[op]: rule.value, [op]: value,
}, },
}, },
}, },
@@ -1,5 +1,7 @@
import { useMemo } from 'react';
import { Stack, Group } from '@mantine/core'; import { Stack, Group } from '@mantine/core';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { AnimatePresence, motion } from 'framer-motion';
import get from 'lodash/get'; import get from 'lodash/get';
import set from 'lodash/set'; import set from 'lodash/set';
import { nanoid } from 'nanoid/non-secure'; import { nanoid } from 'nanoid/non-secure';
@@ -12,6 +14,7 @@ import {
Select, Select,
TextInput, TextInput,
} from '@/renderer/components'; } from '@/renderer/components';
import { useGenreList } from '@/renderer/features/genres';
export enum FilterGroupType { export enum FilterGroupType {
AND = 'AND', AND = 'AND',
@@ -19,10 +22,10 @@ export enum FilterGroupType {
} }
export type AdvancedFilterRule = { export type AdvancedFilterRule = {
field: string | null; field?: string | null;
operator: string | null; operator?: string | null;
uniqueId: string; uniqueId: string;
value: string | number | Date | undefined | null | any; value?: string | number | Date | undefined | null | any;
}; };
export type AdvancedFilterGroup = { export type AdvancedFilterGroup = {
@@ -58,8 +61,8 @@ const NUMBER_FILTER_OPTIONS_DATA = [
]; ];
const ID_FILTER_OPTIONS_DATA = [ const ID_FILTER_OPTIONS_DATA = [
{ label: 'is', value: 'equals' }, { label: 'is', value: '=' },
{ label: 'is not', value: 'not' }, { label: 'is not', value: '!=' },
]; ];
const FILTER_GROUP_OPTIONS_DATA = [ const FILTER_GROUP_OPTIONS_DATA = [
@@ -75,73 +78,96 @@ const FILTER_GROUP_OPTIONS_DATA = [
const FILTER_OPTIONS_DATA = [ const FILTER_OPTIONS_DATA = [
{ {
default: '~',
label: 'Artist Title', label: 'Artist Title',
value: 'artists.name', value: 'artists.name',
}, },
{ {
default: '=',
label: 'Artist Rating', label: 'Artist Rating',
value: 'artists.ratings.value', value: 'artists.ratings.value',
}, },
{ {
default: '=',
label: 'Artist Genre', label: 'Artist Genre',
value: 'artists.genre', value: 'artists.genres.id',
}, },
{ {
default: '~',
label: 'Album Artist Title', label: 'Album Artist Title',
value: 'albumArtists.name', value: 'albumArtists.name',
}, },
{ {
default: '=',
label: 'Album Artist Rating', label: 'Album Artist Rating',
value: 'albumArtists.ratings.value', value: 'albumArtists.ratings.value',
}, },
{ {
default: '=',
label: 'Album Artist Genre', label: 'Album Artist Genre',
value: 'albumArtists.genre', value: 'albumArtists.genres.id',
}, },
{ {
default: '~',
label: 'Album Title', label: 'Album Title',
value: 'albums.name', value: 'albums.name',
}, },
{ {
label: 'Album Genre', default: '=',
value: 'albums.genre',
},
{
label: 'Album Rating', label: 'Album Rating',
value: 'albums.ratings.value', value: 'albums.ratings.value',
}, },
{ {
default: '=',
label: 'Album Genre',
value: 'albums.genres.id',
},
{
default: '=',
label: 'Album Year', label: 'Album Year',
value: 'albums.releaseYear', value: 'albums.releaseYear',
}, },
{ {
default: '<',
label: 'Album Release Date', label: 'Album Release Date',
value: 'albums.releaseDate', value: 'albums.releaseDate',
}, },
{ {
label: 'Album Plays', default: '=',
disabled: true,
label: 'Album Play Count',
value: 'albums.playCount', value: 'albums.playCount',
}, },
{ {
default: '<',
label: 'Album Date Added', label: 'Album Date Added',
value: 'albums.dateAdded', value: 'albums.dateAdded',
}, },
{ {
default: '~',
label: 'Track Title', label: 'Track Title',
value: 'songs.name', value: 'songs.name',
}, },
{ {
label: 'Track Plays', default: '=',
value: 'songs.playCount',
},
{
label: 'Track Rating', label: 'Track Rating',
value: 'songs.ratings.value', 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 = { const OPTIONS_MAP = {
'albumArtists.genre': { 'albumArtists.genres.id': {
type: 'id', type: 'id',
}, },
'albumArtists.name': { 'albumArtists.name': {
@@ -156,7 +182,7 @@ const OPTIONS_MAP = {
'albums.favorite': { 'albums.favorite': {
type: 'boolean', type: 'boolean',
}, },
'albums.genre': { 'albums.genres.id': {
type: 'id', type: 'id',
}, },
'albums.name': { 'albums.name': {
@@ -174,7 +200,7 @@ const OPTIONS_MAP = {
'albums.releaseYear': { 'albums.releaseYear': {
type: 'number', type: 'number',
}, },
'artists.genre': { 'artists.genres.id': {
type: 'id', type: 'id',
}, },
'artists.name': { 'artists.name': {
@@ -183,6 +209,9 @@ const OPTIONS_MAP = {
'artists.ratings.value': { 'artists.ratings.value': {
type: 'number', type: 'number',
}, },
'songs.genres.id': {
type: 'id',
},
'songs.name': { 'songs.name': {
type: 'string', type: 'string',
}, },
@@ -218,7 +247,7 @@ export const formatAdvancedFiltersGroups = (groups: AdvancedFilterGroup[]) => {
}; };
// Prevent query key from constantly changing due to empty rules or groups // Prevent query key from constantly changing due to empty rules or groups
export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => { export const encodeAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => {
const updatedFilter = { const updatedFilter = {
...filter, ...filter,
group: formatAdvancedFiltersGroups(filter.group), group: formatAdvancedFiltersGroups(filter.group),
@@ -227,7 +256,7 @@ export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => {
.map((rule) => ({ ...rule, uniqueId: undefined })), .map((rule) => ({ ...rule, uniqueId: undefined })),
}; };
return updatedFilter; return encodeURI(JSON.stringify(updatedFilter));
}; };
interface FilterOptionProps { interface FilterOptionProps {
@@ -252,6 +281,48 @@ const FilterOption = ({
onChangeValue, onChangeValue,
}: FilterOptionProps) => { }: FilterOptionProps) => {
const { field, operator, uniqueId, value } = data; 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 = () => { const handleDeleteRule = () => {
onDeleteRule({ groupIndex, level, uniqueId }); 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({ return onChangeValue({
groupIndex, groupIndex,
level, level,
@@ -337,10 +419,10 @@ const FilterOption = ({
}; };
const filterInputValueMap = { const filterInputValueMap = {
'albumArtists.genre': ( 'albumArtists.genres.id': (
<Select <Select
searchable searchable
data={[]} data={genresData?.albumArtist || []}
maxWidth={175} maxWidth={175}
size="xs" size="xs"
value={value} value={value}
@@ -380,10 +462,10 @@ const FilterOption = ({
onChange={handleChangeValue} onChange={handleChangeValue}
/> />
), ),
'albums.genre': ( 'albums.genres.id': (
<Select <Select
searchable searchable
data={[]} data={genresData?.album || []}
maxWidth={175} maxWidth={175}
size="xs" size="xs"
value={value} value={value}
@@ -443,10 +525,10 @@ const FilterOption = ({
onChange={handleChangeValue} onChange={handleChangeValue}
/> />
), ),
'artists.genre': ( 'artists.genres.id': (
<Select <Select
searchable searchable
data={[]} data={genresData?.artist || []}
maxWidth={175} maxWidth={175}
size="xs" size="xs"
value={value} value={value}
@@ -474,6 +556,17 @@ const FilterOption = ({
onChange={handleChangeValue} onChange={handleChangeValue}
/> />
), ),
'songs.genres.id': (
<Select
searchable
data={genresData?.song || []}
maxWidth={175}
size="xs"
value={value}
width="20%"
onChange={handleChangeValue}
/>
),
'songs.name': ( 'songs.name': (
<TextInput <TextInput
maxWidth={175} maxWidth={175}
@@ -640,39 +733,55 @@ const FilterGroup = ({
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
</DropdownMenu> </DropdownMenu>
</Group> </Group>
{data.rules.map((rule: AdvancedFilterRule) => ( <AnimatePresence key="advanced-filter-option" initial={false}>
<FilterOption {data.rules.map((rule: AdvancedFilterRule) => (
key={rule.uniqueId} <motion.div
data={rule} key={rule.uniqueId}
groupIndex={groupIndex || []} animate={{ opacity: 1, x: 0 }}
level={level} exit={{ opacity: 0, x: -25 }}
noRemove={data.rules.length === 1} initial={{ opacity: 0, x: -25 }}
onChangeField={onChangeField} transition={{ duration: 0.2, ease: 'easeInOut' }}
onChangeOperator={onChangeOperator} >
onChangeValue={onChangeValue} <FilterOption
onDeleteRule={onDeleteRule} data={rule}
/> groupIndex={groupIndex || []}
))} level={level}
{data.group && ( noRemove={data.rules.length === 1}
<>
{data.group.map((group: AdvancedFilterGroup, index: number) => (
<FilterGroup
key={group.uniqueId}
data={group}
groupIndex={[...(groupIndex || []), index]}
level={level + 1}
uniqueId={group.uniqueId}
onAddRule={onAddRule}
onAddRuleGroup={onAddRuleGroup}
onChangeField={onChangeField} onChangeField={onChangeField}
onChangeOperator={onChangeOperator} onChangeOperator={onChangeOperator}
onChangeType={onChangeType}
onChangeValue={onChangeValue} onChangeValue={onChangeValue}
onDeleteRule={onDeleteRule} onDeleteRule={onDeleteRule}
onDeleteRuleGroup={onDeleteRuleGroup}
/> />
</motion.div>
))}
</AnimatePresence>
{data.group && (
<AnimatePresence key="advanced-filter-group" initial={false}>
{data.group.map((group: AdvancedFilterGroup, index: number) => (
<motion.div
key={group.uniqueId}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -25 }}
initial={{ opacity: 0, x: -25 }}
transition={{ duration: 0.2, ease: 'easeInOut' }}
>
<FilterGroup
data={group}
groupIndex={[...(groupIndex || []), index]}
level={level + 1}
uniqueId={group.uniqueId}
onAddRule={onAddRule}
onAddRuleGroup={onAddRuleGroup}
onChangeField={onChangeField}
onChangeOperator={onChangeOperator}
onChangeType={onChangeType}
onChangeValue={onChangeValue}
onDeleteRule={onDeleteRule}
onDeleteRuleGroup={onDeleteRuleGroup}
/>
</motion.div>
))} ))}
</> </AnimatePresence>
)} )}
</Stack> </Stack>
); );
@@ -701,10 +810,10 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
group: [], group: [],
rules: [ rules: [
{ {
field: undefined, field: '',
operator: undefined, operator: '',
uniqueId: nanoid(), uniqueId: nanoid(),
value: undefined, value: '',
}, },
], ],
type: FilterGroupType.AND, type: FilterGroupType.AND,
@@ -800,11 +909,15 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
path, path,
get(filtersCopy, path).map((rule: AdvancedFilterRule) => { get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
if (rule.uniqueId !== uniqueId) return rule; if (rule.uniqueId !== uniqueId) return rule;
const defaultOperator = FILTER_OPTIONS_DATA.find(
(option) => option.value === value
)?.default;
return { return {
...rule, ...rule,
field: value, field: value,
operator: null, operator: defaultOperator || '',
value: null, value: '',
}; };
}) })
); );