Use react-query for custom filters

This commit is contained in:
Mark McDowall 2025-11-23 13:20:41 -08:00
parent 9db17883df
commit 645f20dc1b
No known key found for this signature in database
44 changed files with 245 additions and 183 deletions

View file

@ -16,10 +16,10 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import BlockListModel from 'typings/Blocklist';
import { CheckInputChanged } from 'typings/inputs';
@ -61,7 +61,7 @@ function BlocklistContent() {
const filters = useFilters();
const { isRemoving, removeBlocklistItems } = useRemoveBlocklistItems();
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
const customFilters = useCustomFiltersList('blocklist');
const isClearingBlocklistExecuting = useSelector(
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
);

View file

@ -1,12 +1,11 @@
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import { filterBuilderValueTypes } from 'Helpers/Props';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import Blocklist from 'typings/Blocklist';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
@ -43,9 +42,7 @@ const useBlocklist = () => {
const { page, goToPage } = usePage('blocklist');
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
useBlocklistOptions();
const customFilters = useSelector(
createCustomFiltersSelector('blocklist')
) as CustomFilter[];
const customFilters = useCustomFiltersList('blocklist');
const filters = useMemo(() => {
return findSelectedFilters(selectedFilterKey, FILTERS, customFilters);

View file

@ -13,11 +13,11 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import HistoryItem from 'typings/History';
import { TableOptionsChangePayload } from 'typings/Table';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
@ -59,7 +59,7 @@ function History() {
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
useSelector(createEpisodesFetchingSelector());
const customFilters = useSelector(createCustomFiltersSelector('history'));
const customFilters = useCustomFiltersList('history');
const dispatch = useDispatch();
const isFetchingAny = isLoading || isEpisodesFetching;

View file

@ -1,12 +1,11 @@
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import { filterBuilderValueTypes } from 'Helpers/Props';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import History from 'typings/History';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
@ -117,9 +116,7 @@ const useHistory = () => {
const { page, goToPage } = usePage('history');
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
useHistoryOptions();
const customFilters = useSelector(
createCustomFiltersSelector('history')
) as CustomFilter[];
const customFilters = useCustomFiltersList('history');
const filters = useMemo(() => {
return findSelectedFilters(selectedFilterKey, FILTERS, customFilters);

View file

@ -23,11 +23,11 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import QueueModel from 'typings/Queue';
@ -81,7 +81,7 @@ function QueueContent() {
const { count } = useQueueStatus();
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
useSelector(createEpisodesFetchingSelector());
const customFilters = useSelector(createCustomFiltersSelector('queue'));
const customFilters = useCustomFiltersList('queue');
const isRefreshMonitoredDownloadsExecuting = useSelector(
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)

View file

@ -1,12 +1,11 @@
import { keepPreviousData, useQueryClient } from '@tanstack/react-query';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { CustomFilter, Filter, FilterBuilderProp } from 'App/State/AppState';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import { filterBuilderValueTypes } from 'Helpers/Props';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import Queue from 'typings/Queue';
import getQueryString from 'Utilities/Fetch/getQueryString';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
@ -67,9 +66,7 @@ const useQueue = () => {
sortKey,
sortDirection,
} = useQueueOptions();
const customFilters = useSelector(
createCustomFiltersSelector('queue')
) as CustomFilter[];
const customFilters = useCustomFiltersList('queue');
const filters = useMemo(() => {
return findSelectedFilters(selectedFilterKey, FILTERS, customFilters);

View file

@ -1,7 +1,7 @@
import Column from 'Components/Table/Column';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { ValidationFailure } from 'typings/pending';
import { Filter, FilterBuilderProp } from './AppState';
export interface Error {
status?: number;

View file

@ -1,11 +1,7 @@
import ModelBase from 'App/ModelBase';
import { FilterBuilderTypes } from 'Helpers/Props/filterBuilderTypes';
import { DateFilterValue, FilterType } from 'Helpers/Props/filterTypes';
import { Error } from './AppSectionState';
import BlocklistAppState from './BlocklistAppState';
import CaptchaAppState from './CaptchaAppState';
import CommandAppState from './CommandAppState';
import CustomFiltersAppState from './CustomFiltersAppState';
import EpisodesAppState from './EpisodesAppState';
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
import ImportSeriesAppState from './ImportSeriesAppState';
@ -18,38 +14,6 @@ import ProviderOptionsAppState from './ProviderOptionsAppState';
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
import SettingsAppState from './SettingsAppState';
export interface FilterBuilderPropOption {
id: string;
name: string;
}
export interface FilterBuilderProp<T> {
name: string;
label: string | (() => string);
type: FilterBuilderTypes;
valueType?: string;
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
}
// TODO: Make generic so key can be keyof T
export interface PropertyFilter {
key: string;
value: string | string[] | number[] | boolean[] | DateFilterValue;
type: FilterType;
}
export interface Filter {
key: string;
label: string | (() => string);
filters: PropertyFilter[];
}
export interface CustomFilter extends ModelBase {
type: string;
label: string;
filters: PropertyFilter[];
}
export interface AppSectionState {
isUpdated: boolean;
isConnected: boolean;
@ -77,7 +41,6 @@ interface AppState {
blocklist: BlocklistAppState;
captcha: CaptchaAppState;
commands: CommandAppState;
customFilters: CustomFiltersAppState;
episodeHistory: HistoryAppState;
episodes: EpisodesAppState;
episodesSelection: EpisodesAppState;

View file

@ -1,8 +1,5 @@
import { CustomFilter } from './AppState';
interface ClientSideCollectionAppState {
totalItems: number;
customFilters: CustomFilter[];
}
export default ClientSideCollectionAppState;

View file

@ -1,12 +0,0 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import { CustomFilter } from './AppState';
interface CustomFiltersAppState
extends AppSectionState<CustomFilter>,
AppSectionDeleteState,
AppSectionSaveState {}
export default CustomFiltersAppState;

View file

@ -3,9 +3,9 @@ import AppSectionState, {
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Column from 'Components/Table/Column';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { SortDirection } from 'Helpers/Props/sortDirections';
import Series from 'Series/Series';
import { Filter, FilterBuilderProp } from './AppState';
export interface SeriesIndexAppState {
sortKey: string;

View file

@ -17,11 +17,11 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Episode from 'Episode/Episode';
import EpisodeFileProvider from 'EpisodeFile/EpisodeFileProvider';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useMeasure from 'Helpers/Hooks/useMeasure';
import { align, icons } from 'Helpers/Props';
import NoSeries from 'Series/NoSeries';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
@ -53,7 +53,7 @@ function CalendarPage() {
const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(commandNames.RSS_SYNC)
);
const customFilters = useSelector(createCustomFiltersSelector('calendar'));
const customFilters = useCustomFiltersList('calendar');
const hasSeries = !!useSelector(createSeriesCountSelector());
const [pageContentRef, { width }] = useMeasure();

View file

@ -3,14 +3,15 @@ import moment from 'moment';
import { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { create } from 'zustand';
import AppState, { Filter, FilterBuilderProp } from 'App/State/AppState';
import AppState from 'App/State/AppState';
import Command from 'Commands/Command';
import * as commandNames from 'Commands/commandNames';
import { setEpisodeQueryKey } from 'Episode/useEpisode';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { filterBuilderValueTypes } from 'Helpers/Props';
import { executeCommandHelper } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { CalendarItem } from 'typings/Calendar';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
@ -81,7 +82,7 @@ const useCalendar = () => {
const time = useCalendarTime();
const selectedFilterKey = useCalendarOption('selectedFilterKey');
const view = useCalendarOption('view');
const customFilters = useSelector(createCustomFiltersSelector('calendar'));
const customFilters = useCustomFiltersList('calendar');
const { start, end } = useMemo(() => {
return getPopulatableRange(dates[0], dates[dates.length - 1], view);

View file

@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { FilterBuilderPropOption } from 'App/State/AppState';
import { FilterBuilderPropOption } from 'Filters/Filter';
import { filterBuilderTypes } from 'Helpers/Props';
import * as filterTypes from 'Helpers/Props/filterTypes';
import sortByProp from 'Utilities/Array/sortByProp';

View file

@ -1,11 +1,5 @@
import { maxBy } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState, {
CustomFilter,
FilterBuilderProp,
PropertyFilter,
} from 'App/State/AppState';
import FormInputGroup, {
ValidationMessage,
} from 'Components/Form/FormInputGroup';
@ -15,9 +9,14 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import {
CustomFilter,
FilterBuilderProp,
PropertyFilter,
} from 'Filters/Filter';
import { useSaveCustomFilter } from 'Filters/useCustomFilters';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { inputTypes } from 'Helpers/Props';
import { saveCustomFilter } from 'Store/Actions/customFilterActions';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import FilterBuilderRow from './FilterBuilderRow';
@ -50,10 +49,8 @@ function FilterBuilderModalContent<T>({
onCancelPress,
onModalClose,
}: FilterBuilderModalContentProps<T>) {
const dispatch = useDispatch();
const { isSaving, saveError } = useSelector(
(state: AppState) => state.customFilters
);
const { newCustomFilter, saveCustomFilter, isSaving, saveError } =
useSaveCustomFilter(id);
const { initialLabel, initialFilters } = useMemo(() => {
if (id) {
@ -121,13 +118,15 @@ function FilterBuilderModalContent<T>({
return;
}
dispatch(saveCustomFilter({ id, type: customFilterType, label, filters }));
}, [id, customFilterType, label, filters, dispatch]);
saveCustomFilter({ type: customFilterType, label, filters });
}, [customFilterType, label, filters, saveCustomFilter]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
if (id) {
dispatchSetFilter({ selectedFilterKey: id });
} else if (newCustomFilter) {
dispatchSetFilter({ selectedFilterKey: newCustomFilter.id });
} else {
const last = maxBy(customFilters, 'id');
@ -141,6 +140,7 @@ function FilterBuilderModalContent<T>({
}, [
id,
customFilters,
newCustomFilter,
isSaving,
wasSaving,
saveError,

View file

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { FilterBuilderProp, PropertyFilter } from 'App/State/AppState';
import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton';
import { FilterBuilderProp, PropertyFilter } from 'Filters/Filter';
import { filterBuilderValueTypes, icons } from 'Helpers/Props';
import {
FilterBuilderTypes,

View file

@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from 'react';
import { FilterBuilderProp } from 'App/State/AppState';
import TagInput, { TagBase } from 'Components/Form/Tag/TagInput';
import { FilterBuilderProp } from 'Filters/Filter';
import {
filterBuilderTypes,
filterBuilderValueTypes,

View file

@ -1,19 +1,16 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { Error } from 'App/State/AppSectionState';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { useDeleteCustomFilter } from 'Filters/useCustomFilters';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons } from 'Helpers/Props';
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
import translate from 'Utilities/String/translate';
import styles from './CustomFilter.css';
interface CustomFilterProps {
id: number;
label: string;
isDeleting: boolean;
deleteError?: Error;
dispatchSetFilter: (payload: { selectedFilterKey: string | number }) => void;
onEditPress: (id: number) => void;
}
@ -21,11 +18,11 @@ interface CustomFilterProps {
function CustomFilter({
id,
label,
isDeleting,
deleteError,
dispatchSetFilter,
onEditPress,
}: CustomFilterProps) {
const { deleteCustomFilter, isDeleting, deleteError } =
useDeleteCustomFilter(id);
const dispatch = useDispatch();
const wasDeleting = usePrevious(isDeleting);
const [isDeletingInternal, setIsDeletingInternal] = useState(false);
@ -37,8 +34,8 @@ function CustomFilter({
const handleRemovePress = useCallback(() => {
setIsDeletingInternal(true);
dispatch(deleteCustomFilter({ id }));
}, [id, dispatch]);
deleteCustomFilter();
}, [deleteCustomFilter]);
useEffect(() => {
if (wasDeleting && !isDeleting && isDeletingInternal && deleteError) {

View file

@ -1,14 +1,10 @@
import React from 'react';
import { useSelector } from 'react-redux';
import AppState, {
CustomFilter as CustomFilterModel,
} from 'App/State/AppState';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import sortByProp from 'Utilities/Array/sortByProp';
import { CustomFilter as CustomFilterModel } from 'Filters/Filter';
import translate from 'Utilities/String/translate';
import CustomFilter from './CustomFilter';
import styles from './CustomFiltersModalContent.css';
@ -28,23 +24,17 @@ function CustomFiltersModalContent({
onEditCustomFilter,
onModalClose,
}: CustomFiltersModalContentProps) {
const { isDeleting, deleteError } = useSelector(
(state: AppState) => state.customFilters
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('CustomFilters')}</ModalHeader>
<ModalBody>
{customFilters.sort(sortByProp('label')).map((customFilter) => {
{customFilters.map((customFilter) => {
return (
<CustomFilter
key={customFilter.id}
id={customFilter.id}
label={customFilter.label}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
onEditPress={onEditCustomFilter}
/>

View file

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react';
import { CustomFilter, FilterBuilderProp } from 'App/State/AppState';
import Modal from 'Components/Modal/Modal';
import { CustomFilter, FilterBuilderProp } from 'Filters/Filter';
import FilterBuilderModalContent from './Builder/FilterBuilderModalContent';
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
import { SetFilter } from './Filter';

View file

@ -4,12 +4,14 @@ import Icon, { IconKind, IconName } from 'Components/Icon';
import SpinnerButton, {
SpinnerButtonProps,
} from 'Components/Link/SpinnerButton';
import { getValidationFailures } from 'Helpers/Hooks/useApiMutation';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { icons } from 'Helpers/Props';
import { ValidationFailure } from 'typings/pending';
import { ApiError } from 'Utilities/Fetch/fetchJson';
import styles from './SpinnerErrorButton.css';
function getTestResult(error: Error | string | undefined) {
function getTestResult(error: ApiError | Error | string | undefined | null) {
if (!error) {
return {
wasSuccessful: true,
@ -18,7 +20,7 @@ function getTestResult(error: Error | string | undefined) {
};
}
if (typeof error === 'string' || error.status !== 400) {
if (typeof error === 'string') {
return {
wasSuccessful: false,
hasWarning: false,
@ -26,31 +28,63 @@ function getTestResult(error: Error | string | undefined) {
};
}
const failures = error.responseJSON as ValidationFailure[];
if ('status' in error) {
if (error.status !== 400) {
return {
wasSuccessful: false,
hasWarning: false,
hasError: true,
};
}
const { hasError, hasWarning } = failures.reduce(
(acc, failure) => {
if (failure.isWarning) {
acc.hasWarning = true;
} else {
acc.hasError = true;
}
const failures = error.responseJSON as ValidationFailure[];
return acc;
},
{ hasWarning: false, hasError: false }
);
const { hasError, hasWarning } = failures.reduce(
(acc, failure) => {
if (failure.isWarning) {
acc.hasWarning = true;
} else {
acc.hasError = true;
}
return acc;
},
{ hasWarning: false, hasError: false }
);
return {
wasSuccessful: false,
hasWarning,
hasError,
};
} else if ('statusCode' in error) {
if (error.statusCode !== 400 || error.statusBody == null) {
return {
wasSuccessful: false,
hasWarning: false,
hasError: true,
};
}
const failures = getValidationFailures(error);
return {
wasSuccessful: false,
hasWarning: failures.warnings.length > 0,
hasError: failures.errors.length > 0,
};
}
return {
wasSuccessful: false,
hasWarning,
hasError,
hasWarning: false,
hasError: true,
};
}
interface SpinnerErrorButtonProps extends SpinnerButtonProps {
isSpinning: boolean;
error?: Error | string;
error?: ApiError | Error | string | null;
children: React.ReactNode;
}

View file

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react';
import { CustomFilter, Filter } from 'App/State/AppState';
import { CustomFilter, Filter } from 'Filters/Filter';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import FilterMenuContent from './FilterMenuContent';

View file

@ -1,5 +1,5 @@
import React from 'react';
import { CustomFilter, Filter } from 'App/State/AppState';
import { CustomFilter, Filter } from 'Filters/Filter';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import FilterMenuItem from './FilterMenuItem';

View file

@ -10,7 +10,7 @@ interface ErrorPageProps {
isLocalStorageSupported: boolean;
translationsError?: Error;
seriesError?: Error;
customFiltersError?: Error;
customFiltersError: ApiError | null;
tagsError: ApiError | null;
qualityProfilesError?: Error;
uiSettingsError?: Error;

View file

@ -0,0 +1,34 @@
import ModelBase from 'App/ModelBase';
import { FilterBuilderTypes } from 'Helpers/Props/filterBuilderTypes';
import { DateFilterValue, FilterType } from 'Helpers/Props/filterTypes';
export interface FilterBuilderPropOption {
id: string;
name: string;
}
export interface FilterBuilderProp<T> {
name: string;
label: string | (() => string);
type: FilterBuilderTypes;
valueType?: string;
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
}
export interface PropertyFilter {
key: string;
value: string | string[] | number[] | boolean[] | DateFilterValue;
type: FilterType;
}
export interface Filter {
key: string;
label: string | (() => string);
filters: PropertyFilter[];
}
export interface CustomFilter extends ModelBase {
type: string;
label: string;
filters: PropertyFilter[];
}

View file

@ -0,0 +1,73 @@
import { useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { sortByProp } from 'Utilities/Array/sortByProp';
import { CustomFilter } from './Filter';
const DEFAULT_CUSTOM_FILTERS: CustomFilter[] = [];
const useCustomFilters = () => {
const result = useApiQuery<CustomFilter[]>({
path: '/customFilter',
});
return {
...result,
data: result.data ?? DEFAULT_CUSTOM_FILTERS,
};
};
export default useCustomFilters;
export const useCustomFiltersList = (type: string) => {
const { data } = useCustomFilters();
return useMemo(() => {
return data.filter((cf) => cf.type === type).sort(sortByProp('label'));
}, [data, type]);
};
export const useSaveCustomFilter = (id: number | null) => {
const queryClient = useQueryClient();
const { mutate, isPending, error, data } = useApiMutation<
CustomFilter,
Partial<CustomFilter>
>({
path: id === null ? '/customFilter' : `/customFilter/${id}`,
method: id === null ? 'POST' : 'PUT',
mutationOptions: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/customFilter'] });
},
},
});
return {
saveCustomFilter: mutate,
isSaving: isPending,
saveError: error,
newCustomFilter: data,
};
};
export const useDeleteCustomFilter = (id: number) => {
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<void, void>({
path: `/customFilter/${id}`,
method: 'DELETE',
mutationOptions: {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['/customFilter'] });
},
},
});
return {
deleteCustomFilter: mutate,
isDeleting: isPending,
deleteError: error,
};
};

View file

@ -2,6 +2,7 @@ import { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import useCustomFilters from 'Filters/useCustomFilters';
import { fetchTranslations } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchSeries } from 'Store/Actions/seriesActions';
@ -17,15 +18,16 @@ import useTags from 'Tags/useTags';
import { ApiError } from 'Utilities/Fetch/fetchJson';
const createErrorsSelector = ({
customFiltersError,
systemStatusError,
tagsError,
}: {
customFiltersError: ApiError | null;
systemStatusError: ApiError | null;
tagsError: ApiError | null;
}) =>
createSelector(
(state: AppState) => state.series.error,
(state: AppState) => state.customFilters.error,
(state: AppState) => state.settings.ui.error,
(state: AppState) => state.settings.qualityProfiles.error,
(state: AppState) => state.settings.languages.error,
@ -34,7 +36,6 @@ const createErrorsSelector = ({
(state: AppState) => state.app.translations.error,
(
seriesError,
customFiltersError,
uiSettingsError,
qualityProfilesError,
languagesError,
@ -43,8 +44,8 @@ const createErrorsSelector = ({
translationsError
) => {
const hasError = !!(
seriesError ||
customFiltersError ||
seriesError ||
uiSettingsError ||
qualityProfilesError ||
languagesError ||
@ -75,6 +76,10 @@ const createErrorsSelector = ({
const useAppPage = () => {
const dispatch = useDispatch();
const { isFetched: isCustomFiltersFetched, error: customFiltersError } =
useCustomFilters();
const { isFetched: isSystemStatusFetched, error: systemStatusError } =
useSystemStatus();
@ -83,7 +88,6 @@ const useAppPage = () => {
const isAppStatePopulated = useSelector(
(state: AppState) =>
state.series.isPopulated &&
state.customFilters.isPopulated &&
state.settings.ui.isPopulated &&
state.settings.qualityProfiles.isPopulated &&
state.settings.languages.isPopulated &&
@ -93,10 +97,13 @@ const useAppPage = () => {
);
const isPopulated =
isAppStatePopulated && isSystemStatusFetched && isTagsFetched;
isAppStatePopulated &&
isCustomFiltersFetched &&
isSystemStatusFetched &&
isTagsFetched;
const { hasError, errors } = useSelector(
createErrorsSelector({ systemStatusError, tagsError })
createErrorsSelector({ customFiltersError, systemStatusError, tagsError })
);
const isLocalStorageSupported = useMemo(() => {

View file

@ -1,6 +1,6 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { PropertyFilter } from 'App/State/AppState';
import { PropertyFilter } from 'Filters/Filter';
import { SortDirection } from 'Helpers/Props/sortDirections';
import fetchJson from 'Utilities/Fetch/fetchJson';
import getQueryPath from 'Utilities/Fetch/getQueryPath';

View file

@ -1,14 +1,13 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageMenuButton from 'Components/Menu/PageMenuButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import InteractiveSearchFilterModal from './InteractiveSearchFilterModal';
@ -25,7 +24,7 @@ interface InteractiveSearchProps {
}
function InteractiveSearch({ type, searchPayload }: InteractiveSearchProps) {
const customFilters = useSelector(createCustomFiltersSelector('releases'));
const customFilters = useCustomFiltersList('releases');
const { columns } = useReleaseOptions();
const {

View file

@ -1,10 +1,10 @@
import { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { create } from 'zustand';
import ModelBase from 'App/ModelBase';
import { Filter, FilterBuilderProp } from 'App/State/AppState';
import { FilterBuilderTag } from 'Components/Filter/Builder/FilterBuilderRowValue';
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { Filter, FilterBuilderProp } from 'Filters/Filter';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import { applySort } from 'Helpers/Hooks/useOptionsStore';
@ -15,7 +15,6 @@ import { SortDirection } from 'Helpers/Props/sortDirections';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import { AlternateTitle } from 'Series/Series';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import CustomFormat from 'typings/CustomFormat';
import Rejection from 'typings/Rejection';
import sortByProp from 'Utilities/Array/sortByProp';
@ -379,7 +378,7 @@ const DEFAULT_RELEASES: Release[] = [];
const THIRTY_MINUTES = 30 * 60 * 1000;
const useReleases = (payload: InteractiveSearchPayload) => {
const customFilters = useSelector(createCustomFiltersSelector('releases'));
const customFilters = useCustomFiltersList('releases');
const { episodeSelectedFilterKey, seasonSelectedFilterKey } =
useReleaseOptions();

View file

@ -1,6 +1,6 @@
import React from 'react';
import { CustomFilter, Filter } from 'App/State/AppState';
import FilterMenu from 'Components/Menu/FilterMenu';
import { CustomFilter, Filter } from 'Filters/Filter';
import SeriesIndexFilterModal from 'Series/Index/SeriesIndexFilterModal';
interface SeriesIndexFilterMenuProps {

View file

@ -22,6 +22,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import withScrollPosition from 'Components/withScrollPosition';
import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { DESCENDING } from 'Helpers/Props/sortDirections';
import ParseToolbarButton from 'Parse/ParseToolbarButton';
@ -83,13 +84,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
view,
}: SeriesAppState & SeriesIndexAppState & ClientSideCollectionAppState =
useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex'));
const customFilters = useCustomFiltersList('series');
const isRssSyncExecuting = useSelector(
createCommandExecutingSelector(RSS_SYNC)
);

View file

@ -1,7 +1,6 @@
import * as app from './appActions';
import * as captcha from './captchaActions';
import * as commands from './commandActions';
import * as customFilters from './customFilterActions';
import * as episodes from './episodeActions';
import * as episodeHistory from './episodeHistoryActions';
import * as episodeSelection from './episodeSelectionActions';
@ -20,7 +19,6 @@ export default [
app,
captcha,
commands,
customFilters,
episodes,
episodeHistory,
episodeSelection,

View file

@ -109,24 +109,12 @@ function sort(items, state) {
return _.orderBy(items, clauses, orders);
}
export function createCustomFiltersSelector(type, alternateType) {
return createSelector(
(state) => state.customFilters.items,
(customFilters) => {
return customFilters.filter((customFilter) => {
return customFilter.type === type || customFilter.type === alternateType;
});
}
);
}
function createClientSideCollectionSelector(section, uiSection) {
return createSelector(
(state) => _.get(state, section),
(state) => _.get(state, uiSection),
createCustomFiltersSelector(section, uiSection),
(sectionState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
(sectionState, uiSectionState = {}) => {
const state = Object.assign({}, sectionState, uiSectionState);
const filtered = filter(state.items, state);
const sorted = sort(filtered, state);
@ -134,7 +122,6 @@ function createClientSideCollectionSelector(section, uiSection) {
return {
...sectionState,
...uiSectionState,
customFilters,
items: sorted,
totalItems: state.items.length
};

View file

@ -1,6 +1,6 @@
import { keepPreviousData } from '@tanstack/react-query';
import { useMemo } from 'react';
import { Filter } from 'App/State/AppState';
import { Filter } from 'Filters/Filter';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import LogEvent from 'typings/LogEvent';

View file

@ -1,4 +1,4 @@
import { PropertyFilter } from 'App/State/AppState';
import { PropertyFilter } from 'Filters/Filter';
export interface QueryParams {
[key: string]:

View file

@ -1,6 +1,6 @@
import _ from 'lodash';
import ModelBase from 'App/ModelBase';
import { CustomFilter, Filter } from 'App/State/AppState';
import { CustomFilter, Filter } from 'Filters/Filter';
import { filterTypes, sortDirections } from 'Helpers/Props';
import { FilterType } from 'Helpers/Props/filterTypes';
import getFilterTypePredicate from 'Helpers/Props/getFilterTypePredicate';

View file

@ -1,4 +1,4 @@
import { CustomFilter, Filter } from 'App/State/AppState';
import { CustomFilter, Filter } from 'Filters/Filter';
export default function findSelectedFilters(
selectedFilterKey: string | number,

View file

@ -1,4 +1,4 @@
import { Filter } from 'App/State/AppState';
import { Filter } from 'Filters/Filter';
export default function getFilterValue<T>(
filters: Filter[],

View file

@ -8,7 +8,6 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -27,6 +26,7 @@ import TablePager from 'Components/Table/TablePager';
import Episode from 'Episode/Episode';
import { useToggleEpisodesMonitored } from 'Episode/useEpisode';
import EpisodeFileProvider from 'EpisodeFile/EpisodeFileProvider';
import { Filter } from 'Filters/Filter';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { executeCommand } from 'Store/Actions/commandActions';

View file

@ -1,8 +1,8 @@
import { keepPreviousData } from '@tanstack/react-query';
import { useEffect } from 'react';
import { Filter } from 'App/State/AppState';
import Episode from 'Episode/Episode';
import { setEpisodeQueryKey } from 'Episode/useEpisode';
import { Filter } from 'Filters/Filter';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import translate from 'Utilities/String/translate';

View file

@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import { Filter } from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -20,6 +19,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import Episode from 'Episode/Episode';
import { useToggleEpisodesMonitored } from 'Episode/useEpisode';
import { Filter } from 'Filters/Filter';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';

View file

@ -1,8 +1,8 @@
import { keepPreviousData } from '@tanstack/react-query';
import { useEffect } from 'react';
import { Filter } from 'App/State/AppState';
import Episode from 'Episode/Episode';
import { setEpisodeQueryKey } from 'Episode/useEpisode';
import { Filter } from 'Filters/Filter';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
import translate from 'Utilities/String/translate';

View file

@ -46,8 +46,10 @@ public ActionResult<CustomFilterResource> UpdateCustomFilter([FromBody] CustomFi
}
[RestDeleteById]
public void DeleteCustomResource(int id)
public ActionResult DeleteCustomResource(int id)
{
_customFilterService.Delete(id);
return NoContent();
}
}