From 645f20dc1b1f5177a0d29fdbffc10b92e85e2adf Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 23 Nov 2025 13:20:41 -0800 Subject: [PATCH] Use react-query for custom filters --- frontend/src/Activity/Blocklist/Blocklist.tsx | 4 +- .../src/Activity/Blocklist/useBlocklist.ts | 9 +-- frontend/src/Activity/History/History.tsx | 4 +- frontend/src/Activity/History/useHistory.ts | 9 +-- frontend/src/Activity/Queue/Queue.tsx | 4 +- frontend/src/Activity/Queue/useQueue.ts | 9 +-- frontend/src/App/State/AppSectionState.ts | 2 +- frontend/src/App/State/AppState.ts | 37 ---------- .../App/State/ClientSideCollectionAppState.ts | 3 - .../src/App/State/CustomFiltersAppState.ts | 12 --- frontend/src/App/State/SeriesAppState.ts | 2 +- frontend/src/Calendar/CalendarPage.tsx | 4 +- frontend/src/Calendar/useCalendar.ts | 7 +- .../Builder/DefaultFilterBuilderRowValue.tsx | 2 +- .../Builder/FilterBuilderModalContent.tsx | 26 +++---- .../Filter/Builder/FilterBuilderRow.tsx | 2 +- .../Filter/Builder/FilterBuilderRowValue.tsx | 2 +- .../Filter/CustomFilters/CustomFilter.tsx | 13 ++-- .../CustomFiltersModalContent.tsx | 14 +--- .../src/Components/Filter/FilterModal.tsx | 2 +- .../Components/Link/SpinnerErrorButton.tsx | 68 ++++++++++++----- frontend/src/Components/Menu/FilterMenu.tsx | 2 +- .../src/Components/Menu/FilterMenuContent.tsx | 2 +- frontend/src/Components/Page/ErrorPage.tsx | 2 +- frontend/src/Filters/Filter.ts | 34 +++++++++ frontend/src/Filters/useCustomFilters.ts | 73 +++++++++++++++++++ frontend/src/Helpers/Hooks/useAppPage.ts | 19 +++-- .../src/Helpers/Hooks/usePagedApiQuery.ts | 2 +- .../InteractiveSearch/InteractiveSearch.tsx | 5 +- frontend/src/InteractiveSearch/useReleases.ts | 7 +- .../Index/Menus/SeriesIndexFilterMenu.tsx | 2 +- frontend/src/Series/Index/SeriesIndex.tsx | 4 +- frontend/src/Store/Actions/index.js | 2 - .../createClientSideCollectionSelector.js | 17 +---- frontend/src/System/Events/useEvents.ts | 2 +- .../src/Utilities/Fetch/getQueryString.ts | 2 +- .../Filter/clientSideFilterAndSort.ts | 2 +- .../Utilities/Filter/findSelectedFilters.ts | 2 +- .../src/Utilities/Filter/getFilterValue.ts | 2 +- .../src/Wanted/CutoffUnmet/CutoffUnmet.tsx | 2 +- .../src/Wanted/CutoffUnmet/useCutoffUnmet.tsx | 2 +- frontend/src/Wanted/Missing/Missing.tsx | 2 +- frontend/src/Wanted/Missing/useMissing.tsx | 2 +- .../CustomFilters/CustomFilterController.cs | 4 +- 44 files changed, 245 insertions(+), 183 deletions(-) delete mode 100644 frontend/src/App/State/CustomFiltersAppState.ts create mode 100644 frontend/src/Filters/Filter.ts create mode 100644 frontend/src/Filters/useCustomFilters.ts diff --git a/frontend/src/Activity/Blocklist/Blocklist.tsx b/frontend/src/Activity/Blocklist/Blocklist.tsx index 7528b66a4..d8e5cb6dc 100644 --- a/frontend/src/Activity/Blocklist/Blocklist.tsx +++ b/frontend/src/Activity/Blocklist/Blocklist.tsx @@ -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) ); diff --git a/frontend/src/Activity/Blocklist/useBlocklist.ts b/frontend/src/Activity/Blocklist/useBlocklist.ts index ad093706c..cc9b9431a 100644 --- a/frontend/src/Activity/Blocklist/useBlocklist.ts +++ b/frontend/src/Activity/Blocklist/useBlocklist.ts @@ -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); diff --git a/frontend/src/Activity/History/History.tsx b/frontend/src/Activity/History/History.tsx index 751f4a17f..e26882173 100644 --- a/frontend/src/Activity/History/History.tsx +++ b/frontend/src/Activity/History/History.tsx @@ -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; diff --git a/frontend/src/Activity/History/useHistory.ts b/frontend/src/Activity/History/useHistory.ts index 94be4913d..251673a10 100644 --- a/frontend/src/Activity/History/useHistory.ts +++ b/frontend/src/Activity/History/useHistory.ts @@ -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); diff --git a/frontend/src/Activity/Queue/Queue.tsx b/frontend/src/Activity/Queue/Queue.tsx index dc5a5a057..3d967f3fd 100644 --- a/frontend/src/Activity/Queue/Queue.tsx +++ b/frontend/src/Activity/Queue/Queue.tsx @@ -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) diff --git a/frontend/src/Activity/Queue/useQueue.ts b/frontend/src/Activity/Queue/useQueue.ts index f2183d590..620181f97 100644 --- a/frontend/src/Activity/Queue/useQueue.ts +++ b/frontend/src/Activity/Queue/useQueue.ts @@ -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); diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 8f8b8ba0e..73d3c511e 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -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; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index badce70dd..5db9953ec 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -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 { - 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; diff --git a/frontend/src/App/State/ClientSideCollectionAppState.ts b/frontend/src/App/State/ClientSideCollectionAppState.ts index f4110ef73..411efcda2 100644 --- a/frontend/src/App/State/ClientSideCollectionAppState.ts +++ b/frontend/src/App/State/ClientSideCollectionAppState.ts @@ -1,8 +1,5 @@ -import { CustomFilter } from './AppState'; - interface ClientSideCollectionAppState { totalItems: number; - customFilters: CustomFilter[]; } export default ClientSideCollectionAppState; diff --git a/frontend/src/App/State/CustomFiltersAppState.ts b/frontend/src/App/State/CustomFiltersAppState.ts deleted file mode 100644 index abe2bb371..000000000 --- a/frontend/src/App/State/CustomFiltersAppState.ts +++ /dev/null @@ -1,12 +0,0 @@ -import AppSectionState, { - AppSectionDeleteState, - AppSectionSaveState, -} from 'App/State/AppSectionState'; -import { CustomFilter } from './AppState'; - -interface CustomFiltersAppState - extends AppSectionState, - AppSectionDeleteState, - AppSectionSaveState {} - -export default CustomFiltersAppState; diff --git a/frontend/src/App/State/SeriesAppState.ts b/frontend/src/App/State/SeriesAppState.ts index 5da5987dd..377352c59 100644 --- a/frontend/src/App/State/SeriesAppState.ts +++ b/frontend/src/App/State/SeriesAppState.ts @@ -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; diff --git a/frontend/src/Calendar/CalendarPage.tsx b/frontend/src/Calendar/CalendarPage.tsx index 512a2be66..56da1aef5 100644 --- a/frontend/src/Calendar/CalendarPage.tsx +++ b/frontend/src/Calendar/CalendarPage.tsx @@ -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(); diff --git a/frontend/src/Calendar/useCalendar.ts b/frontend/src/Calendar/useCalendar.ts index bb0af5db0..5190feeca 100644 --- a/frontend/src/Calendar/useCalendar.ts +++ b/frontend/src/Calendar/useCalendar.ts @@ -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); diff --git a/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx index e2e981721..a8563eda0 100644 --- a/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/DefaultFilterBuilderRowValue.tsx @@ -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'; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx index 752c15ef7..2e98c256a 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx +++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.tsx @@ -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({ onCancelPress, onModalClose, }: FilterBuilderModalContentProps) { - 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({ 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({ }, [ id, customFilters, + newCustomFilter, isSaving, wasSaving, saveError, diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx index 353a109e3..26297e397 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.tsx @@ -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, diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx index e39bbe749..7ead03bca 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.tsx @@ -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, diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx b/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx index ae3644f18..c01686381 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx +++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.tsx @@ -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) { diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx index 29fa256be..41e899d90 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.tsx @@ -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 ( {translate('CustomFilters')} - {customFilters.sort(sortByProp('label')).map((customFilter) => { + {customFilters.map((customFilter) => { return ( diff --git a/frontend/src/Components/Filter/FilterModal.tsx b/frontend/src/Components/Filter/FilterModal.tsx index 69038a552..3c67055c5 100644 --- a/frontend/src/Components/Filter/FilterModal.tsx +++ b/frontend/src/Components/Filter/FilterModal.tsx @@ -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'; diff --git a/frontend/src/Components/Link/SpinnerErrorButton.tsx b/frontend/src/Components/Link/SpinnerErrorButton.tsx index 221a12b61..57c043389 100644 --- a/frontend/src/Components/Link/SpinnerErrorButton.tsx +++ b/frontend/src/Components/Link/SpinnerErrorButton.tsx @@ -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; } diff --git a/frontend/src/Components/Menu/FilterMenu.tsx b/frontend/src/Components/Menu/FilterMenu.tsx index cbf4f1d3f..d4e3c7178 100644 --- a/frontend/src/Components/Menu/FilterMenu.tsx +++ b/frontend/src/Components/Menu/FilterMenu.tsx @@ -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'; diff --git a/frontend/src/Components/Menu/FilterMenuContent.tsx b/frontend/src/Components/Menu/FilterMenuContent.tsx index 2f0783774..e6a7e21e9 100644 --- a/frontend/src/Components/Menu/FilterMenuContent.tsx +++ b/frontend/src/Components/Menu/FilterMenuContent.tsx @@ -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'; diff --git a/frontend/src/Components/Page/ErrorPage.tsx b/frontend/src/Components/Page/ErrorPage.tsx index 15c7afd98..6364a364b 100644 --- a/frontend/src/Components/Page/ErrorPage.tsx +++ b/frontend/src/Components/Page/ErrorPage.tsx @@ -10,7 +10,7 @@ interface ErrorPageProps { isLocalStorageSupported: boolean; translationsError?: Error; seriesError?: Error; - customFiltersError?: Error; + customFiltersError: ApiError | null; tagsError: ApiError | null; qualityProfilesError?: Error; uiSettingsError?: Error; diff --git a/frontend/src/Filters/Filter.ts b/frontend/src/Filters/Filter.ts new file mode 100644 index 000000000..b3653f1d0 --- /dev/null +++ b/frontend/src/Filters/Filter.ts @@ -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 { + 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[]; +} diff --git a/frontend/src/Filters/useCustomFilters.ts b/frontend/src/Filters/useCustomFilters.ts new file mode 100644 index 000000000..b55512410 --- /dev/null +++ b/frontend/src/Filters/useCustomFilters.ts @@ -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({ + 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 + >({ + 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({ + path: `/customFilter/${id}`, + method: 'DELETE', + mutationOptions: { + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/customFilter'] }); + }, + }, + }); + + return { + deleteCustomFilter: mutate, + isDeleting: isPending, + deleteError: error, + }; +}; diff --git a/frontend/src/Helpers/Hooks/useAppPage.ts b/frontend/src/Helpers/Hooks/useAppPage.ts index 4b27d38e1..b418c3c9e 100644 --- a/frontend/src/Helpers/Hooks/useAppPage.ts +++ b/frontend/src/Helpers/Hooks/useAppPage.ts @@ -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(() => { diff --git a/frontend/src/Helpers/Hooks/usePagedApiQuery.ts b/frontend/src/Helpers/Hooks/usePagedApiQuery.ts index d243832db..b3584986c 100644 --- a/frontend/src/Helpers/Hooks/usePagedApiQuery.ts +++ b/frontend/src/Helpers/Hooks/usePagedApiQuery.ts @@ -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'; diff --git a/frontend/src/InteractiveSearch/InteractiveSearch.tsx b/frontend/src/InteractiveSearch/InteractiveSearch.tsx index 0256a08e9..a9abc8a27 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearch.tsx +++ b/frontend/src/InteractiveSearch/InteractiveSearch.tsx @@ -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 { diff --git a/frontend/src/InteractiveSearch/useReleases.ts b/frontend/src/InteractiveSearch/useReleases.ts index 5b5ddf660..7b10a0399 100644 --- a/frontend/src/InteractiveSearch/useReleases.ts +++ b/frontend/src/InteractiveSearch/useReleases.ts @@ -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(); diff --git a/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx b/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx index 0ef73cbff..a08c4fc8e 100644 --- a/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx +++ b/frontend/src/Series/Index/Menus/SeriesIndexFilterMenu.tsx @@ -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 { diff --git a/frontend/src/Series/Index/SeriesIndex.tsx b/frontend/src/Series/Index/SeriesIndex.tsx index 5895c4ff8..7a4d9d49f 100644 --- a/frontend/src/Series/Index/SeriesIndex.tsx +++ b/frontend/src/Series/Index/SeriesIndex.tsx @@ -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) ); diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js index 4d1db0d50..5c9c44385 100644 --- a/frontend/src/Store/Actions/index.js +++ b/frontend/src/Store/Actions/index.js @@ -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, diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js index bb7ec293f..0cf1c3d32 100644 --- a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -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 }; diff --git a/frontend/src/System/Events/useEvents.ts b/frontend/src/System/Events/useEvents.ts index e5ed75444..27b460635 100644 --- a/frontend/src/System/Events/useEvents.ts +++ b/frontend/src/System/Events/useEvents.ts @@ -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'; diff --git a/frontend/src/Utilities/Fetch/getQueryString.ts b/frontend/src/Utilities/Fetch/getQueryString.ts index 5d71347c8..1afc6419a 100644 --- a/frontend/src/Utilities/Fetch/getQueryString.ts +++ b/frontend/src/Utilities/Fetch/getQueryString.ts @@ -1,4 +1,4 @@ -import { PropertyFilter } from 'App/State/AppState'; +import { PropertyFilter } from 'Filters/Filter'; export interface QueryParams { [key: string]: diff --git a/frontend/src/Utilities/Filter/clientSideFilterAndSort.ts b/frontend/src/Utilities/Filter/clientSideFilterAndSort.ts index a9a700b99..1cb550437 100644 --- a/frontend/src/Utilities/Filter/clientSideFilterAndSort.ts +++ b/frontend/src/Utilities/Filter/clientSideFilterAndSort.ts @@ -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'; diff --git a/frontend/src/Utilities/Filter/findSelectedFilters.ts b/frontend/src/Utilities/Filter/findSelectedFilters.ts index 89211f628..254d0946b 100644 --- a/frontend/src/Utilities/Filter/findSelectedFilters.ts +++ b/frontend/src/Utilities/Filter/findSelectedFilters.ts @@ -1,4 +1,4 @@ -import { CustomFilter, Filter } from 'App/State/AppState'; +import { CustomFilter, Filter } from 'Filters/Filter'; export default function findSelectedFilters( selectedFilterKey: string | number, diff --git a/frontend/src/Utilities/Filter/getFilterValue.ts b/frontend/src/Utilities/Filter/getFilterValue.ts index a7de501c9..d03588bd7 100644 --- a/frontend/src/Utilities/Filter/getFilterValue.ts +++ b/frontend/src/Utilities/Filter/getFilterValue.ts @@ -1,4 +1,4 @@ -import { Filter } from 'App/State/AppState'; +import { Filter } from 'Filters/Filter'; export default function getFilterValue( filters: Filter[], diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx index 7e4c7b832..3d94b884b 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx @@ -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'; diff --git a/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx b/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx index 5e4c8ffbc..18926e347 100644 --- a/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx +++ b/frontend/src/Wanted/CutoffUnmet/useCutoffUnmet.tsx @@ -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'; diff --git a/frontend/src/Wanted/Missing/Missing.tsx b/frontend/src/Wanted/Missing/Missing.tsx index 252833ce5..54846ed85 100644 --- a/frontend/src/Wanted/Missing/Missing.tsx +++ b/frontend/src/Wanted/Missing/Missing.tsx @@ -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'; diff --git a/frontend/src/Wanted/Missing/useMissing.tsx b/frontend/src/Wanted/Missing/useMissing.tsx index 4c4e4dbbc..1a4b2deae 100644 --- a/frontend/src/Wanted/Missing/useMissing.tsx +++ b/frontend/src/Wanted/Missing/useMissing.tsx @@ -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'; diff --git a/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs b/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs index c5ac59bcb..61a267b68 100644 --- a/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs +++ b/src/Sonarr.Api.V5/CustomFilters/CustomFilterController.cs @@ -46,8 +46,10 @@ public ActionResult UpdateCustomFilter([FromBody] CustomFi } [RestDeleteById] - public void DeleteCustomResource(int id) + public ActionResult DeleteCustomResource(int id) { _customFilterService.Delete(id); + + return NoContent(); } }