mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Progress on advanced filters
This commit is contained in:
@@ -24,10 +24,15 @@ const getList = async (
|
||||
res: Response
|
||||
) => {
|
||||
const { serverId } = req.params;
|
||||
const { take, skip, serverUrlId } = req.query;
|
||||
const { take, skip, serverUrlId, advancedFilters } = req.query;
|
||||
|
||||
const decodedAdvancedFilters = advancedFilters && decodeURI(advancedFilters);
|
||||
const jsonAdvancedFilters =
|
||||
decodedAdvancedFilters && JSON.parse(decodedAdvancedFilters);
|
||||
|
||||
const albums = await service.albums.findMany({
|
||||
...req.query,
|
||||
advancedFilters: jsonAdvancedFilters,
|
||||
serverId,
|
||||
skip: Number(skip),
|
||||
take: Number(take),
|
||||
@@ -66,6 +71,7 @@ const getDetailSongList = async (
|
||||
|
||||
const albums = await service.albums.findMany({
|
||||
...req.query,
|
||||
advancedFilters: undefined,
|
||||
serverId,
|
||||
skip: Number(skip),
|
||||
take: Number(take),
|
||||
|
||||
@@ -57,7 +57,7 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_RELEASED:
|
||||
order = { releaseDate: orderBy, year: orderBy };
|
||||
order = { releaseDate: orderBy };
|
||||
break;
|
||||
|
||||
case AlbumSort.DATE_RELEASED_YEAR:
|
||||
@@ -80,7 +80,204 @@ const sort = (sortBy: AlbumSort, orderBy: SortOrder) => {
|
||||
return order;
|
||||
};
|
||||
|
||||
export enum FilterGroupType {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
}
|
||||
|
||||
export type AdvancedFilterRule = {
|
||||
field: string | null;
|
||||
operator: string | null;
|
||||
uniqueId: string;
|
||||
value: string | number | Date | undefined | null | any;
|
||||
};
|
||||
|
||||
export type AdvancedFilterGroup = {
|
||||
group: AdvancedFilterGroup[];
|
||||
rules: AdvancedFilterRule[];
|
||||
type: FilterGroupType;
|
||||
uniqueId: string;
|
||||
};
|
||||
|
||||
const advancedFilterGroup = (
|
||||
groups: AdvancedFilterGroup[],
|
||||
user: AuthUser,
|
||||
data: any[]
|
||||
) => {
|
||||
if (groups.length === 0) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const filterGroups: any[] = [];
|
||||
|
||||
for (const group of groups) {
|
||||
const rootType = group.type.toUpperCase();
|
||||
const query: any = {
|
||||
[rootType]: [],
|
||||
};
|
||||
|
||||
for (const rule of group.rules) {
|
||||
if (rule.field && rule.operator) {
|
||||
const [table, field, relationField] = rule.field.split('.');
|
||||
|
||||
if (field === 'ratings') {
|
||||
if (table === 'albums') {
|
||||
query[rootType].push({
|
||||
[field]: {
|
||||
some: {
|
||||
[relationField]: {
|
||||
[rule.operator]: rule.value,
|
||||
},
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
query[rootType].push({
|
||||
[table]: {
|
||||
some: {
|
||||
[field]: {
|
||||
some: {
|
||||
[relationField]: {
|
||||
[rule.operator]: rule.value,
|
||||
},
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (table === 'albums') {
|
||||
const obj = {
|
||||
[field]: {
|
||||
[rule.operator]: rule.value,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
};
|
||||
|
||||
query[rootType].push(obj);
|
||||
} else {
|
||||
const obj = {
|
||||
[table]: {
|
||||
some: {
|
||||
[field]: {
|
||||
[rule.operator]: rule.value,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
query[rootType].push(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (group.group.length > 0) {
|
||||
const b = advancedFilterGroup(group.group, user, data);
|
||||
b.forEach((c) => query[rootType].push(c));
|
||||
}
|
||||
|
||||
data.push(query);
|
||||
filterGroups.push(query);
|
||||
}
|
||||
|
||||
return filterGroups;
|
||||
};
|
||||
|
||||
const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
|
||||
const rootQueryType = filter.type.toUpperCase();
|
||||
const rootQuery = {
|
||||
[rootQueryType]: [] as any[],
|
||||
};
|
||||
|
||||
const operatorMap = {
|
||||
'!=': 'not',
|
||||
'!~': 'contains',
|
||||
$: 'endsWith',
|
||||
'<': 'lt',
|
||||
'<=': 'lte',
|
||||
'=': 'equals',
|
||||
'>': 'gt',
|
||||
'>=': 'gte',
|
||||
'^': 'startsWith',
|
||||
'~': 'contains',
|
||||
};
|
||||
|
||||
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 op = operatorMap[rule.operator as keyof typeof operatorMap];
|
||||
|
||||
switch (table) {
|
||||
case 'albums':
|
||||
if (field === 'ratings') {
|
||||
rootQuery[rootQueryType].push({
|
||||
[field]: {
|
||||
[condition]: {
|
||||
[relationField]: {
|
||||
[op]: rule.value,
|
||||
},
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
rootQuery[rootQueryType].push({
|
||||
[field]: {
|
||||
mode: 'insensitive',
|
||||
[op]: rule.value,
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
if (field === 'ratings') {
|
||||
rootQuery[rootQueryType].push({
|
||||
[table]: {
|
||||
some: {
|
||||
[field]: {
|
||||
some: {
|
||||
[relationField]: {
|
||||
[op]: rule.value,
|
||||
},
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
rootQuery[rootQueryType].push({
|
||||
[table]: {
|
||||
[condition]: {
|
||||
[field]: {
|
||||
mode: 'insensitive',
|
||||
[op]: rule.value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const groups = advancedFilterGroup(filter.group, user, []);
|
||||
for (const group of groups) {
|
||||
rootQuery[rootQueryType].push(group);
|
||||
}
|
||||
|
||||
return rootQuery;
|
||||
};
|
||||
|
||||
export const albumHelpers = {
|
||||
advancedFilter,
|
||||
include,
|
||||
sort,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { AuthUser } from '@/middleware';
|
||||
import { OffsetPagination, SortOrder } from '@/types/types';
|
||||
import { ApiError } from '@/utils';
|
||||
import { AlbumSort } from '@helpers/albums.helpers';
|
||||
import { AdvancedFilterGroup, AlbumSort } from '@helpers/albums.helpers';
|
||||
import { helpers } from '@helpers/index';
|
||||
import { prisma } from '@lib/prisma';
|
||||
|
||||
@@ -24,6 +25,7 @@ const findById = async (user: AuthUser, options: { id: string }) => {
|
||||
};
|
||||
|
||||
export type AlbumFindManyOptions = {
|
||||
advancedFilters?: AdvancedFilterGroup;
|
||||
orderBy: SortOrder;
|
||||
serverFolderId?: string[];
|
||||
serverId: string;
|
||||
@@ -32,8 +34,16 @@ export type AlbumFindManyOptions = {
|
||||
} & OffsetPagination;
|
||||
|
||||
const findMany = async (options: AlbumFindManyOptions) => {
|
||||
const { take, serverFolderId, skip, sortBy, orderBy, user, serverId } =
|
||||
options;
|
||||
const {
|
||||
take,
|
||||
serverFolderId,
|
||||
skip,
|
||||
sortBy,
|
||||
orderBy,
|
||||
user,
|
||||
serverId,
|
||||
advancedFilters,
|
||||
} = options;
|
||||
|
||||
const serverFolderIds =
|
||||
serverFolderId ||
|
||||
@@ -42,6 +52,9 @@ const findMany = async (options: AlbumFindManyOptions) => {
|
||||
let totalEntries = 0;
|
||||
let albums;
|
||||
|
||||
const advancedFiltersQuery =
|
||||
advancedFilters && helpers.albums.advancedFilter(advancedFilters, user);
|
||||
|
||||
if (sortBy === AlbumSort.RATING) {
|
||||
const [count, result] = await prisma.$transaction([
|
||||
prisma.albumRating.count({
|
||||
@@ -92,14 +105,24 @@ const findMany = async (options: AlbumFindManyOptions) => {
|
||||
} else {
|
||||
[totalEntries, albums] = await prisma.$transaction([
|
||||
prisma.album.count({
|
||||
where: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||
where: {
|
||||
AND: [
|
||||
helpers.shared.serverFolderFilter(serverFolderIds),
|
||||
advancedFiltersQuery as Prisma.AlbumWhereInput,
|
||||
],
|
||||
},
|
||||
}),
|
||||
prisma.album.findMany({
|
||||
include: helpers.albums.include(user, { songs: false }),
|
||||
orderBy: [helpers.albums.sort(sortBy, orderBy)],
|
||||
skip,
|
||||
take,
|
||||
where: { OR: helpers.shared.serverFolderFilter(serverFolderIds) },
|
||||
where: {
|
||||
AND: [
|
||||
helpers.shared.serverFolderFilter(serverFolderIds),
|
||||
advancedFiltersQuery as Prisma.AlbumWhereInput,
|
||||
],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const list = {
|
||||
...serverFolderIdValidation,
|
||||
...orderByValidation,
|
||||
...serverUrlIdValidation,
|
||||
advancedFilters: z.optional(z.string()),
|
||||
sortBy: z.nativeEnum(AlbumSort),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export enum AlbumSort {
|
||||
}
|
||||
|
||||
export type AlbumListParams = PaginationParams & {
|
||||
advancedFilters?: string;
|
||||
orderBy: SortOrder;
|
||||
serverFolderId?: string[];
|
||||
serverUrlId?: string;
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { useState } from 'react';
|
||||
import { Box, Stack, Group } from '@mantine/core';
|
||||
import _ from 'lodash';
|
||||
import dayjs from 'dayjs';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { RiAddLine, RiMore2Line, RiSubtractLine } from 'react-icons/ri';
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
NumberInput,
|
||||
Select,
|
||||
TextInput,
|
||||
} from '@/renderer/components';
|
||||
|
||||
enum FilterGroupType {
|
||||
AND = 'and',
|
||||
OR = 'or',
|
||||
export enum FilterGroupType {
|
||||
AND = 'AND',
|
||||
OR = 'OR',
|
||||
}
|
||||
|
||||
type AdvancedFilterRule = {
|
||||
field: string;
|
||||
operator: string;
|
||||
export type AdvancedFilterRule = {
|
||||
field: string | null;
|
||||
operator: string | null;
|
||||
uniqueId: string;
|
||||
value: string;
|
||||
value: string | number | Date | undefined | null | any;
|
||||
};
|
||||
|
||||
type AdvancedFilterGroup = {
|
||||
export type AdvancedFilterGroup = {
|
||||
group: AdvancedFilterGroup[];
|
||||
rules: AdvancedFilterRule[];
|
||||
type: FilterGroupType;
|
||||
@@ -33,6 +35,8 @@ type AdvancedFilterGroup = {
|
||||
const DATE_FILTER_OPTIONS_DATA = [
|
||||
{ label: 'is before', value: '<' },
|
||||
{ label: 'is after', value: '>' },
|
||||
{ label: 'is before or equal to', value: '<=' },
|
||||
{ label: 'is after or equal to', value: '>=' },
|
||||
];
|
||||
|
||||
const STRING_FILTER_OPTIONS_DATA = [
|
||||
@@ -49,164 +53,413 @@ const NUMBER_FILTER_OPTIONS_DATA = [
|
||||
{ label: 'is not', value: '!=' },
|
||||
{ label: 'is greater than', value: '>' },
|
||||
{ label: 'is less than', value: '<' },
|
||||
{ label: 'is greater than or equal to', value: '>=' },
|
||||
{ label: 'is less than or equal to', value: '<=' },
|
||||
];
|
||||
|
||||
const ID_FILTER_OPTIONS_DATA = [
|
||||
{ label: 'is', value: 'equals' },
|
||||
{ label: 'is not', value: 'not' },
|
||||
];
|
||||
|
||||
const FILTER_GROUP_OPTIONS_DATA = [
|
||||
{
|
||||
label: 'Match ALL',
|
||||
label: 'Match all',
|
||||
value: FilterGroupType.AND,
|
||||
},
|
||||
{
|
||||
label: 'Match ANY',
|
||||
label: 'Match any',
|
||||
value: FilterGroupType.OR,
|
||||
},
|
||||
];
|
||||
|
||||
const FILTER_OPTIONS_DATA = [
|
||||
{
|
||||
label: 'Artist Title',
|
||||
value: 'artist.title',
|
||||
label: 'Artist Name',
|
||||
value: 'artists.name',
|
||||
},
|
||||
{
|
||||
label: 'Artist Rating',
|
||||
value: 'artist.rating',
|
||||
value: 'artists.ratings.value',
|
||||
},
|
||||
{
|
||||
label: 'Artist Genre',
|
||||
value: 'artist.genre',
|
||||
value: 'artists.genre',
|
||||
},
|
||||
{
|
||||
label: 'Album Title',
|
||||
value: 'album.title',
|
||||
label: 'Album Artist Name',
|
||||
value: 'albumArtists.name',
|
||||
},
|
||||
{
|
||||
label: 'Album Artist Rating',
|
||||
value: 'albumArtists.ratings.value',
|
||||
},
|
||||
{
|
||||
label: 'Album Artist Genre',
|
||||
value: 'albumArtists.genre',
|
||||
},
|
||||
{
|
||||
label: 'Album Name',
|
||||
value: 'albums.name',
|
||||
},
|
||||
{
|
||||
label: 'Album Genre',
|
||||
value: 'album.genre',
|
||||
value: 'albums.genre',
|
||||
},
|
||||
{
|
||||
label: 'Album Rating',
|
||||
value: 'album.rating',
|
||||
value: 'albums.ratings.value',
|
||||
},
|
||||
{
|
||||
label: 'Album Year',
|
||||
value: 'album.year',
|
||||
value: 'albums.year',
|
||||
},
|
||||
{
|
||||
label: 'Album Release Date',
|
||||
value: 'album.releaseDate',
|
||||
value: 'albums.releaseDate',
|
||||
},
|
||||
{
|
||||
label: 'Album Plays',
|
||||
value: 'album.playCount',
|
||||
value: 'albums.playCount',
|
||||
},
|
||||
{
|
||||
label: 'Album Date Added',
|
||||
value: 'album.dateAdded',
|
||||
value: 'albums.dateAdded',
|
||||
},
|
||||
{
|
||||
label: 'Track Title',
|
||||
value: 'track.title',
|
||||
label: 'Track Name',
|
||||
value: 'songs.name',
|
||||
},
|
||||
{
|
||||
label: 'Track Plays',
|
||||
value: 'track.plays',
|
||||
value: 'songs.playCount',
|
||||
},
|
||||
{
|
||||
label: 'Track Rating',
|
||||
value: 'track.rating',
|
||||
value: 'songs.ratings.value',
|
||||
},
|
||||
];
|
||||
|
||||
const OPTIONS_MAP = {
|
||||
'album.dateAdded': {
|
||||
'albumArtists.genre': {
|
||||
type: 'id',
|
||||
},
|
||||
'albumArtists.name': {
|
||||
type: 'string',
|
||||
},
|
||||
'albumArtists.ratings.value': {
|
||||
type: 'number',
|
||||
},
|
||||
'albums.dateAdded': {
|
||||
type: 'date',
|
||||
},
|
||||
'album.favorite': {
|
||||
'albums.favorite': {
|
||||
type: 'boolean',
|
||||
},
|
||||
'album.genre': {
|
||||
'albums.genre': {
|
||||
type: 'id',
|
||||
},
|
||||
'albums.name': {
|
||||
type: 'string',
|
||||
},
|
||||
'album.playCount': {
|
||||
'albums.playCount': {
|
||||
type: 'number',
|
||||
},
|
||||
'album.rating': {
|
||||
'albums.ratings.value': {
|
||||
type: 'number',
|
||||
},
|
||||
'album.releaseDate': {
|
||||
'albums.releaseDate': {
|
||||
type: 'date',
|
||||
},
|
||||
'album.title': {
|
||||
type: 'string',
|
||||
},
|
||||
'album.year': {
|
||||
'albums.year': {
|
||||
type: 'number',
|
||||
},
|
||||
'artist.genre': {
|
||||
'artists.genre': {
|
||||
type: 'id',
|
||||
},
|
||||
'artists.name': {
|
||||
type: 'string',
|
||||
},
|
||||
'artist.rating': {
|
||||
'artists.ratings.value': {
|
||||
type: 'number',
|
||||
},
|
||||
'artist.title': {
|
||||
'songs.name': {
|
||||
type: 'string',
|
||||
},
|
||||
'track.plays': {
|
||||
'songs.playCount': {
|
||||
type: 'number',
|
||||
},
|
||||
'track.rating': {
|
||||
'songs.ratings.value': {
|
||||
type: 'number',
|
||||
},
|
||||
'track.title': {
|
||||
type: 'string',
|
||||
},
|
||||
};
|
||||
|
||||
const FilterOption = ({ level, onDeleteRule, uniqueId, groupIndex }: any) => {
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
string | null | typeof OPTIONS_MAP
|
||||
>(FILTER_OPTIONS_DATA[0].value);
|
||||
interface FilterOptionProps {
|
||||
data: AdvancedFilterRule;
|
||||
groupIndex: number[];
|
||||
level: number;
|
||||
onChangeField: (args: any) => void;
|
||||
onChangeOperator: (args: any) => void;
|
||||
onChangeValue: (args: any) => void;
|
||||
onDeleteRule: (args: DeleteArgs) => void;
|
||||
}
|
||||
|
||||
const FilterOption = ({
|
||||
data,
|
||||
level,
|
||||
onDeleteRule,
|
||||
groupIndex,
|
||||
onChangeField,
|
||||
onChangeOperator,
|
||||
onChangeValue,
|
||||
}: FilterOptionProps) => {
|
||||
const { field, operator, uniqueId, value } = data;
|
||||
|
||||
const handleDeleteRule = () => {
|
||||
onDeleteRule({ groupIndex, level, uniqueId });
|
||||
};
|
||||
|
||||
const filterMap = {
|
||||
date: <Select data={DATE_FILTER_OPTIONS_DATA} size="xs" width={150} />,
|
||||
number: <Select data={NUMBER_FILTER_OPTIONS_DATA} size="xs" width={150} />,
|
||||
string: <Select data={STRING_FILTER_OPTIONS_DATA} size="xs" width={150} />,
|
||||
const handleChangeField = (e: any) => {
|
||||
onChangeField({ groupIndex, level, uniqueId, value: e });
|
||||
};
|
||||
|
||||
const filterInputMap = {
|
||||
'album.dateAdded': <TextInput size="xs" width={150} />,
|
||||
'album.genre': <Select searchable data={['hello']} size="xs" width={150} />,
|
||||
'album.playCount': <NumberInput size="xs" width={150} />,
|
||||
'album.rating': <NumberInput size="xs" width={150} />,
|
||||
'album.releaseDate': <TextInput size="xs" width={150} />,
|
||||
'album.title': <TextInput size="xs" width={150} />,
|
||||
'album.year': <NumberInput size="xs" width={150} />,
|
||||
'artist.genre': <Select searchable data={[]} size="xs" width={150} />,
|
||||
'artist.rating': <NumberInput size="xs" width={150} />,
|
||||
'artist.title': <TextInput size="xs" width={150} />,
|
||||
'track.plays': <NumberInput size="xs" width={150} />,
|
||||
'track.rating': <NumberInput size="xs" width={150} />,
|
||||
'track.title': <TextInput size="xs" width={150} />,
|
||||
const handleChangeOperator = (e: any) => {
|
||||
onChangeOperator({ groupIndex, level, uniqueId, value: e });
|
||||
};
|
||||
|
||||
const handleChangeValue = (e: any) => {
|
||||
const isDirectValue =
|
||||
typeof e === 'string' ||
|
||||
typeof e === 'number' ||
|
||||
typeof e === 'undefined' ||
|
||||
typeof e === null;
|
||||
|
||||
if (isDirectValue) {
|
||||
return onChangeValue({
|
||||
groupIndex,
|
||||
level,
|
||||
uniqueId,
|
||||
value: e,
|
||||
});
|
||||
}
|
||||
|
||||
return onChangeValue({
|
||||
groupIndex,
|
||||
level,
|
||||
uniqueId,
|
||||
value: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
const filterOperatorMap = {
|
||||
date: (
|
||||
<Select
|
||||
data={DATE_FILTER_OPTIONS_DATA}
|
||||
size="xs"
|
||||
value={operator}
|
||||
width={150}
|
||||
onChange={handleChangeOperator}
|
||||
/>
|
||||
),
|
||||
id: (
|
||||
<Select
|
||||
data={ID_FILTER_OPTIONS_DATA}
|
||||
size="xs"
|
||||
value={operator}
|
||||
width={150}
|
||||
onChange={handleChangeOperator}
|
||||
/>
|
||||
),
|
||||
number: (
|
||||
<Select
|
||||
data={NUMBER_FILTER_OPTIONS_DATA}
|
||||
size="xs"
|
||||
value={operator}
|
||||
width={150}
|
||||
onChange={handleChangeOperator}
|
||||
/>
|
||||
),
|
||||
string: (
|
||||
<Select
|
||||
data={STRING_FILTER_OPTIONS_DATA}
|
||||
size="xs"
|
||||
value={operator}
|
||||
width={150}
|
||||
onChange={handleChangeOperator}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
const filterInputValueMap = {
|
||||
'albumArtists.genre': (
|
||||
<Select
|
||||
searchable
|
||||
data={[]}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albumArtists.name': (
|
||||
<TextInput
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albumArtists.ratings.value': (
|
||||
<NumberInput
|
||||
max={5}
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albums.dateAdded': (
|
||||
<DatePicker
|
||||
initialLevel="year"
|
||||
maxDate={dayjs(new Date()).year(3000).toDate()}
|
||||
minDate={dayjs(new Date()).year(1950).toDate()}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albums.genre': (
|
||||
<Select
|
||||
searchable
|
||||
data={[]}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albums.name': (
|
||||
<TextInput
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albums.playCount': (
|
||||
<NumberInput
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={(e) => handleChangeValue(e)}
|
||||
/>
|
||||
),
|
||||
'albums.ratings.value': (
|
||||
<NumberInput
|
||||
max={5}
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albums.releaseDate': (
|
||||
<DatePicker
|
||||
initialLevel="year"
|
||||
maxDate={dayjs(new Date()).year(3000).toDate()}
|
||||
minDate={dayjs(new Date()).year(1950).toDate()}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'albums.year': (
|
||||
<NumberInput
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'artists.genre': (
|
||||
<Select
|
||||
searchable
|
||||
data={[]}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'artists.name': (
|
||||
<TextInput
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'artists.ratings.value': (
|
||||
<NumberInput
|
||||
max={5}
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'songs.name': (
|
||||
<TextInput size="xs" width={150} onChange={handleChangeValue} />
|
||||
),
|
||||
'songs.playCount': (
|
||||
<NumberInput
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
'songs.ratings.value': (
|
||||
<NumberInput
|
||||
max={5}
|
||||
min={0}
|
||||
size="xs"
|
||||
value={value}
|
||||
width={150}
|
||||
onChange={handleChangeValue}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<Group ml={level === 0 ? '10px' : `${level * 10}px`}>
|
||||
<Group ml={`${(level + 1) * 10}px`}>
|
||||
<Select
|
||||
data={FILTER_OPTIONS_DATA}
|
||||
size="xs"
|
||||
onChange={setSelectedOption}
|
||||
value={field}
|
||||
onChange={handleChangeField}
|
||||
/>
|
||||
{selectedOption &&
|
||||
filterMap[
|
||||
OPTIONS_MAP[selectedOption as keyof typeof OPTIONS_MAP]
|
||||
.type as keyof typeof filterMap
|
||||
]}
|
||||
{selectedOption &&
|
||||
filterInputMap[selectedOption as keyof typeof filterInputMap]}
|
||||
{field ? (
|
||||
filterOperatorMap[
|
||||
OPTIONS_MAP[field as keyof typeof OPTIONS_MAP]
|
||||
.type as keyof typeof filterOperatorMap
|
||||
]
|
||||
) : (
|
||||
<TextInput disabled size="xs" width={150} />
|
||||
)}
|
||||
{field ? (
|
||||
filterInputValueMap[field as keyof typeof filterInputValueMap]
|
||||
) : (
|
||||
<TextInput disabled size="xs" width={150} />
|
||||
)}
|
||||
<Button
|
||||
px={5}
|
||||
size="xs"
|
||||
@@ -237,6 +490,10 @@ interface FilterGroupProps {
|
||||
level: number;
|
||||
onAddRule: (args: AddArgs) => void;
|
||||
onAddRuleGroup: (args: AddArgs) => void;
|
||||
onChangeField: (args: any) => void;
|
||||
onChangeOperator: (args: any) => void;
|
||||
onChangeType: (args: any) => void;
|
||||
onChangeValue: (args: any) => void;
|
||||
onDeleteRule: (args: DeleteArgs) => void;
|
||||
onDeleteRuleGroup: (args: DeleteArgs) => void;
|
||||
uniqueId: string;
|
||||
@@ -249,6 +506,10 @@ const FilterGroup = ({
|
||||
onDeleteRuleGroup,
|
||||
onDeleteRule,
|
||||
onAddRuleGroup,
|
||||
onChangeType,
|
||||
onChangeField,
|
||||
onChangeOperator,
|
||||
onChangeValue,
|
||||
groupIndex,
|
||||
uniqueId,
|
||||
}: FilterGroupProps) => {
|
||||
@@ -264,13 +525,18 @@ const FilterGroup = ({
|
||||
onDeleteRuleGroup({ groupIndex, level, uniqueId });
|
||||
};
|
||||
|
||||
const handleChangeType = (value: string | null) => {
|
||||
onChangeType({ groupIndex, level, value });
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack ml={`${level * 10}px`}>
|
||||
<Group>
|
||||
<Select
|
||||
data={FILTER_GROUP_OPTIONS_DATA}
|
||||
defaultValue={FILTER_GROUP_OPTIONS_DATA[0].value}
|
||||
size="xs"
|
||||
value={data.type}
|
||||
onChange={handleChangeType}
|
||||
/>
|
||||
<Button
|
||||
px={5}
|
||||
@@ -300,16 +566,16 @@ const FilterGroup = ({
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
{data.rules.map((rule: AdvancedFilterRule) => (
|
||||
<>
|
||||
<FilterOption
|
||||
key={rule.uniqueId}
|
||||
groupIndex={groupIndex || []}
|
||||
level={level}
|
||||
uniqueId={rule.uniqueId}
|
||||
onAddRule={handleAddRule}
|
||||
onDeleteRule={onDeleteRule}
|
||||
/>
|
||||
</>
|
||||
<FilterOption
|
||||
key={rule.uniqueId}
|
||||
data={rule}
|
||||
groupIndex={groupIndex || []}
|
||||
level={level}
|
||||
onChangeField={onChangeField}
|
||||
onChangeOperator={onChangeOperator}
|
||||
onChangeValue={onChangeValue}
|
||||
onDeleteRule={onDeleteRule}
|
||||
/>
|
||||
))}
|
||||
{data.group && (
|
||||
<>
|
||||
@@ -322,6 +588,10 @@ const FilterGroup = ({
|
||||
uniqueId={group.uniqueId}
|
||||
onAddRule={onAddRule}
|
||||
onAddRuleGroup={onAddRuleGroup}
|
||||
onChangeField={onChangeField}
|
||||
onChangeOperator={onChangeOperator}
|
||||
onChangeType={onChangeType}
|
||||
onChangeValue={onChangeValue}
|
||||
onDeleteRule={onDeleteRule}
|
||||
onDeleteRuleGroup={onDeleteRuleGroup}
|
||||
/>
|
||||
@@ -332,14 +602,7 @@ const FilterGroup = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const AdvancedFilters = () => {
|
||||
const [filters, setFilters] = useState<AdvancedFilterGroup>({
|
||||
group: [],
|
||||
rules: [],
|
||||
type: FilterGroupType.AND,
|
||||
uniqueId: nanoid(),
|
||||
});
|
||||
|
||||
export const AdvancedFilters = ({ filters, setFilters }: any) => {
|
||||
const handleAddRuleGroup = (args: AddArgs) => {
|
||||
const { level, groupIndex } = args;
|
||||
const filtersCopy = { ...filters };
|
||||
@@ -356,12 +619,19 @@ export const AdvancedFilters = () => {
|
||||
};
|
||||
|
||||
const path = getPath(level);
|
||||
const updatedFilters = _.set(filtersCopy, path, [
|
||||
..._.get(filtersCopy, path),
|
||||
const updatedFilters = set(filtersCopy, path, [
|
||||
...get(filtersCopy, path),
|
||||
{
|
||||
group: [],
|
||||
rules: [],
|
||||
type: 'push',
|
||||
rules: [
|
||||
{
|
||||
field: undefined,
|
||||
operator: undefined,
|
||||
uniqueId: nanoid(),
|
||||
value: undefined,
|
||||
},
|
||||
],
|
||||
type: FilterGroupType.AND,
|
||||
uniqueId: nanoid(),
|
||||
},
|
||||
]);
|
||||
@@ -390,8 +660,8 @@ export const AdvancedFilters = () => {
|
||||
|
||||
const path = getPath(level);
|
||||
|
||||
const updatedFilters = _.set(filtersCopy, path, [
|
||||
..._.get(filtersCopy, path).filter(
|
||||
const updatedFilters = set(filtersCopy, path, [
|
||||
...get(filtersCopy, path).filter(
|
||||
(group: AdvancedFilterGroup) => group.uniqueId !== uniqueId
|
||||
),
|
||||
]);
|
||||
@@ -399,25 +669,30 @@ export const AdvancedFilters = () => {
|
||||
setFilters(updatedFilters);
|
||||
};
|
||||
|
||||
const getRulePath = (level: number, groupIndex: number[]) => {
|
||||
if (level === 0) return 'rules';
|
||||
|
||||
const str = [];
|
||||
for (const index of groupIndex) {
|
||||
str.push(`group[${index}]`);
|
||||
}
|
||||
|
||||
return `${str.join('.')}.rules`;
|
||||
};
|
||||
|
||||
const handleAddRule = (args: AddArgs) => {
|
||||
const { level, groupIndex } = args;
|
||||
const filtersCopy = { ...filters };
|
||||
|
||||
const getPath = (level: number) => {
|
||||
if (level === 0) return 'rules';
|
||||
|
||||
const str = [];
|
||||
for (const index of groupIndex) {
|
||||
str.push(`group[${index}]`);
|
||||
}
|
||||
|
||||
return `${str.join('.')}.rules`;
|
||||
};
|
||||
|
||||
const path = getPath(level);
|
||||
const updatedFilters = _.set(filtersCopy, path, [
|
||||
..._.get(filtersCopy, path),
|
||||
{ filter: 'newrule', uniqueId: nanoid() },
|
||||
const path = getRulePath(level, groupIndex);
|
||||
const updatedFilters = set(filtersCopy, path, [
|
||||
...get(filtersCopy, path),
|
||||
{
|
||||
field: null,
|
||||
operator: null,
|
||||
uniqueId: nanoid(),
|
||||
value: null,
|
||||
},
|
||||
]);
|
||||
|
||||
setFilters(updatedFilters);
|
||||
@@ -427,22 +702,11 @@ export const AdvancedFilters = () => {
|
||||
const { uniqueId, level, groupIndex } = args;
|
||||
const filtersCopy = { ...filters };
|
||||
|
||||
const getPath = (level: number) => {
|
||||
if (level === 0) return 'rules';
|
||||
|
||||
const str = [];
|
||||
for (const index of groupIndex) {
|
||||
str.push(`group[${index}]`);
|
||||
}
|
||||
|
||||
return `${str.join('.')}.rules`;
|
||||
};
|
||||
|
||||
const path = getPath(level);
|
||||
const updatedFilters = _.set(
|
||||
const path = getRulePath(level, groupIndex);
|
||||
const updatedFilters = set(
|
||||
filtersCopy,
|
||||
path,
|
||||
_.get(filtersCopy, path).filter(
|
||||
get(filtersCopy, path).filter(
|
||||
(rule: AdvancedFilterRule) => rule.uniqueId !== uniqueId
|
||||
)
|
||||
);
|
||||
@@ -450,6 +714,95 @@ export const AdvancedFilters = () => {
|
||||
setFilters(updatedFilters);
|
||||
};
|
||||
|
||||
const handleChangeField = (args: any) => {
|
||||
const { uniqueId, level, groupIndex, value } = args;
|
||||
const filtersCopy = { ...filters };
|
||||
|
||||
const path = getRulePath(level, groupIndex);
|
||||
const updatedFilters = set(
|
||||
filtersCopy,
|
||||
path,
|
||||
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
||||
if (rule.uniqueId !== uniqueId) return rule;
|
||||
return {
|
||||
...rule,
|
||||
field: value,
|
||||
operator: null,
|
||||
value: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setFilters(updatedFilters);
|
||||
};
|
||||
|
||||
const handleChangeType = (args: any) => {
|
||||
const { level, groupIndex, value } = args;
|
||||
|
||||
const filtersCopy = { ...filters };
|
||||
|
||||
if (level === 0) {
|
||||
return setFilters({ ...filtersCopy, type: value });
|
||||
}
|
||||
|
||||
const getTypePath = () => {
|
||||
const str = [];
|
||||
for (let i = 0; i < groupIndex.length; i += 1) {
|
||||
str.push(`group[${groupIndex[i]}]`);
|
||||
}
|
||||
|
||||
return `${str.join('.')}`;
|
||||
};
|
||||
|
||||
const path = getTypePath();
|
||||
const updatedFilters = set(filtersCopy, path, {
|
||||
...get(filtersCopy, path),
|
||||
type: value,
|
||||
});
|
||||
|
||||
return setFilters(updatedFilters);
|
||||
};
|
||||
|
||||
const handleChangeOperator = (args: any) => {
|
||||
const { uniqueId, level, groupIndex, value } = args;
|
||||
const filtersCopy = { ...filters };
|
||||
|
||||
const path = getRulePath(level, groupIndex);
|
||||
const updatedFilters = set(
|
||||
filtersCopy,
|
||||
path,
|
||||
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
||||
if (rule.uniqueId !== uniqueId) return rule;
|
||||
return {
|
||||
...rule,
|
||||
operator: value,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setFilters(updatedFilters);
|
||||
};
|
||||
|
||||
const handleChangeValue = (args: any) => {
|
||||
const { uniqueId, level, groupIndex, value } = args;
|
||||
const filtersCopy = { ...filters };
|
||||
|
||||
const path = getRulePath(level, groupIndex);
|
||||
const updatedFilters = set(
|
||||
filtersCopy,
|
||||
path,
|
||||
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
||||
if (rule.uniqueId !== uniqueId) return rule;
|
||||
return {
|
||||
...rule,
|
||||
value,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setFilters(updatedFilters);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box m={10}>
|
||||
<FilterGroup
|
||||
@@ -459,6 +812,10 @@ export const AdvancedFilters = () => {
|
||||
uniqueId={filters.uniqueId}
|
||||
onAddRule={handleAddRule}
|
||||
onAddRuleGroup={handleAddRuleGroup}
|
||||
onChangeField={handleChangeField}
|
||||
onChangeOperator={handleChangeOperator}
|
||||
onChangeType={handleChangeType}
|
||||
onChangeValue={handleChangeValue}
|
||||
onDeleteRule={handleDeleteRule}
|
||||
onDeleteRuleGroup={handleDeleteRuleGroup}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable no-plusplus */
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import { Group, Checkbox } from '@mantine/core';
|
||||
import { useSetState } from '@mantine/hooks';
|
||||
import { useDebouncedValue, useSetState } from '@mantine/hooks';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { RiArrowDownSLine } from 'react-icons/ri';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { api } from '@/renderer/api';
|
||||
@@ -17,6 +18,11 @@ import {
|
||||
VirtualGridContainer,
|
||||
VirtualInfiniteGrid,
|
||||
} from '@/renderer/components';
|
||||
import {
|
||||
AdvancedFilterGroup,
|
||||
AdvancedFilters,
|
||||
FilterGroupType,
|
||||
} from '@/renderer/features/albums/components/advanced-filters';
|
||||
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
|
||||
import { useServerList } from '@/renderer/features/servers';
|
||||
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
||||
@@ -60,12 +66,23 @@ export const AlbumListRoute = () => {
|
||||
sortBy: AlbumSort.NAME,
|
||||
});
|
||||
|
||||
const [advancedFilters, setAdvancedFilters] = useState<AdvancedFilterGroup>({
|
||||
group: [],
|
||||
rules: [{ field: null, operator: null, uniqueId: nanoid(), value: null }],
|
||||
type: FilterGroupType.AND,
|
||||
uniqueId: nanoid(),
|
||||
});
|
||||
|
||||
const [debouncedFilters] = useDebouncedValue(advancedFilters, 300);
|
||||
const encoded = encodeURI(JSON.stringify(debouncedFilters));
|
||||
|
||||
const serverFolders = useMemo(() => {
|
||||
const server = servers?.data.find((server) => server.id === serverId);
|
||||
return server?.serverFolders;
|
||||
}, [serverId, servers]);
|
||||
|
||||
const { data: albums } = useAlbumList({
|
||||
advancedFilters: encoded,
|
||||
orderBy: filters.orderBy,
|
||||
serverFolderId: filters.serverFolderId,
|
||||
skip: 0,
|
||||
@@ -76,9 +93,17 @@ export const AlbumListRoute = () => {
|
||||
const fetch = useCallback(
|
||||
async ({ skip, take }) => {
|
||||
const albums = await queryClient.fetchQuery(
|
||||
queryKeys.albums.list(serverId, { skip, take, ...filters }),
|
||||
queryKeys.albums.list(serverId, {
|
||||
skip,
|
||||
take,
|
||||
...filters,
|
||||
advancedFilters: encoded,
|
||||
}),
|
||||
async () =>
|
||||
api.albums.getAlbumList({ serverId }, { skip, take, ...filters })
|
||||
api.albums.getAlbumList(
|
||||
{ serverId },
|
||||
{ skip, take, ...filters, advancedFilters: encoded }
|
||||
)
|
||||
);
|
||||
|
||||
// * Adds server token
|
||||
@@ -95,7 +120,7 @@ export const AlbumListRoute = () => {
|
||||
|
||||
return albums;
|
||||
},
|
||||
[filters, isImageTokenRequired, queryClient, serverId, serverToken]
|
||||
[encoded, filters, isImageTokenRequired, queryClient, serverId, serverToken]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -171,12 +196,18 @@ export const AlbumListRoute = () => {
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<ViewTypeButton
|
||||
handler={setViewType}
|
||||
menuProps={{ position: 'bottom-end' }}
|
||||
type={viewType}
|
||||
/>
|
||||
<Group position="right">
|
||||
<ViewTypeButton
|
||||
handler={setViewType}
|
||||
menuProps={{ position: 'bottom-end' }}
|
||||
type={viewType}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<AdvancedFilters
|
||||
filters={advancedFilters}
|
||||
setFilters={setAdvancedFilters}
|
||||
/>
|
||||
<VirtualGridAutoSizerContainer>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
@@ -201,7 +232,7 @@ export const AlbumListRoute = () => {
|
||||
itemGap={20}
|
||||
itemSize={200}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
minimumBatchSize={100}
|
||||
minimumBatchSize={40}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user