Progress on advanced filters

This commit is contained in:
jeffvli
2022-11-03 03:25:10 -07:00
parent be05c1df79
commit 53a7d728b3
3 changed files with 269 additions and 110 deletions
@@ -1,4 +1,4 @@
import { Box, Stack, Group } from '@mantine/core';
import { Stack, Group } from '@mantine/core';
import dayjs from 'dayjs';
import get from 'lodash/get';
import set from 'lodash/set';
@@ -75,7 +75,7 @@ const FILTER_GROUP_OPTIONS_DATA = [
const FILTER_OPTIONS_DATA = [
{
label: 'Artist Name',
label: 'Artist Title',
value: 'artists.name',
},
{
@@ -87,7 +87,7 @@ const FILTER_OPTIONS_DATA = [
value: 'artists.genre',
},
{
label: 'Album Artist Name',
label: 'Album Artist Title',
value: 'albumArtists.name',
},
{
@@ -99,7 +99,7 @@ const FILTER_OPTIONS_DATA = [
value: 'albumArtists.genre',
},
{
label: 'Album Name',
label: 'Album Title',
value: 'albums.name',
},
{
@@ -112,7 +112,7 @@ const FILTER_OPTIONS_DATA = [
},
{
label: 'Album Year',
value: 'albums.year',
value: 'albums.releaseYear',
},
{
label: 'Album Release Date',
@@ -127,7 +127,7 @@ const FILTER_OPTIONS_DATA = [
value: 'albums.dateAdded',
},
{
label: 'Track Name',
label: 'Track Title',
value: 'songs.name',
},
{
@@ -171,7 +171,7 @@ const OPTIONS_MAP = {
'albums.releaseDate': {
type: 'date',
},
'albums.year': {
'albums.releaseYear': {
type: 'number',
},
'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 {
data: AdvancedFilterRule;
groupIndex: number[];
level: number;
noRemove: boolean;
onChangeField: (args: any) => void;
onChangeOperator: (args: any) => void;
onChangeValue: (args: any) => void;
@@ -209,6 +246,7 @@ const FilterOption = ({
level,
onDeleteRule,
groupIndex,
noRemove,
onChangeField,
onChangeOperator,
onChangeValue,
@@ -254,37 +292,45 @@ const FilterOption = ({
const filterOperatorMap = {
date: (
<Select
searchable
data={DATE_FILTER_OPTIONS_DATA}
maxWidth={175}
size="xs"
value={operator}
width={150}
width="20%"
onChange={handleChangeOperator}
/>
),
id: (
<Select
searchable
data={ID_FILTER_OPTIONS_DATA}
maxWidth={175}
size="xs"
value={operator}
width={150}
width="20%"
onChange={handleChangeOperator}
/>
),
number: (
<Select
searchable
data={NUMBER_FILTER_OPTIONS_DATA}
maxWidth={175}
size="xs"
value={operator}
width={150}
width="20%"
onChange={handleChangeOperator}
/>
),
string: (
<Select
searchable
data={STRING_FILTER_OPTIONS_DATA}
maxWidth={175}
size="xs"
value={operator}
width={150}
width="20%"
onChange={handleChangeOperator}
/>
),
@@ -295,27 +341,30 @@ const FilterOption = ({
<Select
searchable
data={[]}
maxWidth={175}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'albumArtists.name': (
<TextInput
maxWidth={175}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'albumArtists.ratings.value': (
<NumberInput
max={5}
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
@@ -323,10 +372,11 @@ const FilterOption = ({
<DatePicker
initialLevel="year"
maxDate={dayjs(new Date()).year(3000).toDate()}
maxWidth={175}
minDate={dayjs(new Date()).year(1950).toDate()}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
@@ -334,36 +384,40 @@ const FilterOption = ({
<Select
searchable
data={[]}
maxWidth={175}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'albums.name': (
<TextInput
maxWidth={175}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'albums.playCount': (
<NumberInput
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={(e) => handleChangeValue(e)}
/>
),
'albums.ratings.value': (
<NumberInput
max={5}
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
@@ -371,19 +425,21 @@ const FilterOption = ({
<DatePicker
initialLevel="year"
maxDate={dayjs(new Date()).year(3000).toDate()}
maxWidth={175}
minDate={dayjs(new Date()).year(1950).toDate()}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'albums.year': (
'albums.releaseYear': (
<NumberInput
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
@@ -391,60 +447,75 @@ const FilterOption = ({
<Select
searchable
data={[]}
maxWidth={175}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'artists.name': (
<TextInput
maxWidth={175}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'artists.ratings.value': (
<NumberInput
max={5}
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'songs.name': (
<TextInput size="xs" width={150} onChange={handleChangeValue} />
<TextInput
maxWidth={175}
size="xs"
width="20%"
onChange={handleChangeValue}
/>
),
'songs.playCount': (
<NumberInput
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
'songs.ratings.value': (
<NumberInput
max={5}
maxWidth={175}
min={0}
size="xs"
value={value}
width={150}
width="20%"
onChange={handleChangeValue}
/>
),
};
const ml = (level + 1) * 10 - level * 5;
return (
<Group ml={`${(level + 1) * 10}px`}>
<Group ml={ml}>
<Select
searchable
data={FILTER_OPTIONS_DATA}
maxWidth={175}
size="xs"
value={field}
width="20%"
onChange={handleChangeField}
/>
{field ? (
@@ -453,14 +524,15 @@ const FilterOption = ({
.type as keyof typeof filterOperatorMap
]
) : (
<TextInput disabled size="xs" width={150} />
<TextInput disabled maxWidth={175} size="xs" width="20%" />
)}
{field ? (
filterInputValueMap[field as keyof typeof filterInputValueMap]
) : (
<TextInput disabled size="xs" width={150} />
<TextInput disabled maxWidth={175} size="xs" width="20%" />
)}
<Button
disabled={noRemove}
px={5}
size="xs"
tooltip={{ label: 'Remove rule' }}
@@ -533,9 +605,12 @@ const FilterGroup = ({
<Stack ml={`${level * 10}px`}>
<Group>
<Select
searchable
data={FILTER_GROUP_OPTIONS_DATA}
maxWidth={175}
size="xs"
value={data.type}
width="20%"
onChange={handleChangeType}
/>
<Button
@@ -571,6 +646,7 @@ const FilterGroup = ({
data={rule}
groupIndex={groupIndex || []}
level={level}
noRemove={data.rules.length === 1}
onChangeField={onChangeField}
onChangeOperator={onChangeOperator}
onChangeValue={onChangeValue}
@@ -804,7 +880,7 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
};
return (
<Box m={10}>
<>
<FilterGroup
data={filters}
groupIndex={[]}
@@ -819,6 +895,6 @@ export const AdvancedFilters = ({ filters, setFilters }: any) => {
onDeleteRule={handleDeleteRule}
onDeleteRuleGroup={handleDeleteRuleGroup}
/>
</Box>
</>
);
};
@@ -1,10 +1,10 @@
/* eslint-disable no-plusplus */
import { useState, useCallback, useMemo } from 'react';
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 { nanoid } from 'nanoid';
import { RiArrowDownSLine } from 'react-icons/ri';
import { RiArrowDownSLine, RiArrowLeftLine } from 'react-icons/ri';
import AutoSizer from 'react-virtualized-auto-sizer';
import { api } from '@/renderer/api';
import { AlbumSort } from '@/renderer/api/albums.api';
@@ -13,6 +13,9 @@ import { SortOrder } from '@/renderer/api/types';
import {
Button,
DropdownMenu,
NumberInput,
ScrollArea,
Paper,
Text,
VirtualGridAutoSizerContainer,
VirtualGridContainer,
@@ -22,13 +25,13 @@ import {
AdvancedFilterGroup,
AdvancedFilters,
FilterGroupType,
formatAdvancedFiltersQuery,
} 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';
import { AppRoute } from '@/renderer/router/routes';
import { useAuthStore } from '@/renderer/store';
import { Font } from '@/renderer/styles';
import { LibraryItem } from '@/renderer/types';
import {
ViewType,
@@ -36,20 +39,20 @@ import {
} from '../../library/components/ViewTypeButton';
const FILTERS = [
{ name: 'Title', value: AlbumSort.NAME },
{ name: 'Date added', value: AlbumSort.DATE_ADDED },
{
name: 'Date added (remote)',
name: 'Date Added (remote)',
value: AlbumSort.DATE_ADDED_REMOTE,
},
{ name: 'Date released', 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: 'Release Date', value: AlbumSort.DATE_RELEASED },
{ 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: 'Descending', value: SortOrder.DESC },
];
@@ -66,15 +69,20 @@ export const AlbumListRoute = () => {
sortBy: AlbumSort.NAME,
});
const [advancedFilters, setAdvancedFilters] = useState<AdvancedFilterGroup>({
const [isAdvFilter, toggleAdvFilter] = useToggle();
const [rawAdvFilters, setRawAdvFilters] = 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 [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 300);
const advancedFilters = useMemo(() => {
const value = formatAdvancedFiltersQuery(debouncedAdvFilters);
return encodeURI(JSON.stringify(value));
}, [debouncedAdvFilters]);
const serverFolders = useMemo(() => {
const server = servers?.data.find((server) => server.id === serverId);
@@ -82,7 +90,7 @@ export const AlbumListRoute = () => {
}, [serverId, servers]);
const { data: albums } = useAlbumList({
advancedFilters: encoded,
advancedFilters,
orderBy: filters.orderBy,
serverFolderId: filters.serverFolderId,
skip: 0,
@@ -97,12 +105,12 @@ export const AlbumListRoute = () => {
skip,
take,
...filters,
advancedFilters: encoded,
advancedFilters,
}),
async () =>
api.albums.getAlbumList(
{ serverId },
{ skip, take, ...filters, advancedFilters: encoded }
{ skip, take, ...filters, advancedFilters }
)
);
@@ -120,7 +128,14 @@ export const AlbumListRoute = () => {
return albums;
},
[encoded, filters, isImageTokenRequired, queryClient, serverId, serverToken]
[
advancedFilters,
filters,
isImageTokenRequired,
queryClient,
serverId,
serverToken,
]
);
return (
@@ -128,7 +143,7 @@ export const AlbumListRoute = () => {
<VirtualGridContainer>
<Group m={10} position="apart">
<Group>
<Text font={Font.POPPINS} size="lg">
<Text noSelect size="lg">
Albums
</Text>
<DropdownMenu position="bottom-start">
@@ -144,26 +159,54 @@ export const AlbumListRoute = () => {
{FILTERS.map((filter) => (
<DropdownMenu.Item
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 })}
>
{filter.name}
</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>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<Group>
{SORT.find((s) => s.value === filters.orderBy)?.name}{' '}
{ORDER.find((s) => s.value === filters.orderBy)?.name}{' '}
<RiArrowDownSLine size={15} />
</Group>
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{SORT.map((sort) => (
{ORDER.map((sort) => (
<DropdownMenu.Item
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 })}
>
{sort.name}
@@ -204,10 +247,37 @@ export const AlbumListRoute = () => {
/>
</Group>
</Group>
<AdvancedFilters
filters={advancedFilters}
setFilters={setAdvancedFilters}
/>
{isAdvFilter && (
<>
<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>
<AutoSizer>
{({ height, width }) => (