Add server-specific album filters
This commit is contained in:
parent
223cf469f4
commit
57c34637cf
7 changed files with 323 additions and 39 deletions
|
@ -247,7 +247,16 @@ const getAlbumDetail = async (args: AlbumDetailArgs): Promise<JFAlbumDetail> =>
|
||||||
const getAlbumList = async (args: AlbumListArgs): Promise<JFAlbumList> => {
|
const getAlbumList = async (args: AlbumListArgs): Promise<JFAlbumList> => {
|
||||||
const { query, server, signal } = args;
|
const { query, server, signal } = args;
|
||||||
|
|
||||||
const searchParams: JFAlbumListParams = {
|
const yearsGroup = [];
|
||||||
|
if (query.jfParams?.minYear && query.jfParams?.maxYear) {
|
||||||
|
for (let i = Number(query.jfParams.minYear); i <= Number(query.jfParams.maxYear); i += 1) {
|
||||||
|
yearsGroup.push(String(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearsFilter = yearsGroup.length ? yearsGroup.join(',') : undefined;
|
||||||
|
|
||||||
|
const searchParams: JFAlbumListParams & { maxYear?: number; minYear?: number } = {
|
||||||
includeItemTypes: 'MusicAlbum',
|
includeItemTypes: 'MusicAlbum',
|
||||||
limit: query.limit,
|
limit: query.limit,
|
||||||
parentId: query.musicFolderId,
|
parentId: query.musicFolderId,
|
||||||
|
@ -257,6 +266,9 @@ const getAlbumList = async (args: AlbumListArgs): Promise<JFAlbumList> => {
|
||||||
sortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
sortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
...query.jfParams,
|
...query.jfParams,
|
||||||
|
maxYear: undefined,
|
||||||
|
minYear: undefined,
|
||||||
|
years: yearsFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await api
|
const data = await api
|
||||||
|
|
|
@ -499,11 +499,16 @@ export enum JFAlbumListSort {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type JFAlbumListParams = {
|
export type JFAlbumListParams = {
|
||||||
|
albumArtistIds?: string;
|
||||||
|
artistIds?: string;
|
||||||
filters?: string;
|
filters?: string;
|
||||||
|
genreIds?: string;
|
||||||
genres?: string;
|
genres?: string;
|
||||||
includeItemTypes: 'MusicAlbum';
|
includeItemTypes: 'MusicAlbum';
|
||||||
|
isFavorite?: boolean;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
sortBy?: JFAlbumListSort;
|
sortBy?: JFAlbumListSort;
|
||||||
|
tags?: string;
|
||||||
years?: string;
|
years?: string;
|
||||||
} & JFBaseParams &
|
} & JFBaseParams &
|
||||||
JFPaginationParams;
|
JFPaginationParams;
|
||||||
|
|
|
@ -215,7 +215,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise<NDAlbumList> => {
|
||||||
const res = await api.get('api/album', {
|
const res = await api.get('api/album', {
|
||||||
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
headers: { 'x-nd-authorization': `Bearer ${server?.ndCredential}` },
|
||||||
prefixUrl: server?.url,
|
prefixUrl: server?.url,
|
||||||
searchParams,
|
searchParams: parseSearchParams(searchParams),
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -287,9 +287,15 @@ export enum AlbumListSort {
|
||||||
|
|
||||||
export type AlbumListQuery = {
|
export type AlbumListQuery = {
|
||||||
jfParams?: {
|
jfParams?: {
|
||||||
|
albumArtistIds?: string;
|
||||||
|
artistIds?: string;
|
||||||
filters?: string;
|
filters?: string;
|
||||||
|
genreIds?: string;
|
||||||
genres?: string;
|
genres?: string;
|
||||||
years?: string;
|
isFavorite?: boolean;
|
||||||
|
maxYear?: number; // Parses to years
|
||||||
|
minYear?: number; // Parses to years
|
||||||
|
tags?: string;
|
||||||
};
|
};
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string;
|
musicFolderId?: string;
|
||||||
|
@ -299,6 +305,7 @@ export type AlbumListQuery = {
|
||||||
genre_id?: string;
|
genre_id?: string;
|
||||||
has_rating?: boolean;
|
has_rating?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
recently_played?: boolean;
|
||||||
starred?: boolean;
|
starred?: boolean;
|
||||||
year?: number;
|
year?: number;
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,9 +3,22 @@ import { useCallback } from 'react';
|
||||||
import { Flex, Slider } from '@mantine/core';
|
import { Flex, Slider } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import { RiArrowDownSLine } from 'react-icons/ri';
|
import {
|
||||||
import { AlbumListSort, SortOrder } from '/@/renderer/api/types';
|
RiArrowDownSLine,
|
||||||
import { Button, DropdownMenu, PageHeader, SearchInput } from '/@/renderer/components';
|
RiFilter3Line,
|
||||||
|
RiFolder2Line,
|
||||||
|
RiSortAsc,
|
||||||
|
RiSortDesc,
|
||||||
|
} from 'react-icons/ri';
|
||||||
|
import { AlbumListSort, ServerType, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
PageHeader,
|
||||||
|
Popover,
|
||||||
|
SearchInput,
|
||||||
|
TextTitle,
|
||||||
|
} from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useAlbumListStore,
|
useAlbumListStore,
|
||||||
|
@ -15,6 +28,8 @@ import {
|
||||||
import { CardDisplayType } from '/@/renderer/types';
|
import { CardDisplayType } from '/@/renderer/types';
|
||||||
import { useMusicFolders } from '/@/renderer/features/shared';
|
import { useMusicFolders } from '/@/renderer/features/shared';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||||
|
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
|
@ -35,6 +50,7 @@ const FILTERS = {
|
||||||
{ name: 'Random', value: AlbumListSort.RANDOM },
|
{ name: 'Random', value: AlbumListSort.RANDOM },
|
||||||
{ name: 'Rating', value: AlbumListSort.RATING },
|
{ name: 'Rating', value: AlbumListSort.RATING },
|
||||||
{ name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED },
|
{ name: 'Recently Added', value: AlbumListSort.RECENTLY_ADDED },
|
||||||
|
{ name: 'Recently Played', value: AlbumListSort.RECENTLY_PLAYED },
|
||||||
{ name: 'Song Count', value: AlbumListSort.SONG_COUNT },
|
{ name: 'Song Count', value: AlbumListSort.SONG_COUNT },
|
||||||
{ name: 'Favorited', value: AlbumListSort.FAVORITED },
|
{ name: 'Favorited', value: AlbumListSort.FAVORITED },
|
||||||
{ name: 'Year', value: AlbumListSort.YEAR },
|
{ name: 'Year', value: AlbumListSort.YEAR },
|
||||||
|
@ -68,8 +84,6 @@ export const AlbumListHeader = () => {
|
||||||
)?.name) ||
|
)?.name) ||
|
||||||
'Unknown';
|
'Unknown';
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((s) => s.value === filters.sortOrder)?.name;
|
|
||||||
|
|
||||||
const setSize = throttle(
|
const setSize = throttle(
|
||||||
(e: number) =>
|
(e: number) =>
|
||||||
setPage({
|
setPage({
|
||||||
|
@ -101,13 +115,9 @@ export const AlbumListHeader = () => {
|
||||||
const handleSetOrder = useCallback(
|
const handleSetOrder = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
debounce(
|
setFilter({
|
||||||
() =>
|
sortOrder: e.currentTarget.value as SortOrder,
|
||||||
setFilter({
|
});
|
||||||
sortOrder: e.currentTarget.value as SortOrder,
|
|
||||||
}),
|
|
||||||
1000,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[setFilter],
|
[setFilter],
|
||||||
);
|
);
|
||||||
|
@ -153,19 +163,25 @@ export const AlbumListHeader = () => {
|
||||||
<HeaderItems>
|
<HeaderItems>
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
gap="sm"
|
gap="md"
|
||||||
justify="center"
|
justify="center"
|
||||||
>
|
>
|
||||||
<DropdownMenu position="bottom-end">
|
<DropdownMenu position="bottom">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
pl={0}
|
||||||
|
pr="0.5rem"
|
||||||
rightIcon={<RiArrowDownSLine size={15} />}
|
rightIcon={<RiArrowDownSLine size={15} />}
|
||||||
size="xl"
|
size="xl"
|
||||||
sx={{ paddingLeft: 0, paddingRight: 0 }}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
Albums
|
<TextTitle
|
||||||
|
fw="bold"
|
||||||
|
order={2}
|
||||||
|
>
|
||||||
|
Albums
|
||||||
|
</TextTitle>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
|
@ -201,7 +217,7 @@ export const AlbumListHeader = () => {
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
@ -224,14 +240,18 @@ export const AlbumListHeader = () => {
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
fw="normal"
|
fw="normal"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{sortOrderLabel}
|
{filters.sortOrder === SortOrder.ASC ? (
|
||||||
|
<RiSortAsc size={15} />
|
||||||
|
) : (
|
||||||
|
<RiSortDesc size={15} />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
|
@ -247,29 +267,49 @@ export const AlbumListHeader = () => {
|
||||||
))}
|
))}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu position="bottom-start">
|
{server?.type === ServerType.JELLYFIN && (
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu position="bottom">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
fw="normal"
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<RiFolder2Line size={15} />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
{musicFoldersQuery.data?.map((folder) => (
|
||||||
|
<DropdownMenu.Item
|
||||||
|
key={`musicFolder-${folder.id}`}
|
||||||
|
$isActive={filters.musicFolderId === folder.id}
|
||||||
|
value={folder.id}
|
||||||
|
onClick={handleSetMusicFolder}
|
||||||
|
>
|
||||||
|
{folder.name}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
))}
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
<Popover>
|
||||||
|
<Popover.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
fw="normal"
|
fw="normal"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
Folder
|
<RiFilter3Line size={15} />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</Popover.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<Popover.Dropdown>
|
||||||
{musicFoldersQuery.data?.map((folder) => (
|
{server?.type === ServerType.NAVIDROME ? (
|
||||||
<DropdownMenu.Item
|
<NavidromeAlbumFilters />
|
||||||
key={`musicFolder-${folder.id}`}
|
) : (
|
||||||
$isActive={filters.musicFolderId === folder.id}
|
<JellyfinAlbumFilters />
|
||||||
value={folder.id}
|
)}
|
||||||
onClick={handleSetMusicFolder}
|
</Popover.Dropdown>
|
||||||
>
|
</Popover>
|
||||||
{folder.name}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex>
|
<Flex>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { ChangeEvent, useMemo } from 'react';
|
||||||
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
|
import { MultiSelect, NumberInput, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
|
||||||
|
export const JellyfinAlbumFilters = () => {
|
||||||
|
const { filter } = useAlbumListStore();
|
||||||
|
const setFilters = useSetAlbumFilters();
|
||||||
|
|
||||||
|
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||||
|
const genreListQuery = useGenreList(null);
|
||||||
|
|
||||||
|
const genreList = useMemo(() => {
|
||||||
|
if (!genreListQuery?.data) return [];
|
||||||
|
return genreListQuery.data.map((genre) => ({
|
||||||
|
label: genre.name,
|
||||||
|
value: genre.id,
|
||||||
|
}));
|
||||||
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
|
const selectedGenres = useMemo(() => {
|
||||||
|
return filter.jfParams?.genreIds?.split(',');
|
||||||
|
}, [filter.jfParams?.genreIds]);
|
||||||
|
|
||||||
|
const toggleFilters = [
|
||||||
|
{
|
||||||
|
label: 'Is favorited',
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilters({
|
||||||
|
jfParams: { ...filter.jfParams, isFavorite: e.currentTarget.checked ? true : undefined },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: filter.jfParams?.isFavorite,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleMinYearFilter = debounce((e: number | undefined) => {
|
||||||
|
if (e && (e < 1700 || e > 2300)) return;
|
||||||
|
setFilters({
|
||||||
|
jfParams: {
|
||||||
|
...filter.jfParams,
|
||||||
|
minYear: e,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const handleMaxYearFilter = debounce((e: number | undefined) => {
|
||||||
|
if (e && (e < 1700 || e > 2300)) return;
|
||||||
|
setFilters({
|
||||||
|
jfParams: {
|
||||||
|
...filter.jfParams,
|
||||||
|
maxYear: e,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
||||||
|
const genreFilterString = e?.join(',');
|
||||||
|
setFilters({
|
||||||
|
jfParams: {
|
||||||
|
...filter.jfParams,
|
||||||
|
genreIds: genreFilterString,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0.8rem">
|
||||||
|
{toggleFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
key={`nd-filter-${filter.label}`}
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<Switch
|
||||||
|
checked={filter?.value || false}
|
||||||
|
size="xs"
|
||||||
|
onChange={filter.onChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Group position="apart">
|
||||||
|
<Text>Year range</Text>
|
||||||
|
<Group>
|
||||||
|
<NumberInput
|
||||||
|
required
|
||||||
|
max={2300}
|
||||||
|
min={1700}
|
||||||
|
size="sm"
|
||||||
|
value={filter.jfParams?.minYear}
|
||||||
|
width={60}
|
||||||
|
onChange={handleMinYearFilter}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
max={2300}
|
||||||
|
min={1700}
|
||||||
|
size="sm"
|
||||||
|
value={filter.jfParams?.maxYear}
|
||||||
|
width={60}
|
||||||
|
onChange={handleMaxYearFilter}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Stack>
|
||||||
|
<Text>Genres</Text>
|
||||||
|
<MultiSelect
|
||||||
|
clearable
|
||||||
|
searchable
|
||||||
|
data={genreList}
|
||||||
|
defaultValue={selectedGenres}
|
||||||
|
width={250}
|
||||||
|
onChange={handleGenresFilter}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Stack>
|
||||||
|
<Text>Tags</Text>
|
||||||
|
<MultiSelect
|
||||||
|
disabled
|
||||||
|
data={[]}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
|
import { NumberInput, Switch, Text } from '/@/renderer/components';
|
||||||
|
import { useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
|
export const NavidromeAlbumFilters = () => {
|
||||||
|
const { filter } = useAlbumListStore();
|
||||||
|
const setFilters = useSetAlbumFilters();
|
||||||
|
|
||||||
|
const toggleFilters = [
|
||||||
|
{
|
||||||
|
label: 'Is rated',
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilters({
|
||||||
|
ndParams: { ...filter.ndParams, has_rating: e.currentTarget.checked ? true : undefined },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: filter.ndParams?.has_rating,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Is favorited',
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilters({
|
||||||
|
ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: filter.ndParams?.starred,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Is compilation',
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilters({
|
||||||
|
ndParams: { ...filter.ndParams, compilation: e.currentTarget.checked ? true : undefined },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: filter.ndParams?.compilation,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Is recently played',
|
||||||
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilters({
|
||||||
|
ndParams: {
|
||||||
|
...filter.ndParams,
|
||||||
|
recently_played: e.currentTarget.checked ? true : undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
value: filter.ndParams?.recently_played,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleYearFilter = debounce((e: number | undefined) => {
|
||||||
|
setFilters({
|
||||||
|
ndParams: {
|
||||||
|
...filter.ndParams,
|
||||||
|
year: e,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="0.8rem">
|
||||||
|
{toggleFilters.map((filter) => (
|
||||||
|
<Group
|
||||||
|
key={`nd-filter-${filter.label}`}
|
||||||
|
position="apart"
|
||||||
|
>
|
||||||
|
<Text>{filter.label}</Text>
|
||||||
|
<Switch
|
||||||
|
checked={filter?.value || false}
|
||||||
|
size="xs"
|
||||||
|
onChange={filter.onChange}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
))}
|
||||||
|
<Divider my="0.5rem" />
|
||||||
|
<Group position="apart">
|
||||||
|
<Text>Year</Text>
|
||||||
|
<NumberInput
|
||||||
|
max={5000}
|
||||||
|
min={0}
|
||||||
|
size="xs"
|
||||||
|
value={filter.ndParams?.year}
|
||||||
|
width={50}
|
||||||
|
onChange={handleYearFilter}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
Reference in a new issue