mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Progress on advanced filters
This commit is contained in:
@@ -99,6 +99,21 @@ export type AdvancedFilterGroup = {
|
|||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const operatorMap = {
|
||||||
|
'!=': 'not',
|
||||||
|
'!~': 'contains',
|
||||||
|
$: 'endsWith',
|
||||||
|
'<': 'lt',
|
||||||
|
'<=': 'lte',
|
||||||
|
'=': 'equals',
|
||||||
|
'>': 'gt',
|
||||||
|
'>=': 'gte',
|
||||||
|
'^': 'startsWith',
|
||||||
|
'~': 'contains',
|
||||||
|
};
|
||||||
|
|
||||||
|
const insensitiveFields = ['name'];
|
||||||
|
|
||||||
const advancedFilterGroup = (
|
const advancedFilterGroup = (
|
||||||
groups: AdvancedFilterGroup[],
|
groups: AdvancedFilterGroup[],
|
||||||
user: AuthUser,
|
user: AuthUser,
|
||||||
@@ -119,57 +134,65 @@ 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 op = operatorMap[rule.operator as keyof typeof operatorMap];
|
||||||
|
|
||||||
|
switch (table) {
|
||||||
|
case 'albums':
|
||||||
|
if (field === 'ratings') {
|
||||||
|
query[rootType].push({
|
||||||
|
[field]: {
|
||||||
|
[condition]: {
|
||||||
|
[relationField]: {
|
||||||
|
[op]: rule.value,
|
||||||
|
},
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (field === 'ratings') {
|
|
||||||
if (table === 'albums') {
|
|
||||||
query[rootType].push({
|
query[rootType].push({
|
||||||
[field]: {
|
[field]: {
|
||||||
some: {
|
mode: insensitiveFields.includes(field)
|
||||||
[relationField]: {
|
? 'insensitive'
|
||||||
[rule.operator]: rule.value,
|
: undefined,
|
||||||
},
|
[op]: rule.value,
|
||||||
userId: user.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
break;
|
||||||
query[rootType].push({
|
|
||||||
[table]: {
|
default:
|
||||||
some: {
|
if (field === 'ratings') {
|
||||||
[field]: {
|
query[rootType].push({
|
||||||
some: {
|
[table]: {
|
||||||
[relationField]: {
|
some: {
|
||||||
[rule.operator]: rule.value,
|
[field]: {
|
||||||
|
some: {
|
||||||
|
[relationField]: {
|
||||||
|
[op]: rule.value,
|
||||||
|
},
|
||||||
|
userId: user.id,
|
||||||
},
|
},
|
||||||
userId: user.id,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
break;
|
||||||
}
|
}
|
||||||
} else if (table === 'albums') {
|
|
||||||
const obj = {
|
|
||||||
[field]: {
|
|
||||||
[rule.operator]: rule.value,
|
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
query[rootType].push(obj);
|
query[rootType].push({
|
||||||
} else {
|
[table]: {
|
||||||
const obj = {
|
[condition]: {
|
||||||
[table]: {
|
[field]: {
|
||||||
some: {
|
mode: 'insensitive',
|
||||||
[field]: {
|
[op]: rule.value,
|
||||||
[rule.operator]: rule.value,
|
},
|
||||||
mode: 'insensitive',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
};
|
break;
|
||||||
|
|
||||||
query[rootType].push(obj);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,19 +215,6 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
|
|||||||
[rootQueryType]: [] as any[],
|
[rootQueryType]: [] as any[],
|
||||||
};
|
};
|
||||||
|
|
||||||
const operatorMap = {
|
|
||||||
'!=': 'not',
|
|
||||||
'!~': 'contains',
|
|
||||||
$: 'endsWith',
|
|
||||||
'<': 'lt',
|
|
||||||
'<=': 'lte',
|
|
||||||
'=': 'equals',
|
|
||||||
'>': 'gt',
|
|
||||||
'>=': 'gte',
|
|
||||||
'^': 'startsWith',
|
|
||||||
'~': 'contains',
|
|
||||||
};
|
|
||||||
|
|
||||||
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('.');
|
||||||
@@ -226,9 +236,12 @@ const advancedFilter = (filter: AdvancedFilterGroup, user: AuthUser) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
rootQuery[rootQueryType].push({
|
rootQuery[rootQueryType].push({
|
||||||
[field]: {
|
[field]: {
|
||||||
mode: 'insensitive',
|
mode: insensitiveFields.includes(field)
|
||||||
|
? 'insensitive'
|
||||||
|
: undefined,
|
||||||
[op]: rule.value,
|
[op]: rule.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Box, Stack, Group } from '@mantine/core';
|
import { Stack, Group } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
@@ -75,7 +75,7 @@ const FILTER_GROUP_OPTIONS_DATA = [
|
|||||||
|
|
||||||
const FILTER_OPTIONS_DATA = [
|
const FILTER_OPTIONS_DATA = [
|
||||||
{
|
{
|
||||||
label: 'Artist Name',
|
label: 'Artist Title',
|
||||||
value: 'artists.name',
|
value: 'artists.name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,7 +87,7 @@ const FILTER_OPTIONS_DATA = [
|
|||||||
value: 'artists.genre',
|
value: 'artists.genre',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Album Artist Name',
|
label: 'Album Artist Title',
|
||||||
value: 'albumArtists.name',
|
value: 'albumArtists.name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -99,7 +99,7 @@ const FILTER_OPTIONS_DATA = [
|
|||||||
value: 'albumArtists.genre',
|
value: 'albumArtists.genre',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Album Name',
|
label: 'Album Title',
|
||||||
value: 'albums.name',
|
value: 'albums.name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -112,7 +112,7 @@ const FILTER_OPTIONS_DATA = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Album Year',
|
label: 'Album Year',
|
||||||
value: 'albums.year',
|
value: 'albums.releaseYear',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Album Release Date',
|
label: 'Album Release Date',
|
||||||
@@ -127,7 +127,7 @@ const FILTER_OPTIONS_DATA = [
|
|||||||
value: 'albums.dateAdded',
|
value: 'albums.dateAdded',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Track Name',
|
label: 'Track Title',
|
||||||
value: 'songs.name',
|
value: 'songs.name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -171,7 +171,7 @@ const OPTIONS_MAP = {
|
|||||||
'albums.releaseDate': {
|
'albums.releaseDate': {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
'albums.year': {
|
'albums.releaseYear': {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
'artists.genre': {
|
'artists.genre': {
|
||||||
@@ -194,10 +194,47 @@ const OPTIONS_MAP = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const formatAdvancedFiltersGroups = (groups: AdvancedFilterGroup[]) => {
|
||||||
|
const filterGroups: any[] = [];
|
||||||
|
|
||||||
|
for (const group of groups) {
|
||||||
|
const rules = group.rules
|
||||||
|
.filter((rule) => rule.field && rule.operator && rule.value)
|
||||||
|
.map((rule) => ({ ...rule, uniqueId: undefined }));
|
||||||
|
|
||||||
|
const updatedGroup = { ...group, rules, uniqueId: undefined };
|
||||||
|
|
||||||
|
if (group.group.length > 0) {
|
||||||
|
const nestedRuleGroup = formatAdvancedFiltersGroups(group.group);
|
||||||
|
nestedRuleGroup.forEach((group) => groups.push(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedGroup.rules.length > 0) {
|
||||||
|
filterGroups.push(updatedGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterGroups;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent query key from constantly changing due to empty rules or groups
|
||||||
|
export const formatAdvancedFiltersQuery = (filter: AdvancedFilterGroup) => {
|
||||||
|
const updatedFilter = {
|
||||||
|
...filter,
|
||||||
|
group: formatAdvancedFiltersGroups(filter.group),
|
||||||
|
rules: filter.rules
|
||||||
|
.filter((rule) => rule.field && rule.operator && rule.value)
|
||||||
|
.map((rule) => ({ ...rule, uniqueId: undefined })),
|
||||||
|
};
|
||||||
|
|
||||||
|
return updatedFilter;
|
||||||
|
};
|
||||||
|
|
||||||
interface FilterOptionProps {
|
interface FilterOptionProps {
|
||||||
data: AdvancedFilterRule;
|
data: AdvancedFilterRule;
|
||||||
groupIndex: number[];
|
groupIndex: number[];
|
||||||
level: number;
|
level: number;
|
||||||
|
noRemove: boolean;
|
||||||
onChangeField: (args: any) => void;
|
onChangeField: (args: any) => void;
|
||||||
onChangeOperator: (args: any) => void;
|
onChangeOperator: (args: any) => void;
|
||||||
onChangeValue: (args: any) => void;
|
onChangeValue: (args: any) => void;
|
||||||
@@ -209,6 +246,7 @@ const FilterOption = ({
|
|||||||
level,
|
level,
|
||||||
onDeleteRule,
|
onDeleteRule,
|
||||||
groupIndex,
|
groupIndex,
|
||||||
|
noRemove,
|
||||||
onChangeField,
|
onChangeField,
|
||||||
onChangeOperator,
|
onChangeOperator,
|
||||||
onChangeValue,
|
onChangeValue,
|
||||||
@@ -254,37 +292,45 @@ const FilterOption = ({
|
|||||||
const filterOperatorMap = {
|
const filterOperatorMap = {
|
||||||
date: (
|
date: (
|
||||||
<Select
|
<Select
|
||||||
|
searchable
|
||||||
data={DATE_FILTER_OPTIONS_DATA}
|
data={DATE_FILTER_OPTIONS_DATA}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={operator}
|
value={operator}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
id: (
|
id: (
|
||||||
<Select
|
<Select
|
||||||
|
searchable
|
||||||
data={ID_FILTER_OPTIONS_DATA}
|
data={ID_FILTER_OPTIONS_DATA}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={operator}
|
value={operator}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
number: (
|
number: (
|
||||||
<Select
|
<Select
|
||||||
|
searchable
|
||||||
data={NUMBER_FILTER_OPTIONS_DATA}
|
data={NUMBER_FILTER_OPTIONS_DATA}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={operator}
|
value={operator}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
string: (
|
string: (
|
||||||
<Select
|
<Select
|
||||||
|
searchable
|
||||||
data={STRING_FILTER_OPTIONS_DATA}
|
data={STRING_FILTER_OPTIONS_DATA}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={operator}
|
value={operator}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeOperator}
|
onChange={handleChangeOperator}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -295,27 +341,30 @@ const FilterOption = ({
|
|||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={[]}
|
data={[]}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'albumArtists.name': (
|
'albumArtists.name': (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'albumArtists.ratings.value': (
|
'albumArtists.ratings.value': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
max={5}
|
max={5}
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -323,10 +372,11 @@ const FilterOption = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
initialLevel="year"
|
initialLevel="year"
|
||||||
maxDate={dayjs(new Date()).year(3000).toDate()}
|
maxDate={dayjs(new Date()).year(3000).toDate()}
|
||||||
|
maxWidth={175}
|
||||||
minDate={dayjs(new Date()).year(1950).toDate()}
|
minDate={dayjs(new Date()).year(1950).toDate()}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -334,36 +384,40 @@ const FilterOption = ({
|
|||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={[]}
|
data={[]}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'albums.name': (
|
'albums.name': (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'albums.playCount': (
|
'albums.playCount': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={(e) => handleChangeValue(e)}
|
onChange={(e) => handleChangeValue(e)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'albums.ratings.value': (
|
'albums.ratings.value': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
max={5}
|
max={5}
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -371,19 +425,21 @@ const FilterOption = ({
|
|||||||
<DatePicker
|
<DatePicker
|
||||||
initialLevel="year"
|
initialLevel="year"
|
||||||
maxDate={dayjs(new Date()).year(3000).toDate()}
|
maxDate={dayjs(new Date()).year(3000).toDate()}
|
||||||
|
maxWidth={175}
|
||||||
minDate={dayjs(new Date()).year(1950).toDate()}
|
minDate={dayjs(new Date()).year(1950).toDate()}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'albums.year': (
|
'albums.releaseYear': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -391,60 +447,75 @@ const FilterOption = ({
|
|||||||
<Select
|
<Select
|
||||||
searchable
|
searchable
|
||||||
data={[]}
|
data={[]}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'artists.name': (
|
'artists.name': (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'artists.ratings.value': (
|
'artists.ratings.value': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
max={5}
|
max={5}
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'songs.name': (
|
'songs.name': (
|
||||||
<TextInput size="xs" width={150} onChange={handleChangeValue} />
|
<TextInput
|
||||||
|
maxWidth={175}
|
||||||
|
size="xs"
|
||||||
|
width="20%"
|
||||||
|
onChange={handleChangeValue}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
'songs.playCount': (
|
'songs.playCount': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
'songs.ratings.value': (
|
'songs.ratings.value': (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
max={5}
|
max={5}
|
||||||
|
maxWidth={175}
|
||||||
min={0}
|
min={0}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={value}
|
value={value}
|
||||||
width={150}
|
width="20%"
|
||||||
onChange={handleChangeValue}
|
onChange={handleChangeValue}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ml = (level + 1) * 10 - level * 5;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group ml={`${(level + 1) * 10}px`}>
|
<Group ml={ml}>
|
||||||
<Select
|
<Select
|
||||||
|
searchable
|
||||||
data={FILTER_OPTIONS_DATA}
|
data={FILTER_OPTIONS_DATA}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={field}
|
value={field}
|
||||||
|
width="20%"
|
||||||
onChange={handleChangeField}
|
onChange={handleChangeField}
|
||||||
/>
|
/>
|
||||||
{field ? (
|
{field ? (
|
||||||
@@ -453,14 +524,15 @@ const FilterOption = ({
|
|||||||
.type as keyof typeof filterOperatorMap
|
.type as keyof typeof filterOperatorMap
|
||||||
]
|
]
|
||||||
) : (
|
) : (
|
||||||
<TextInput disabled size="xs" width={150} />
|
<TextInput disabled maxWidth={175} size="xs" width="20%" />
|
||||||
)}
|
)}
|
||||||
{field ? (
|
{field ? (
|
||||||
filterInputValueMap[field as keyof typeof filterInputValueMap]
|
filterInputValueMap[field as keyof typeof filterInputValueMap]
|
||||||
) : (
|
) : (
|
||||||
<TextInput disabled size="xs" width={150} />
|
<TextInput disabled maxWidth={175} size="xs" width="20%" />
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
disabled={noRemove}
|
||||||
px={5}
|
px={5}
|
||||||
size="xs"
|
size="xs"
|
||||||
tooltip={{ label: 'Remove rule' }}
|
tooltip={{ label: 'Remove rule' }}
|
||||||
@@ -533,9 +605,12 @@ const FilterGroup = ({
|
|||||||
<Stack ml={`${level * 10}px`}>
|
<Stack ml={`${level * 10}px`}>
|
||||||
<Group>
|
<Group>
|
||||||
<Select
|
<Select
|
||||||
|
searchable
|
||||||
data={FILTER_GROUP_OPTIONS_DATA}
|
data={FILTER_GROUP_OPTIONS_DATA}
|
||||||
|
maxWidth={175}
|
||||||
size="xs"
|
size="xs"
|
||||||
value={data.type}
|
value={data.type}
|
||||||
|
width="20%"
|
||||||
onChange={handleChangeType}
|
onChange={handleChangeType}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
@@ -571,6 +646,7 @@ const FilterGroup = ({
|
|||||||
data={rule}
|
data={rule}
|
||||||
groupIndex={groupIndex || []}
|
groupIndex={groupIndex || []}
|
||||||
level={level}
|
level={level}
|
||||||
|
noRemove={data.rules.length === 1}
|
||||||
onChangeField={onChangeField}
|
onChangeField={onChangeField}
|
||||||
onChangeOperator={onChangeOperator}
|
onChangeOperator={onChangeOperator}
|
||||||
onChangeValue={onChangeValue}
|
onChangeValue={onChangeValue}
|
||||||
@@ -804,7 +880,7 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m={10}>
|
<>
|
||||||
<FilterGroup
|
<FilterGroup
|
||||||
data={filters}
|
data={filters}
|
||||||
groupIndex={[]}
|
groupIndex={[]}
|
||||||
@@ -819,6 +895,6 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
|
|||||||
onDeleteRule={handleDeleteRule}
|
onDeleteRule={handleDeleteRule}
|
||||||
onDeleteRuleGroup={handleDeleteRuleGroup}
|
onDeleteRuleGroup={handleDeleteRuleGroup}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
/* eslint-disable no-plusplus */
|
/* eslint-disable no-plusplus */
|
||||||
import { useState, useCallback, useMemo } from 'react';
|
import { useState, useCallback, useMemo } from 'react';
|
||||||
import { Group, Checkbox } from '@mantine/core';
|
import { Group, Checkbox } from '@mantine/core';
|
||||||
import { useDebouncedValue, useSetState } from '@mantine/hooks';
|
import { useDebouncedValue, useSetState, useToggle } from '@mantine/hooks';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { RiArrowDownSLine } from 'react-icons/ri';
|
import { RiArrowDownSLine, RiArrowLeftLine } from 'react-icons/ri';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { api } from '@/renderer/api';
|
import { api } from '@/renderer/api';
|
||||||
import { AlbumSort } from '@/renderer/api/albums.api';
|
import { AlbumSort } from '@/renderer/api/albums.api';
|
||||||
@@ -13,6 +13,9 @@ import { SortOrder } from '@/renderer/api/types';
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
NumberInput,
|
||||||
|
ScrollArea,
|
||||||
|
Paper,
|
||||||
Text,
|
Text,
|
||||||
VirtualGridAutoSizerContainer,
|
VirtualGridAutoSizerContainer,
|
||||||
VirtualGridContainer,
|
VirtualGridContainer,
|
||||||
@@ -22,13 +25,13 @@ import {
|
|||||||
AdvancedFilterGroup,
|
AdvancedFilterGroup,
|
||||||
AdvancedFilters,
|
AdvancedFilters,
|
||||||
FilterGroupType,
|
FilterGroupType,
|
||||||
|
formatAdvancedFiltersQuery,
|
||||||
} from '@/renderer/features/albums/components/advanced-filters';
|
} from '@/renderer/features/albums/components/advanced-filters';
|
||||||
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
|
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
|
||||||
import { useServerList } from '@/renderer/features/servers';
|
import { useServerList } from '@/renderer/features/servers';
|
||||||
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
||||||
import { AppRoute } from '@/renderer/router/routes';
|
import { AppRoute } from '@/renderer/router/routes';
|
||||||
import { useAuthStore } from '@/renderer/store';
|
import { useAuthStore } from '@/renderer/store';
|
||||||
import { Font } from '@/renderer/styles';
|
|
||||||
import { LibraryItem } from '@/renderer/types';
|
import { LibraryItem } from '@/renderer/types';
|
||||||
import {
|
import {
|
||||||
ViewType,
|
ViewType,
|
||||||
@@ -36,20 +39,20 @@ import {
|
|||||||
} from '../../library/components/ViewTypeButton';
|
} from '../../library/components/ViewTypeButton';
|
||||||
|
|
||||||
const FILTERS = [
|
const FILTERS = [
|
||||||
|
{ name: 'Title', value: AlbumSort.NAME },
|
||||||
{ name: 'Date added', value: AlbumSort.DATE_ADDED },
|
{ name: 'Date added', value: AlbumSort.DATE_ADDED },
|
||||||
{
|
{
|
||||||
name: 'Date added (remote)',
|
name: 'Date Added (remote)',
|
||||||
value: AlbumSort.DATE_ADDED_REMOTE,
|
value: AlbumSort.DATE_ADDED_REMOTE,
|
||||||
},
|
},
|
||||||
{ name: 'Date released', value: AlbumSort.DATE_RELEASED },
|
{ name: 'Release Date', value: AlbumSort.DATE_RELEASED },
|
||||||
{ name: 'Favorites', value: AlbumSort.FAVORITE },
|
|
||||||
{ name: 'Random', value: AlbumSort.RANDOM },
|
|
||||||
{ name: 'Rating', value: AlbumSort.RATING },
|
|
||||||
{ name: 'Title', value: AlbumSort.NAME },
|
|
||||||
{ name: 'Year', value: AlbumSort.DATE_RELEASED_YEAR },
|
{ name: 'Year', value: AlbumSort.DATE_RELEASED_YEAR },
|
||||||
|
{ name: 'Random', value: AlbumSort.RANDOM },
|
||||||
|
{ name: 'Favorites', value: AlbumSort.FAVORITE },
|
||||||
|
{ name: 'Rating', value: AlbumSort.RATING },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SORT = [
|
const ORDER = [
|
||||||
{ name: 'Ascending', value: SortOrder.ASC },
|
{ name: 'Ascending', value: SortOrder.ASC },
|
||||||
{ name: 'Descending', value: SortOrder.DESC },
|
{ name: 'Descending', value: SortOrder.DESC },
|
||||||
];
|
];
|
||||||
@@ -66,15 +69,20 @@ export const AlbumListRoute = () => {
|
|||||||
sortBy: AlbumSort.NAME,
|
sortBy: AlbumSort.NAME,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [advancedFilters, setAdvancedFilters] = useState<AdvancedFilterGroup>({
|
const [isAdvFilter, toggleAdvFilter] = useToggle();
|
||||||
|
const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>({
|
||||||
group: [],
|
group: [],
|
||||||
rules: [{ field: null, operator: null, uniqueId: nanoid(), value: null }],
|
rules: [{ field: null, operator: null, uniqueId: nanoid(), value: null }],
|
||||||
type: FilterGroupType.AND,
|
type: FilterGroupType.AND,
|
||||||
uniqueId: nanoid(),
|
uniqueId: nanoid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [debouncedFilters] = useDebouncedValue(advancedFilters, 300);
|
const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 300);
|
||||||
const encoded = encodeURI(JSON.stringify(debouncedFilters));
|
|
||||||
|
const advancedFilters = useMemo(() => {
|
||||||
|
const value = formatAdvancedFiltersQuery(debouncedAdvFilters);
|
||||||
|
return encodeURI(JSON.stringify(value));
|
||||||
|
}, [debouncedAdvFilters]);
|
||||||
|
|
||||||
const serverFolders = useMemo(() => {
|
const serverFolders = useMemo(() => {
|
||||||
const server = servers?.data.find((server) => server.id === serverId);
|
const server = servers?.data.find((server) => server.id === serverId);
|
||||||
@@ -82,7 +90,7 @@ export const AlbumListRoute = () => {
|
|||||||
}, [serverId, servers]);
|
}, [serverId, servers]);
|
||||||
|
|
||||||
const { data: albums } = useAlbumList({
|
const { data: albums } = useAlbumList({
|
||||||
advancedFilters: encoded,
|
advancedFilters,
|
||||||
orderBy: filters.orderBy,
|
orderBy: filters.orderBy,
|
||||||
serverFolderId: filters.serverFolderId,
|
serverFolderId: filters.serverFolderId,
|
||||||
skip: 0,
|
skip: 0,
|
||||||
@@ -97,12 +105,12 @@ export const AlbumListRoute = () => {
|
|||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
...filters,
|
...filters,
|
||||||
advancedFilters: encoded,
|
advancedFilters,
|
||||||
}),
|
}),
|
||||||
async () =>
|
async () =>
|
||||||
api.albums.getAlbumList(
|
api.albums.getAlbumList(
|
||||||
{ serverId },
|
{ serverId },
|
||||||
{ skip, take, ...filters, advancedFilters: encoded }
|
{ skip, take, ...filters, advancedFilters }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -120,7 +128,14 @@ export const AlbumListRoute = () => {
|
|||||||
|
|
||||||
return albums;
|
return albums;
|
||||||
},
|
},
|
||||||
[encoded, filters, isImageTokenRequired, queryClient, serverId, serverToken]
|
[
|
||||||
|
advancedFilters,
|
||||||
|
filters,
|
||||||
|
isImageTokenRequired,
|
||||||
|
queryClient,
|
||||||
|
serverId,
|
||||||
|
serverToken,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -128,7 +143,7 @@ export const AlbumListRoute = () => {
|
|||||||
<VirtualGridContainer>
|
<VirtualGridContainer>
|
||||||
<Group m={10} position="apart">
|
<Group m={10} position="apart">
|
||||||
<Group>
|
<Group>
|
||||||
<Text font={Font.POPPINS} size="lg">
|
<Text noSelect size="lg">
|
||||||
Albums
|
Albums
|
||||||
</Text>
|
</Text>
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom-start">
|
||||||
@@ -144,26 +159,54 @@ export const AlbumListRoute = () => {
|
|||||||
{FILTERS.map((filter) => (
|
{FILTERS.map((filter) => (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key={`filter-${filter.value}`}
|
key={`filter-${filter.value}`}
|
||||||
|
color={
|
||||||
|
filter.value === filters.sortBy
|
||||||
|
? 'var(--primary-color)'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
rightSection={
|
||||||
|
filter.value === filters.sortBy ? (
|
||||||
|
<RiArrowLeftLine />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
onClick={() => setFilters({ sortBy: filter.value })}
|
onClick={() => setFilters({ sortBy: filter.value })}
|
||||||
>
|
>
|
||||||
{filter.name}
|
{filter.name}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
))}
|
))}
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Item
|
||||||
|
color={isAdvFilter ? 'var(--primary-color)' : undefined}
|
||||||
|
rightSection={isAdvFilter ? <RiArrowLeftLine /> : undefined}
|
||||||
|
onClick={() => toggleAdvFilter()}
|
||||||
|
>
|
||||||
|
Advanced Filters
|
||||||
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom-start">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button compact variant="subtle">
|
<Button compact variant="subtle">
|
||||||
<Group>
|
<Group>
|
||||||
{SORT.find((s) => s.value === filters.orderBy)?.name}{' '}
|
{ORDER.find((s) => s.value === filters.orderBy)?.name}{' '}
|
||||||
<RiArrowDownSLine size={15} />
|
<RiArrowDownSLine size={15} />
|
||||||
</Group>
|
</Group>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
{SORT.map((sort) => (
|
{ORDER.map((sort) => (
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
key={`sort-${sort.value}`}
|
key={`sort-${sort.value}`}
|
||||||
|
color={
|
||||||
|
sort.value === filters.orderBy
|
||||||
|
? 'var(--primary-color)'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
rightSection={
|
||||||
|
sort.value === filters.orderBy ? (
|
||||||
|
<RiArrowLeftLine />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
onClick={() => setFilters({ orderBy: sort.value })}
|
onClick={() => setFilters({ orderBy: sort.value })}
|
||||||
>
|
>
|
||||||
{sort.name}
|
{sort.name}
|
||||||
@@ -204,10 +247,37 @@ export const AlbumListRoute = () => {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<AdvancedFilters
|
{isAdvFilter && (
|
||||||
filters={advancedFilters}
|
<>
|
||||||
setFilters={setAdvancedFilters}
|
<Paper sx={{ maxHeight: '20vh' }}>
|
||||||
/>
|
<ScrollArea
|
||||||
|
my={10}
|
||||||
|
px={10}
|
||||||
|
sx={{ height: '100%', width: '100%' }}
|
||||||
|
>
|
||||||
|
<Group noWrap my={10} position="apart">
|
||||||
|
<Group>
|
||||||
|
<Text>Advanced Filters</Text>
|
||||||
|
<NumberInput
|
||||||
|
disabled
|
||||||
|
min={1}
|
||||||
|
placeholder="Limit"
|
||||||
|
size="xs"
|
||||||
|
width={75}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Button disabled uppercase>
|
||||||
|
Save as...
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<AdvancedFilters
|
||||||
|
filters={rawAdvFilters}
|
||||||
|
setFilters={setRawAdvFilters}
|
||||||
|
/>
|
||||||
|
</ScrollArea>
|
||||||
|
</Paper>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<VirtualGridAutoSizerContainer>
|
<VirtualGridAutoSizerContainer>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
|
|||||||
Reference in New Issue
Block a user