add reset button to list filters

This commit is contained in:
jeffvli
2026-01-17 17:43:55 -08:00
parent 5b519320c2
commit 9b97a3fa61
11 changed files with 180 additions and 105 deletions
@@ -38,10 +38,16 @@ export const SelectWithInvalidData = ({ data, defaultValue, ...props }: SelectPr
); );
}; };
export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: MultiSelectProps) => { export const MultiSelectWithInvalidData = ({
data,
defaultValue,
value,
...props
}: MultiSelectProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const currentValue = value ?? defaultValue;
const [fullData, missing] = useMemo(() => { const [fullData, missing] = useMemo(() => {
if (defaultValue?.length) { if (currentValue?.length) {
const validValues = new Set<string>(); const validValues = new Set<string>();
for (const item of data || []) { for (const item of data || []) {
if (typeof item === 'string') { if (typeof item === 'string') {
@@ -53,9 +59,9 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
const missingFields: string[] = []; const missingFields: string[] = [];
for (const value of defaultValue) { for (const val of currentValue) {
if (!validValues.has(value)) { if (!validValues.has(val)) {
missingFields.push(value); missingFields.push(val);
} }
} }
@@ -65,7 +71,7 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
} }
return [data, []]; return [data, []];
}, [data, defaultValue]); }, [data, currentValue]);
const error = useMemo( const error = useMemo(
() => () =>
@@ -75,5 +81,13 @@ export const MultiSelectWithInvalidData = ({ data, defaultValue, ...props }: Mul
[missing, t], [missing, t],
); );
return <MultiSelect data={fullData} defaultValue={defaultValue} error={error} {...props} />; return (
<MultiSelect
data={fullData}
defaultValue={defaultValue}
error={error}
value={value}
{...props}
/>
);
}; };
@@ -15,6 +15,7 @@ import {
} from '/@/renderer/features/shared/components/multi-select-rows'; } from '/@/renderer/features/shared/components/multi-select-rows';
import { useCurrentServerId } from '/@/renderer/store'; import { useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store'; import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -44,6 +45,7 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
const isGenrePage = customFilters?.genreIds !== undefined; const isGenrePage = customFilters?.genreIds !== undefined;
const { const {
clear,
query, query,
setAlbumArtist, setAlbumArtist,
setCompilation, setCompilation,
@@ -297,10 +299,10 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
<Stack px="md" py="md"> <Stack px="md" py="md">
{yesNoFilter.map((filter) => ( {yesNoFilter.map((filter) => (
<YesNoSelect <YesNoSelect
defaultValue={filter.value ? filter.value.toString() : undefined}
key={`jf-filter-${filter.label}`} key={`jf-filter-${filter.label}`}
label={filter.label} label={filter.label}
onChange={(e) => filter.onChange(e ? e === 'true' : undefined)} onChange={(e) => filter.onChange(e ? e === 'true' : undefined)}
value={filter.value ? filter.value.toString() : undefined}
/> />
))} ))}
{!disableArtistFilter && ( {!disableArtistFilter && (
@@ -338,35 +340,39 @@ export const JellyfinAlbumFilters = ({ disableArtistFilter }: JellyfinAlbumFilte
<Divider my="md" /> <Divider my="md" />
<Group grow> <Group grow>
<NumberInput <NumberInput
defaultValue={query.minYear ?? undefined}
hideControls={false} hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })} label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={2300} max={2300}
min={1700} min={1700}
onChange={(e) => debouncedHandleMinYearFilter(e)} onChange={(e) => debouncedHandleMinYearFilter(e)}
required={!!query.minYear} required={!!query.minYear}
value={query.minYear ?? undefined}
/> />
<NumberInput <NumberInput
defaultValue={query.maxYear ?? undefined}
hideControls={false} hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })} label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={2300} max={2300}
min={1700} min={1700}
onChange={(e) => debouncedHandleMaxYearFilter(e)} onChange={(e) => debouncedHandleMaxYearFilter(e)}
required={!!query.minYear} required={!!query.minYear}
value={query.maxYear ?? undefined}
/> />
</Group> </Group>
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && ( {tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
<MultiSelectWithInvalidData <MultiSelectWithInvalidData
clearable clearable
data={tagsQuery.data.boolTags} data={tagsQuery.data.boolTags}
defaultValue={query._custom?.[tagsQuery.data.boolTags.join('|')] || []}
label={t('common.tags', { postProcess: 'sentenceCase' })} label={t('common.tags', { postProcess: 'sentenceCase' })}
onChange={handleTagFilter} onChange={handleTagFilter}
searchable searchable
value={query._custom?.[tagsQuery.data.boolTags.join('|')] || []}
width={250} width={250}
/> />
)} )}
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Stack> </Stack>
); );
}; };
@@ -17,6 +17,7 @@ import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store'; import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { titleCase } from '/@/renderer/utils'; import { titleCase } from '/@/renderer/utils';
import { NDSongQueryFieldsLabelMap } from '/@/shared/api/navidrome/navidrome-types'; import { NDSongQueryFieldsLabelMap } from '/@/shared/api/navidrome/navidrome-types';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -45,6 +46,7 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
const isGenrePage = customFilters?.genreIds !== undefined; const isGenrePage = customFilters?.genreIds !== undefined;
const { const {
clear,
query, query,
setAlbumArtist, setAlbumArtist,
setCompilation, setCompilation,
@@ -285,11 +287,11 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
</Text> </Text>
<SegmentedControl <SegmentedControl
data={segmentedControlData} data={segmentedControlData}
defaultValue={booleanToSegmentValue(query.favorite)}
onChange={(value) => { onChange={(value) => {
setFavorite(segmentValueToBoolean(value)); setFavorite(segmentValueToBoolean(value));
}} }}
size="sm" size="sm"
value={booleanToSegmentValue(query.favorite)}
w="100%" w="100%"
/> />
</Stack> </Stack>
@@ -299,18 +301,18 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
</Text> </Text>
<SegmentedControl <SegmentedControl
data={segmentedControlData} data={segmentedControlData}
defaultValue={booleanToSegmentValue(query.compilation)}
onChange={(value) => { onChange={(value) => {
setCompilation(segmentValueToBoolean(value)); setCompilation(segmentValueToBoolean(value));
}} }}
size="sm" size="sm"
value={booleanToSegmentValue(query.compilation)}
w="100%" w="100%"
/> />
</Stack> </Stack>
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group justify="space-between" key={`nd-filter-${filter.label}`}> <Group justify="space-between" key={`nd-filter-${filter.label}`}>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<Switch defaultChecked={filter?.value ?? false} onChange={filter.onChange} /> <Switch checked={filter?.value ?? false} onChange={filter.onChange} />
</Group> </Group>
))} ))}
{!disableArtistFilter && ( {!disableArtistFilter && (
@@ -346,15 +348,19 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
)} )}
<Divider my="md" /> <Divider my="md" />
<NumberInput <NumberInput
defaultValue={query.minYear ?? undefined}
hideControls={false} hideControls={false}
label={t('common.year', { postProcess: 'titleCase' })} label={t('common.year', { postProcess: 'titleCase' })}
max={5000} max={5000}
min={0} min={0}
onChange={(e) => debouncedHandleYearFilter(e)} onChange={(e) => debouncedHandleYearFilter(e)}
value={query.minYear ?? undefined}
/> />
<Divider my="md" /> <Divider my="md" />
<TagFilters /> <TagFilters />
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Stack> </Stack>
); );
}; };
@@ -377,7 +383,7 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
[options], [options],
); );
const defaultValue = useMemo(() => { const currentValue = useMemo(() => {
if (!value) return []; if (!value) return [];
return Array.isArray(value) ? value : [value]; return Array.isArray(value) ? value : [value];
}, [value]); }, [value]);
@@ -397,12 +403,12 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
<MultiSelectWithInvalidData <MultiSelectWithInvalidData
clearable clearable
data={selectData} data={selectData}
defaultValue={defaultValue}
key={tagValue} key={tagValue}
label={label} label={label}
limit={100} limit={100}
onChange={handleChange} onChange={handleChange}
searchable searchable
value={currentValue}
/> />
); );
}; };
@@ -13,6 +13,7 @@ import {
} from '/@/renderer/features/shared/components/multi-select-rows'; } from '/@/renderer/features/shared/components/multi-select-rows';
import { useCurrentServerId } from '/@/renderer/store'; import { useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store'; import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -37,7 +38,7 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
const isGenrePage = customFilters?.genreIds !== undefined; const isGenrePage = customFilters?.genreIds !== undefined;
const { query, setAlbumArtist, setFavorite, setGenreId, setMaxYear, setMinYear } = const { clear, query, setAlbumArtist, setFavorite, setGenreId, setMaxYear, setMinYear } =
useAlbumListFilters(); useAlbumListFilters();
const albumArtistListQuery = useSuspenseQuery( const albumArtistListQuery = useSuspenseQuery(
@@ -215,7 +216,7 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group justify="space-between" key={`ss-filter-${filter.label}`}> <Group justify="space-between" key={`ss-filter-${filter.label}`}>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<Switch defaultChecked={filter.value ?? false} onChange={filter.onChange} /> <Switch checked={filter.value ?? false} onChange={filter.onChange} />
</Group> </Group>
))} ))}
{!disableArtistFilter && ( {!disableArtistFilter && (
@@ -251,24 +252,28 @@ export const SubsonicAlbumFilters = ({ disableArtistFilter }: SubsonicAlbumFilte
<Divider my="md" /> <Divider my="md" />
<Group grow> <Group grow>
<NumberInput <NumberInput
defaultValue={query.minYear ?? undefined}
disabled={Boolean(query.genreIds && query.genreIds.length > 0)} disabled={Boolean(query.genreIds && query.genreIds.length > 0)}
hideControls={false} hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })} label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={5000} max={5000}
min={0} min={0}
onChange={(e) => debouncedHandleMinYearFilter(e)} onChange={(e) => debouncedHandleMinYearFilter(e)}
value={query.minYear ?? undefined}
/> />
<NumberInput <NumberInput
defaultValue={query.maxYear ?? undefined}
disabled={Boolean(query.genreIds && query.genreIds.length > 0)} disabled={Boolean(query.genreIds && query.genreIds.length > 0)}
hideControls={false} hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })} label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={5000} max={5000}
min={0} min={0}
onChange={(e) => debouncedHandleMaxYearFilter(e)} onChange={(e) => debouncedHandleMaxYearFilter(e)}
value={query.maxYear ?? undefined}
/> />
</Group> </Group>
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Stack> </Stack>
); );
}; };
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
@@ -10,6 +10,7 @@ import {
parseBooleanParam, parseBooleanParam,
parseCustomFiltersParam, parseCustomFiltersParam,
parseIntParam, parseIntParam,
setMultipleSearchParams,
setSearchParam, setSearchParam,
} from '/@/renderer/utils/query-params'; } from '/@/renderer/utils/query-params';
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types'; import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
@@ -18,12 +19,9 @@ import { ItemListKey } from '/@/shared/types/types';
export const useAlbumListFilters = (listKey?: ItemListKey) => { export const useAlbumListFilters = (listKey?: ItemListKey) => {
const resolvedListKey = listKey ?? ItemListKey.ALBUM; const resolvedListKey = listKey ?? ItemListKey.ALBUM;
const { setSortBy, sortBy } = useSortByFilter<AlbumListSort>( const { sortBy } = useSortByFilter<AlbumListSort>(AlbumListSort.NAME, resolvedListKey);
AlbumListSort.NAME,
resolvedListKey,
);
const { setSortOrder, sortOrder } = useSortOrderFilter(SortOrder.ASC, resolvedListKey); const { sortOrder } = useSortOrderFilter(SortOrder.ASC, resolvedListKey);
const { searchTerm, setSearchTerm } = useSearchTermFilter(''); const { searchTerm, setSearchTerm } = useSearchTermFilter('');
@@ -74,12 +72,6 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
[searchParams], [searchParams],
); );
// Use a ref to track the latest custom filters to avoid stale state during batched updates
const customRef = useRef<null | Record<string, any> | undefined>(custom);
useEffect(() => {
customRef.current = custom;
}, [custom]);
const setGenreId = useCallback( const setGenreId = useCallback(
(value: null | string[]) => { (value: null | string[]) => {
setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.GENRE_ID, value), { setSearchParams((prev) => setSearchParam(prev, FILTER_KEYS.ALBUM.GENRE_ID, value), {
@@ -184,32 +176,27 @@ export const useAlbumListFilters = (listKey?: ItemListKey) => {
); );
const clear = useCallback(() => { const clear = useCallback(() => {
setAlbumArtist(null); setSearchParams(
setCompilation(null); (prev) =>
setCustom(null); setMultipleSearchParams(
setFavorite(null); prev,
setGenreId(null); {
setHasRating(null); [FILTER_KEYS.ALBUM._CUSTOM]: null,
setMaxYear(null); [FILTER_KEYS.ALBUM.ARTIST_IDS]: null,
setMinYear(null); [FILTER_KEYS.ALBUM.COMPILATION]: null,
setRecentlyPlayed(null); [FILTER_KEYS.ALBUM.FAVORITE]: null,
setSearchTerm(null); [FILTER_KEYS.ALBUM.GENRE_ID]: null,
setSortBy(AlbumListSort.NAME); [FILTER_KEYS.ALBUM.HAS_RATING]: null,
setSortOrder(SortOrder.ASC); [FILTER_KEYS.ALBUM.MAX_YEAR]: null,
}, [ [FILTER_KEYS.ALBUM.MIN_YEAR]: null,
setAlbumArtist, [FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: null,
setCompilation, [FILTER_KEYS.SHARED.SEARCH_TERM]: null,
setCustom, },
setFavorite, new Set([FILTER_KEYS.ALBUM._CUSTOM]),
setGenreId, ),
setHasRating, { replace: true },
setMaxYear, );
setMinYear, }, [setSearchParams]);
setRecentlyPlayed,
setSearchTerm,
setSortBy,
setSortOrder,
]);
const query = useMemo( const query = useMemo(
() => ({ () => ({
@@ -1,27 +1,32 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useSearchParams } from 'react-router';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { AlbumArtistListSort, SortOrder } from '/@/shared/types/domain-types'; import { setMultipleSearchParams } from '/@/renderer/utils/query-params';
import { AlbumArtistListSort } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const useAlbumArtistListFilters = () => { export const useAlbumArtistListFilters = () => {
const { setSortBy, sortBy } = useSortByFilter<AlbumArtistListSort>( const { sortBy } = useSortByFilter<AlbumArtistListSort>(null, ItemListKey.ALBUM_ARTIST);
null,
ItemListKey.ALBUM_ARTIST,
);
const { setSortOrder, sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST); const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST);
const { searchTerm, setSearchTerm } = useSearchTermFilter(''); const { searchTerm, setSearchTerm } = useSearchTermFilter('');
const [, setSearchParams] = useSearchParams();
const clear = useCallback(() => { const clear = useCallback(() => {
setSearchTerm(null); setSearchParams(
setSortBy(AlbumArtistListSort.NAME); (prev) =>
setSortOrder(SortOrder.ASC); setMultipleSearchParams(prev, {
}, [setSearchTerm, setSortBy, setSortOrder]); [FILTER_KEYS.SHARED.SEARCH_TERM]: null,
}),
{ replace: true },
);
}, [setSearchParams]);
const query = { const query = {
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined, [FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
@@ -15,6 +15,7 @@ import {
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store'; import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -34,7 +35,7 @@ export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFilters
const server = useCurrentServer(); const server = useCurrentServer();
const serverId = server.id; const serverId = server.id;
const { t } = useTranslation(); const { t } = useTranslation();
const { query, setArtistIds, setCustom, setFavorite, setMaxYear, setMinYear } = const { clear, query, setArtistIds, setCustom, setFavorite, setMaxYear, setMinYear } =
useSongListFilters(); useSongListFilters();
const { customFilters } = useListContext(); const { customFilters } = useListContext();
@@ -280,10 +281,10 @@ export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFilters
<Stack px="md" py="md"> <Stack px="md" py="md">
{yesNoFilters.map((filter) => ( {yesNoFilters.map((filter) => (
<YesNoSelect <YesNoSelect
defaultValue={filter.value ? filter.value.toString() : undefined}
key={`jf-filter-${filter.label}`} key={`jf-filter-${filter.label}`}
label={filter.label} label={filter.label}
onChange={(e) => filter.onChange(e ? e === 'true' : undefined)} onChange={(e) => filter.onChange(e ? e === 'true' : undefined)}
value={filter.value ? filter.value.toString() : undefined}
/> />
))} ))}
{!disableArtistFilter && ( {!disableArtistFilter && (
@@ -320,34 +321,38 @@ export const JellyfinSongFilters = ({ disableArtistFilter }: JellyfinSongFilters
<Divider my="md" /> <Divider my="md" />
<Group grow> <Group grow>
<NumberInput <NumberInput
defaultValue={query.minYear ?? undefined}
hideControls={false} hideControls={false}
label={t('filter.fromYear', { postProcess: 'sentenceCase' })} label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
max={2300} max={2300}
min={1700} min={1700}
onChange={(e) => debouncedHandleMinYearFilter(e)} onChange={(e) => debouncedHandleMinYearFilter(e)}
required={!!query.minYear} required={!!query.minYear}
value={query.minYear ?? undefined}
/> />
<NumberInput <NumberInput
defaultValue={query.maxYear ?? undefined}
hideControls={false} hideControls={false}
label={t('filter.toYear', { postProcess: 'sentenceCase' })} label={t('filter.toYear', { postProcess: 'sentenceCase' })}
max={2300} max={2300}
min={1700} min={1700}
onChange={(e) => debouncedHandleMaxYearFilter(e)} onChange={(e) => debouncedHandleMaxYearFilter(e)}
required={!!query.minYear} required={!!query.minYear}
value={query.maxYear ?? undefined}
/> />
</Group> </Group>
{tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && ( {tagsQuery.data?.boolTags && tagsQuery.data.boolTags.length > 0 && (
<MultiSelectWithInvalidData <MultiSelectWithInvalidData
clearable clearable
data={tagsQuery.data.boolTags} data={tagsQuery.data.boolTags}
defaultValue={selectedTags}
label={t('common.tags', { postProcess: 'sentenceCase' })} label={t('common.tags', { postProcess: 'sentenceCase' })}
onChange={handleTagFilter} onChange={handleTagFilter}
searchable searchable
value={selectedTags}
/> />
)} )}
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Stack> </Stack>
); );
}; };
@@ -17,6 +17,7 @@ import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store'; import { useAppStore, useAppStoreActions } from '/@/renderer/store/app.store';
import { titleCase } from '/@/renderer/utils'; import { titleCase } from '/@/renderer/utils';
import { NDSongQueryFieldsLabelMap } from '/@/shared/api/navidrome/navidrome-types'; import { NDSongQueryFieldsLabelMap } from '/@/shared/api/navidrome/navidrome-types';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -31,7 +32,7 @@ export const NavidromeSongFilters = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const serverId = server.id; const serverId = server.id;
const { query, setArtistIds, setFavorite, setGenreId, setMaxYear, setMinYear } = const { clear, query, setArtistIds, setFavorite, setGenreId, setMaxYear, setMinYear } =
useSongListFilters(); useSongListFilters();
const { customFilters } = useListContext(); const { customFilters } = useListContext();
@@ -252,11 +253,11 @@ export const NavidromeSongFilters = () => {
</Text> </Text>
<SegmentedControl <SegmentedControl
data={segmentedControlData} data={segmentedControlData}
defaultValue={booleanToSegmentValue(query.favorite)}
onChange={(value) => { onChange={(value) => {
setFavorite(segmentValueToBoolean(value)); setFavorite(segmentValueToBoolean(value));
}} }}
size="sm" size="sm"
value={booleanToSegmentValue(query.favorite)}
w="100%" w="100%"
/> />
</Stack> </Stack>
@@ -284,15 +285,19 @@ export const NavidromeSongFilters = () => {
/> />
)} )}
<NumberInput <NumberInput
defaultValue={query.minYear ?? undefined}
hideControls={false} hideControls={false}
label={t('common.year', { postProcess: 'titleCase' })} label={t('common.year', { postProcess: 'titleCase' })}
max={5000} max={5000}
min={0} min={0}
onChange={(e) => debouncedHandleYearFilter(e)} onChange={(e) => debouncedHandleYearFilter(e)}
value={query.minYear ?? undefined}
/> />
<Divider my="md" /> <Divider my="md" />
<TagFilters /> <TagFilters />
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Stack> </Stack>
); );
}; };
@@ -315,7 +320,7 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
[options], [options],
); );
const defaultValue = useMemo(() => { const currentValue = useMemo(() => {
if (!value) return []; if (!value) return [];
return Array.isArray(value) ? value : [value]; return Array.isArray(value) ? value : [value];
}, [value]); }, [value]);
@@ -335,12 +340,12 @@ const TagFilterItem = ({ label, onChange, options, tagValue, value }: TagFilterI
<MultiSelectWithInvalidData <MultiSelectWithInvalidData
clearable clearable
data={selectData} data={selectData}
defaultValue={defaultValue}
key={tagValue} key={tagValue}
label={label} label={label}
limit={100} limit={100}
onChange={handleChange} onChange={handleChange}
searchable searchable
value={currentValue}
/> />
); );
}; };
@@ -5,6 +5,7 @@ import { useListContext } from '/@/renderer/context/list-context';
import { useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { GenreMultiSelectRow } from '/@/renderer/features/shared/components/multi-select-rows'; import { GenreMultiSelectRow } from '/@/renderer/features/shared/components/multi-select-rows';
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { Button } from '/@/shared/components/button/button';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select'; import { VirtualMultiSelect } from '/@/shared/components/multi-select/virtual-multi-select';
@@ -14,7 +15,7 @@ import { Text } from '/@/shared/components/text/text';
export const SubsonicSongFilters = () => { export const SubsonicSongFilters = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { query, setFavorite, setGenreId } = useSongListFilters(); const { clear, query, setFavorite, setGenreId } = useSongListFilters();
const { customFilters } = useListContext(); const { customFilters } = useListContext();
@@ -72,7 +73,7 @@ export const SubsonicSongFilters = () => {
{toggleFilters.map((filter) => ( {toggleFilters.map((filter) => (
<Group justify="space-between" key={`ss-filter-${filter.label}`}> <Group justify="space-between" key={`ss-filter-${filter.label}`}>
<Text>{filter.label}</Text> <Text>{filter.label}</Text>
<Switch defaultChecked={filter.value ?? false} onChange={filter.onChange} /> <Switch checked={filter.value ?? false} onChange={filter.onChange} />
</Group> </Group>
))} ))}
{!isGenrePage && ( {!isGenrePage && (
@@ -90,6 +91,10 @@ export const SubsonicSongFilters = () => {
/> />
</> </>
)} )}
<Divider my="md" />
<Button fullWidth onClick={clear} variant="subtle">
{t('common.reset', { postProcess: 'sentenceCase' })}
</Button>
</Stack> </Stack>
); );
}; };
@@ -11,6 +11,7 @@ import {
parseCustomFiltersParam, parseCustomFiltersParam,
parseIntParam, parseIntParam,
setJsonSearchParam, setJsonSearchParam,
setMultipleSearchParams,
setSearchParam, setSearchParam,
} from '/@/renderer/utils/query-params'; } from '/@/renderer/utils/query-params';
import { SongListSort, SortOrder } from '/@/shared/types/domain-types'; import { SongListSort, SortOrder } from '/@/shared/types/domain-types';
@@ -19,9 +20,9 @@ import { ItemListKey } from '/@/shared/types/types';
export const useSongListFilters = (listKey?: ItemListKey) => { export const useSongListFilters = (listKey?: ItemListKey) => {
const resolvedListKey = listKey ?? ItemListKey.SONG; const resolvedListKey = listKey ?? ItemListKey.SONG;
const { setSortBy, sortBy } = useSortByFilter<SongListSort>(SongListSort.NAME, resolvedListKey); const { sortBy } = useSortByFilter<SongListSort>(SongListSort.NAME, resolvedListKey);
const { setSortOrder, sortOrder } = useSortOrderFilter(SortOrder.ASC, resolvedListKey); const { sortOrder } = useSortOrderFilter(SortOrder.ASC, resolvedListKey);
const { searchTerm, setSearchTerm } = useSearchTermFilter(''); const { searchTerm, setSearchTerm } = useSearchTermFilter('');
@@ -145,28 +146,25 @@ export const useSongListFilters = (listKey?: ItemListKey) => {
); );
const clear = useCallback(() => { const clear = useCallback(() => {
setAlbumIds(null); setSearchParams(
setArtistIds(null); (prev) =>
setCustom(null); setMultipleSearchParams(
setFavorite(null); prev,
setGenreId(null); {
setMaxYear(null); [FILTER_KEYS.SHARED.SEARCH_TERM]: null,
setMinYear(null); [FILTER_KEYS.SONG._CUSTOM]: null,
setSearchTerm(null); [FILTER_KEYS.SONG.ALBUM_IDS]: null,
setSortBy(SongListSort.NAME); [FILTER_KEYS.SONG.ARTIST_IDS]: null,
setSortOrder(SortOrder.ASC); [FILTER_KEYS.SONG.FAVORITE]: null,
}, [ [FILTER_KEYS.SONG.GENRE_ID]: null,
setAlbumIds, [FILTER_KEYS.SONG.MAX_YEAR]: null,
setArtistIds, [FILTER_KEYS.SONG.MIN_YEAR]: null,
setCustom, },
setFavorite, new Set([FILTER_KEYS.SONG._CUSTOM]),
setGenreId, ),
setMaxYear, { replace: true },
setMinYear, );
setSearchTerm, }, [setSearchParams]);
setSortBy,
setSortOrder,
]);
const query = useMemo( const query = useMemo(
() => ({ () => ({
+39
View File
@@ -123,6 +123,45 @@ export const setJsonSearchParam = (
return newParams; return newParams;
}; };
export const setMultipleSearchParams = (
searchParams: URLSearchParams,
params: Record<
string,
boolean | null | number | Record<string, any> | string | string[] | undefined
>,
jsonKeys?: Set<string>,
): URLSearchParams => {
const newParams = new URLSearchParams(searchParams);
for (const [key, value] of Object.entries(params)) {
if (value === null || value === undefined) {
newParams.delete(key);
continue;
}
if (jsonKeys?.has(key)) {
if (typeof value === 'object' && !Array.isArray(value)) {
newParams.set(key, JSON.stringify(value));
} else {
newParams.delete(key);
}
} else {
if (Array.isArray(value)) {
newParams.delete(key);
value.forEach((v) => newParams.append(key, String(v)));
} else if (typeof value === 'boolean') {
newParams.set(key, String(value));
} else if (typeof value === 'number') {
newParams.set(key, String(value));
} else {
newParams.set(key, value as string);
}
}
}
return newParams;
};
/** /**
* Parse custom filters from URLSearchParams with validation * Parse custom filters from URLSearchParams with validation
*/ */