mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add reset button to list filters
This commit is contained in:
@@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user