diff --git a/frontend/src/Activity/Queue/Queue.tsx b/frontend/src/Activity/Queue/Queue.tsx index 8ee085568..833c1bb4d 100644 --- a/frontend/src/Activity/Queue/Queue.tsx +++ b/frontend/src/Activity/Queue/Queue.tsx @@ -7,6 +7,7 @@ import React, { useState, } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; @@ -22,14 +23,13 @@ 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 useSelectState from 'Helpers/Hooks/useSelectState'; import { align, icons, kinds } from 'Helpers/Props'; 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 { SelectStateInputProps } from 'typings/props'; +import QueueModel from 'typings/Queue'; import { TableOptionsChangePayload } from 'typings/Table'; import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; import { @@ -37,7 +37,6 @@ import { unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import QueueFilterModal from './QueueFilterModal'; import QueueOptions from './QueueOptions'; import { @@ -54,7 +53,7 @@ import useQueue, { useRemoveQueueItems, } from './useQueue'; -function Queue() { +function QueueContent() { const dispatch = useDispatch(); const { @@ -89,13 +88,10 @@ function Queue() { const shouldBlockRefresh = useRef(false); const currentQueue = useRef(null); - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); + const { allSelected, allUnselected, selectAll, unselectAll, useSelectedIds } = + useSelect(); + const selectedIds = useSelectedIds(); const isPendingSelected = useMemo(() => { return records.some((item) => { return selectedIds.indexOf(item.id) > -1 && item.status === 'delay'; @@ -120,25 +116,13 @@ function Queue() { const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ - type: value ? 'selectAll' : 'unselectAll', - items: records, - }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [records, setSelectState] - ); - - const handleSelectedChange = useCallback( - ({ id, value, shiftKey = false }: SelectStateInputProps) => { - setSelectState({ - type: 'toggleSelected', - items: records, - id, - isSelected: value, - shiftKey, - }); - }, - [records, setSelectState] + [selectAll, unselectAll] ); const handleRefreshPress = useCallback(() => { @@ -254,10 +238,8 @@ function Queue() { return ( { + selectedIds.every((id: number) => { const item = records.find((i) => i.id === id); return !!(item && item.downloadClientHasPostImportCategory); @@ -350,7 +332,7 @@ function Queue() { } canIgnore={ isConfirmRemoveModalOpen && - selectedIds.every((id) => { + selectedIds.every((id: number) => { const item = records.find((i) => i.id === id); return !!(item && item.seriesId && item.episodeId); @@ -358,7 +340,7 @@ function Queue() { } isPending={ isConfirmRemoveModalOpen && - selectedIds.every((id) => { + selectedIds.every((id: number) => { const item = records.find((i) => i.id === id); if (!item) { @@ -378,4 +360,14 @@ function Queue() { ); } +function Queue() { + const { records } = useQueue(); + + return ( + items={records}> + + + ); +} + export default Queue; diff --git a/frontend/src/Activity/Queue/QueueRow.tsx b/frontend/src/Activity/Queue/QueueRow.tsx index 6ee43add1..edd1d7c19 100644 --- a/frontend/src/Activity/Queue/QueueRow.tsx +++ b/frontend/src/Activity/Queue/QueueRow.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useSelector } from 'react-redux'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; +import { useSelect } from 'App/Select/SelectContext'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import ProgressBar from 'Components/ProgressBar'; @@ -24,7 +25,7 @@ import useSeries from 'Series/useSeries'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import CustomFormat from 'typings/CustomFormat'; import { SelectStateInputProps } from 'typings/props'; -import { +import Queue, { QueueTrackedDownloadState, QueueTrackedDownloadStatus, StatusMessage, @@ -68,9 +69,7 @@ interface QueueRowProps { size: number; sizeLeft: number; isRemoving?: boolean; - isSelected?: boolean; columns: Column[]; - onSelectedChange: (options: SelectStateInputProps) => void; onQueueRowModalOpenOrClose: (isOpen: boolean) => void; } @@ -102,9 +101,7 @@ function QueueRow(props: QueueRowProps) { timeLeft, size, sizeLeft, - isSelected, columns, - onSelectedChange, onQueueRowModalOpenOrClose, } = props; @@ -115,6 +112,8 @@ function QueueRow(props: QueueRowProps) { ); const { removeQueueItem, isRemoving } = useRemoveQueueItem(id); const { grabQueueItem, isGrabbing, grabError } = useGrabQueueItem(id); + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] = useState(false); @@ -156,6 +155,17 @@ function QueueRow(props: QueueRowProps) { setIsRemoveQueueItemModalOpen(false); }, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]); + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, + }); + }, + [toggleSelected] + ); + const progress = 100 - (sizeLeft / size) * 100; const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning'; @@ -167,7 +177,7 @@ function QueueRow(props: QueueRowProps) { {columns.map((column) => { diff --git a/frontend/src/Helpers/Hooks/useSelectState.tsx b/frontend/src/Helpers/Hooks/useSelectState.tsx deleted file mode 100644 index 9593a7e04..000000000 --- a/frontend/src/Helpers/Hooks/useSelectState.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { cloneDeep } from 'lodash'; -import { useReducer } from 'react'; -import areAllSelected from 'Utilities/Table/areAllSelected'; -import selectAll from 'Utilities/Table/selectAll'; -import toggleSelected from 'Utilities/Table/toggleSelected'; - -export type SelectedState = Record; - -export interface SelectStateModel { - id: number | string; -} - -export interface SelectState { - selectedState: SelectedState; - lastToggled: number | string | null; - allSelected: boolean; - allUnselected: boolean; -} - -export type SelectAction = - | { type: 'reset' } - | { type: 'selectAll'; items: SelectStateModel[] } - | { type: 'unselectAll'; items: SelectStateModel[] } - | { - type: 'toggleSelected'; - id: number | string; - isSelected: boolean | null; - shiftKey: boolean; - items: SelectStateModel[]; - } - | { - type: 'removeItem'; - id: number | string; - } - | { - type: 'updateItems'; - items: SelectStateModel[]; - }; - -export type Dispatch = (action: SelectAction) => void; - -const initialState = { - selectedState: {}, - lastToggled: null, - allSelected: false, - allUnselected: true, - items: [], -}; - -function getSelectedState( - items: SelectStateModel[], - existingState: SelectedState -) { - return items.reduce((acc: SelectedState, item) => { - const id = item.id; - - acc[id] = existingState[id] ?? false; - - return acc; - }, {}); -} - -function selectReducer(state: SelectState, action: SelectAction): SelectState { - const { selectedState } = state; - - switch (action.type) { - case 'reset': { - return cloneDeep(initialState); - } - case 'selectAll': { - return { - ...selectAll(selectedState, true), - }; - } - case 'unselectAll': { - return { - ...selectAll(selectedState, false), - }; - } - case 'toggleSelected': { - const result = { - ...toggleSelected( - state, - action.items, - action.id, - action.isSelected, - action.shiftKey - ), - }; - - return result; - } - case 'updateItems': { - const nextSelectedState = getSelectedState(action.items, selectedState); - - return { - ...state, - ...areAllSelected(nextSelectedState), - selectedState: nextSelectedState, - }; - } - default: { - throw new Error(`Unhandled action type: ${action.type}`); - } - } -} - -export default function useSelectState(): [SelectState, Dispatch] { - const selectedState = getSelectedState([], {}); - - const [state, dispatch] = useReducer(selectReducer, { - selectedState, - lastToggled: null, - allSelected: false, - allUnselected: true, - }); - - return [state, dispatch]; -} diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx index 1e5d45a09..73c4a8b61 100644 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import EpisodesAppState from 'App/State/EpisodesAppState'; import TextInput from 'Components/Form/TextInput'; import Button from 'Components/Link/Button'; @@ -13,7 +14,6 @@ import Scroller from 'Components/Scroller/Scroller'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import Episode from 'Episode/Episode'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds, scrollDirections } from 'Helpers/Props'; import { SortDirection } from 'Helpers/Props/sortDirections'; import { @@ -23,10 +23,8 @@ import { } from 'Store/Actions/episodeSelectionActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { CheckInputChanged, InputChanged } from 'typings/inputs'; -import { SelectStateInputProps } from 'typings/props'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import SelectEpisodeRow from './SelectEpisodeRow'; import styles from './SelectEpisodeModalContent.css'; @@ -74,7 +72,20 @@ interface SelectEpisodeModalContentProps { onModalClose(): unknown; } -function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { +interface SelectEpisodeModalContentInnerProps { + selectedIds: number[] | string[]; + seriesId?: number; + seasonNumber?: number; + selectedDetails?: string; + isAnime: boolean; + modalTitle: string; + onEpisodesSelect(selectedEpisodes: SelectedEpisode[]): unknown; + onModalClose(): unknown; +} + +function SelectEpisodeModalContentInner( + props: SelectEpisodeModalContentInnerProps +) { const { selectedIds, seriesId, @@ -87,17 +98,22 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { } = props; const [filter, setFilter] = useState(''); - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; const { isFetching, isPopulated, items, error, sortKey, sortDirection } = useSelector(episodesSelector()); const dispatch = useDispatch(); + const { + allSelected, + allUnselected, + selectedCount: selectedEpisodesCount, + getSelectedIds, + selectAll, + unselectAll, + } = useSelect(); const filterEpisodeNumber = parseInt(filter); const errorMessage = getErrorMessage(error, translate('EpisodesLoadError')); const selectedCount = selectedIds.length; - const selectedEpisodesCount = getSelectedIds(selectedState).length; const selectionIsValid = selectedEpisodesCount > 0 && selectedEpisodesCount % selectedCount === 0; @@ -110,22 +126,13 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { const onSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }: SelectStateInputProps) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const onSortPress = useCallback( @@ -141,7 +148,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { ); const onEpisodesSelectWrapper = useCallback(() => { - const episodeIds: number[] = getSelectedIds(selectedState); + const episodeIds: number[] = getSelectedIds(); const selectedEpisodes = items.reduce((acc: Episode[], item) => { if (episodeIds.indexOf(item.id) > -1) { @@ -170,7 +177,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { }); onEpisodesSelect(mappedEpisodes); - }, [selectedIds, items, selectedState, onEpisodesSelect]); + }, [selectedIds, items, getSelectedIds, onEpisodesSelect]); useEffect( () => { @@ -240,8 +247,6 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { title={item.title} airDate={item.airDate} isAnime={isAnime} - isSelected={selectedState[item.id]} - onSelectedChange={onSelectedChange} /> ) : null; })} @@ -274,4 +279,14 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { ); } +function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) { + const { items } = useSelector(episodesSelector()); + + return ( + + + + ); +} + export default SelectEpisodeModalContent; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js b/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js deleted file mode 100644 index b94e9adf4..000000000 --- a/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js +++ /dev/null @@ -1,72 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TableRowCell from 'Components/Table/Cells/TableRowCell'; -import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; -import TableRowButton from 'Components/Table/TableRowButton'; - -class SelectEpisodeRow extends Component { - - // - // Listeners - - onPress = () => { - const { - id, - isSelected - } = this.props; - - this.props.onSelectedChange({ id, value: !isSelected }); - }; - - // - // Render - - render() { - const { - id, - episodeNumber, - absoluteEpisodeNumber, - title, - airDate, - isAnime, - isSelected, - onSelectedChange - } = this.props; - - return ( - - - - - {episodeNumber} - {isAnime ? ` (${absoluteEpisodeNumber})` : ''} - - - - {title} - - - - {airDate} - - - ); - } -} - -SelectEpisodeRow.propTypes = { - id: PropTypes.number.isRequired, - episodeNumber: PropTypes.number.isRequired, - absoluteEpisodeNumber: PropTypes.number.isRequired, - title: PropTypes.string.isRequired, - airDate: PropTypes.string.isRequired, - isAnime: PropTypes.bool.isRequired, - isSelected: PropTypes.bool, - onSelectedChange: PropTypes.func.isRequired -}; - -export default SelectEpisodeRow; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.tsx b/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.tsx new file mode 100644 index 000000000..f4b4e13ab --- /dev/null +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.tsx @@ -0,0 +1,65 @@ +import React, { useCallback } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import TableRowButton from 'Components/Table/TableRowButton'; +import Episode from 'Episode/Episode'; +import { SelectStateInputProps } from 'typings/props'; + +interface SelectEpisodeRowProps { + id: number; + episodeNumber: number; + absoluteEpisodeNumber: number | undefined; + title: string; + airDate: string; + isAnime: boolean; + isSelected?: boolean; +} + +function SelectEpisodeRow({ + id, + episodeNumber, + absoluteEpisodeNumber, + title, + airDate, + isAnime, +}: SelectEpisodeRowProps) { + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, + }); + }, + [toggleSelected] + ); + + const handlePress = useCallback(() => { + handleSelectedChange({ id, value: !isSelected, shiftKey: false }); + }, [id, isSelected, handleSelectedChange]); + + return ( + + + + + {episodeNumber} + {isAnime ? ` (${absoluteEpisodeNumber})` : ''} + + + {title} + + {airDate} + + ); +} + +export default SelectEpisodeRow; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx index f9b231e1d..5ca735c99 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.tsx @@ -2,6 +2,7 @@ import { cloneDeep, without } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import AppState from 'App/State/AppState'; import InteractiveImportAppState from 'App/State/InteractiveImportAppState'; import * as commandNames from 'Commands/commandNames'; @@ -24,7 +25,6 @@ import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import { EpisodeFile } from 'EpisodeFile/EpisodeFile'; import usePrevious from 'Helpers/Hooks/usePrevious'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { align, icons, kinds, scrollDirections } from 'Helpers/Props'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; @@ -62,7 +62,6 @@ import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import InteractiveImportRow from './InteractiveImportRow'; import styles from './InteractiveImportModalContent.css'; @@ -238,7 +237,7 @@ export interface InteractiveImportModalContentProps { onModalClose(): void; } -function InteractiveImportModalContent( +function InteractiveImportModalContentInner( props: InteractiveImportModalContentProps ) { const { @@ -286,11 +285,18 @@ function InteractiveImportModalContent( const [filterExistingFiles, setFilterExistingFiles] = useState(false); const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] = useState(null); - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; const previousIsDeleting = usePrevious(isDeleting); const dispatch = useDispatch(); + const { + allSelected, + allUnselected, + selectAll, + unselectAll, + toggleSelected, + useSelectedIds, + } = useSelect(); + const columns: Column[] = useMemo(() => { const result: Column[] = cloneDeep(COLUMNS); @@ -315,9 +321,7 @@ function InteractiveImportModalContent( return result; }, [showSeries, items]); - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); + const selectedIds = useSelectedIds(); const bulkSelectOptions = useMemo(() => { const { seasonSelectDisabled, episodeSelectDisabled } = items.reduce( @@ -432,16 +436,18 @@ function InteractiveImportModalContent( const onSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] + [selectAll, unselectAll] ); const onSelectedChange = useCallback( ({ id, value, hasEpisodeFileId, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, + toggleSelected({ id, isSelected: value, shiftKey, @@ -454,10 +460,9 @@ function InteractiveImportModalContent( ); }, [ - items, withoutEpisodeFileIdRowsSelected, - setSelectState, setWithoutEpisodeFileIdRowsSelected, + toggleSelected, ] ); @@ -893,7 +898,6 @@ function InteractiveImportModalContent( return ( items={items}> + + + ); +} + export default InteractiveImportModalContent; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx index de2514a59..b50f2ab00 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { useSelect } from 'App/Select/SelectContext'; import Icon from 'Components/Icon'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -18,6 +19,7 @@ import { icons, kinds, tooltipPositions } from 'Helpers/Props'; import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent'; import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexerFlagsModal'; +import InteractiveImport from 'InteractiveImport/InteractiveImport'; import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal'; import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal'; @@ -74,7 +76,6 @@ interface InteractiveImportRowProps { columns: Column[]; episodeFileId?: number; isReprocessing?: boolean; - isSelected?: boolean; modalTitle: string; onSelectedChange(result: SelectedChangeProps): void; onValidRowChange(id: number, isValid: boolean): void; @@ -98,7 +99,6 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { indexerFlags, rejections, isReprocessing, - isSelected, modalTitle, episodeFileId, columns, @@ -107,6 +107,8 @@ function InteractiveImportRow(props: InteractiveImportRowProps) { } = props; const dispatch = useDispatch(); + const { useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); const isSeriesColumnVisible = useMemo( () => columns.find((c) => c.name === 'series')?.isVisible ?? false, diff --git a/frontend/src/Organize/OrganizePreviewModalContent.tsx b/frontend/src/Organize/OrganizePreviewModalContent.tsx index 0a2684a0e..374f123f7 100644 --- a/frontend/src/Organize/OrganizePreviewModalContent.tsx +++ b/frontend/src/Organize/OrganizePreviewModalContent.tsx @@ -1,6 +1,8 @@ import React, { useCallback, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import AppState from 'App/State/AppState'; +import { OrganizePreviewModel } from 'App/State/OrganizePreviewAppState'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; import CheckInput from 'Components/Form/CheckInput'; @@ -11,7 +13,6 @@ 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 useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; import formatSeason from 'Season/formatSeason'; import useSeries from 'Series/useSeries'; @@ -19,9 +20,7 @@ import { executeCommand } from 'Store/Actions/commandActions'; import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions'; import { fetchNamingSettings } from 'Store/Actions/settingsActions'; import { CheckInputChanged } from 'typings/inputs'; -import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import OrganizePreviewRow from './OrganizePreviewRow'; import styles from './OrganizePreviewModalContent.css'; @@ -41,7 +40,7 @@ export interface OrganizePreviewModalContentProps { onModalClose: () => void; } -function OrganizePreviewModalContent({ +function OrganizePreviewModalContentInner({ seriesId, seasonNumber, onModalClose, @@ -62,9 +61,10 @@ function OrganizePreviewModalContent({ } = useSelector((state: AppState) => state.settings.naming); const series = useSeries(seriesId)!; - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; + const { allSelected, allUnselected, getSelectedIds, selectAll, unselectAll } = + useSelect(); + const isFetching = isPreviewFetching || isNamingFetching; const isPopulated = isPreviewPopulated && isNamingPopulated; const error = previewError || namingError; @@ -75,26 +75,17 @@ function OrganizePreviewModalContent({ const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const handleSelectedChange = useCallback( - ({ id, value, shiftKey = false }: SelectStateInputProps) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const handleOrganizePress = useCallback(() => { - const files = getSelectedIds(selectedState); + const files = getSelectedIds(); dispatch( executeCommand({ @@ -105,7 +96,7 @@ function OrganizePreviewModalContent({ ); onModalClose(); - }, [seriesId, selectedState, dispatch, onModalClose]); + }, [seriesId, getSelectedIds, dispatch, onModalClose]); useEffect(() => { dispatch(fetchOrganizePreview({ seriesId, seasonNumber })); @@ -167,8 +158,6 @@ function OrganizePreviewModalContent({ id={item.episodeFileId} existingPath={item.existingPath} newPath={item.newPath} - isSelected={selectedState[item.episodeFileId]} - onSelectedChange={handleSelectedChange} /> ); })} @@ -198,4 +187,22 @@ function OrganizePreviewModalContent({ ); } +function OrganizePreviewModalContent({ + seriesId, + seasonNumber, + onModalClose, +}: OrganizePreviewModalContentProps) { + const { items } = useSelector((state: AppState) => state.organizePreview); + + return ( + items={items}> + + + ); +} + export default OrganizePreviewModalContent; diff --git a/frontend/src/Organize/OrganizePreviewRow.tsx b/frontend/src/Organize/OrganizePreviewRow.tsx index 398ea31ea..9428d9829 100644 --- a/frontend/src/Organize/OrganizePreviewRow.tsx +++ b/frontend/src/Organize/OrganizePreviewRow.tsx @@ -1,36 +1,44 @@ import React, { useCallback, useEffect } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; +import { OrganizePreviewModel } from 'App/State/OrganizePreviewAppState'; import CheckInput from 'Components/Form/CheckInput'; import Icon from 'Components/Icon'; import { icons, kinds } from 'Helpers/Props'; import { CheckInputChanged } from 'typings/inputs'; -import { SelectStateInputProps } from 'typings/props'; import styles from './OrganizePreviewRow.css'; interface OrganizePreviewRowProps { id: number; existingPath: string; newPath: string; - isSelected?: boolean; - onSelectedChange: (props: SelectStateInputProps) => void; } function OrganizePreviewRow({ id, existingPath, newPath, - isSelected, - onSelectedChange, }: OrganizePreviewRowProps) { + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + const handleSelectedChange = useCallback( ({ value, shiftKey }: CheckInputChanged) => { - onSelectedChange({ id, value, shiftKey }); + toggleSelected({ + id, + isSelected: value, + shiftKey, + }); }, - [id, onSelectedChange] + [id, toggleSelected] ); useEffect(() => { - onSelectedChange({ id, value: true, shiftKey: false }); - }, [id, onSelectedChange]); + toggleSelected({ + id, + isSelected: true, + shiftKey: false, + }); + }, [id, toggleSelected]); return (
diff --git a/frontend/src/Series/Details/SeriesDetails.tsx b/frontend/src/Series/Details/SeriesDetails.tsx index fa9fb2554..e5e68dfdd 100644 --- a/frontend/src/Series/Details/SeriesDetails.tsx +++ b/frontend/src/Series/Details/SeriesDetails.tsx @@ -60,7 +60,6 @@ import { } from 'Utilities/pagePopulator'; import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles'; import translate from 'Utilities/String/translate'; -import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; import SeriesAlternateTitles from './SeriesAlternateTitles'; import SeriesDetailsLinks from './SeriesDetailsLinks'; @@ -302,15 +301,19 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) { }, []); const handleExpandAllPress = useCallback(() => { - const updated = selectAll( - expandedState.seasons, - !expandedState.allExpanded - ); + const expandAll = !expandedState.allExpanded; + + const newSeasons = Object.keys(expandedState.seasons).reduce< + Record + >((acc, item) => { + acc[item] = expandAll; + return acc; + }, {}); setExpandedState({ - allExpanded: updated.allSelected, - allCollapsed: updated.allUnselected, - seasons: updated.selectedState, + allExpanded: expandAll, + allCollapsed: !expandAll, + seasons: newSeasons, }); }, [expandedState]); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx index db7ca8fc9..4e8a55968 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalContent.tsx @@ -1,5 +1,6 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import { CustomFormatAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; @@ -13,7 +14,6 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; import { bulkDeleteCustomFormats, @@ -21,19 +21,14 @@ import { setManageCustomFormatsSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import CustomFormat from 'typings/CustomFormat'; import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import ManageCustomFormatsEditModal from './Edit/ManageCustomFormatsEditModal'; import ManageCustomFormatsModalRow from './ManageCustomFormatsModalRow'; import styles from './ManageCustomFormatsModalContent.css'; -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageCustomFormatsModalRow ->['onSelectedChange']; - const COLUMNS: Column[] = [ { name: 'name', @@ -58,8 +53,12 @@ interface ManageCustomFormatsModalContentProps { onModalClose(): void; } -function ManageCustomFormatsModalContent( - props: ManageCustomFormatsModalContentProps +interface ManageCustomFormatsModalContentInnerProps { + onModalClose(): void; +} + +function ManageCustomFormatsModalContentInner( + props: ManageCustomFormatsModalContentInnerProps ) { const { onModalClose } = props; @@ -80,15 +79,18 @@ function ManageCustomFormatsModalContent( const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [selectState, setSelectState] = useSelectState(); + const { + allSelected, + allUnselected, + anySelected, + selectedCount, + getSelectedIds, + selectAll, + unselectAll, + useSelectedIds, + } = useSelect(); - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; + const selectedIds = useSelectedIds(); const onSortPress = useCallback( (value: string) => { @@ -114,9 +116,9 @@ function ManageCustomFormatsModalContent( }, [setIsEditModalOpen]); const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteCustomFormats({ ids: selectedIds })); + dispatch(bulkDeleteCustomFormats({ ids: getSelectedIds() })); setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); + }, [getSelectedIds, dispatch]); const onSavePress = useCallback( (payload: object) => { @@ -124,36 +126,26 @@ function ManageCustomFormatsModalContent( dispatch( bulkEditCustomFormats({ - ids: selectedIds, + ids: getSelectedIds(), ...payload, }) ); }, - [selectedIds, dispatch] + [getSelectedIds, dispatch] ); const onSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const errorMessage = getErrorMessage(error, 'Unable to load custom formats.'); - const anySelected = selectedCount > 0; return ( @@ -184,10 +176,8 @@ function ManageCustomFormatsModalContent( return ( ); })} @@ -231,7 +221,7 @@ function ManageCustomFormatsModalContent( kind={kinds.DANGER} title={translate('DeleteSelectedCustomFormats')} message={translate('DeleteSelectedCustomFormatsMessageText', { - count: selectedIds.length, + count: selectedCount, })} confirmLabel={translate('Delete')} onConfirm={onConfirmDelete} @@ -241,4 +231,18 @@ function ManageCustomFormatsModalContent( ); } +function ManageCustomFormatsModalContent( + props: ManageCustomFormatsModalContentProps +) { + const { items }: CustomFormatAppState = useSelector( + createClientSideCollectionSelector('settings.customFormats') + ); + + return ( + + + + ); +} + export default ManageCustomFormatsModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx index db1f0cb36..cfcd461a9 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { useSelect } from 'App/Select/SelectContext'; import AppState from 'App/State/AppState'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; @@ -10,6 +11,7 @@ import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; import { icons } from 'Helpers/Props'; import { deleteCustomFormat } from 'Store/Actions/settingsActions'; +import CustomFormat from 'typings/CustomFormat'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import EditCustomFormatModal from '../EditCustomFormatModal'; @@ -20,8 +22,6 @@ interface ManageCustomFormatsModalRowProps { name: string; includeCustomFormatWhenRenaming: boolean; columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; } function isDeletingSelector() { @@ -33,15 +33,13 @@ function isDeletingSelector() { ); } -function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { - const { - id, - isSelected, - name, - includeCustomFormatWhenRenaming, - onSelectedChange, - } = props; - +function ManageCustomFormatsModalRow({ + id, + name, + includeCustomFormatWhenRenaming, +}: ManageCustomFormatsModalRowProps) { + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); const dispatch = useDispatch(); const isDeleting = useSelector(isDeletingSelector()); @@ -52,12 +50,10 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) { useState(false); const handlelectedChange = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, - }); + ({ id, value, shiftKey }: SelectStateInputProps) => { + toggleSelected({ id, isSelected: value, shiftKey }); }, - [onSelectedChange] + [toggleSelected] ); const handleEditCustomFormatModalOpen = useCallback(() => { diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx index cc6f42531..abcf9fd87 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalContent.tsx @@ -1,5 +1,6 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import { DownloadClientAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; @@ -13,7 +14,6 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; import { bulkDeleteDownloadClients, @@ -21,20 +21,15 @@ import { setManageDownloadClientsSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import DownloadClient from 'typings/DownloadClient'; import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import ManageDownloadClientsEditModal from './Edit/ManageDownloadClientsEditModal'; import ManageDownloadClientsModalRow from './ManageDownloadClientsModalRow'; import TagsModal from './Tags/TagsModal'; import styles from './ManageDownloadClientsModalContent.css'; -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageDownloadClientsModalRow ->['onSelectedChange']; - const COLUMNS: Column[] = [ { name: 'name', @@ -84,11 +79,14 @@ interface ManageDownloadClientsModalContentProps { onModalClose(): void; } -function ManageDownloadClientsModalContent( - props: ManageDownloadClientsModalContentProps -) { - const { onModalClose } = props; +interface ManageDownloadClientsModalContentInnerProps { + onModalClose: () => void; +} +function ManageDownloadClientsModalContentInner({ + onModalClose, +}: ManageDownloadClientsModalContentInnerProps) { + const dispatch = useDispatch(); const { isFetching, isPopulated, @@ -99,24 +97,29 @@ function ManageDownloadClientsModalContent( sortKey, sortDirection, }: DownloadClientAppState = useSelector( - createClientSideCollectionSelector('settings.downloadClients') + createClientSideCollectionSelector( + 'settings.downloadClients', + 'manageDownloadClients' + ) ); - const dispatch = useDispatch(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); - const [selectState, setSelectState] = useSelectState(); + const { + allSelected, + allUnselected, + anySelected, + selectedCount, + getSelectedIds, + selectAll, + unselectAll, + useSelectedIds, + } = useSelect(); - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; + const selectedIds = useSelectedIds(); const onSortPress = useCallback( (value: string) => { @@ -142,9 +145,9 @@ function ManageDownloadClientsModalContent( }, [setIsEditModalOpen]); const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteDownloadClients({ ids: selectedIds })); + dispatch(bulkDeleteDownloadClients({ ids: getSelectedIds() })); setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); + }, [getSelectedIds, dispatch]); const onSavePress = useCallback( (payload: object) => { @@ -152,12 +155,12 @@ function ManageDownloadClientsModalContent( dispatch( bulkEditDownloadClients({ - ids: selectedIds, + ids: getSelectedIds(), ...payload, }) ); }, - [selectedIds, dispatch] + [getSelectedIds, dispatch] ); const onTagsPress = useCallback(() => { @@ -175,40 +178,30 @@ function ManageDownloadClientsModalContent( dispatch( bulkEditDownloadClients({ - ids: selectedIds, + ids: getSelectedIds(), tags, applyTags, }) ); }, - [selectedIds, dispatch] + [getSelectedIds, dispatch] ); const onSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const errorMessage = getErrorMessage( error, 'Unable to load download clients.' ); - const anySelected = selectedCount > 0; return ( @@ -239,10 +232,8 @@ function ManageDownloadClientsModalContent( return ( ); })} @@ -301,7 +292,7 @@ function ManageDownloadClientsModalContent( kind={kinds.DANGER} title={translate('DeleteSelectedDownloadClients')} message={translate('DeleteSelectedDownloadClientsMessageText', { - count: selectedIds.length, + count: selectedCount, })} confirmLabel={translate('Delete')} onConfirm={onConfirmDelete} @@ -311,4 +302,21 @@ function ManageDownloadClientsModalContent( ); } +function ManageDownloadClientsModalContent({ + onModalClose, +}: ManageDownloadClientsModalContentProps) { + const { items }: DownloadClientAppState = useSelector( + createClientSideCollectionSelector( + 'settings.downloadClients', + 'manageDownloadClients' + ) + ); + + return ( + + + + ); +} + export default ManageDownloadClientsModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx index 64959765c..5cb755bc6 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx +++ b/frontend/src/Settings/DownloadClients/DownloadClients/Manage/ManageDownloadClientsModalRow.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; import Label from 'Components/Label'; import SeriesTagList from 'Components/SeriesTagList'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -6,6 +7,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; import { kinds } from 'Helpers/Props'; +import DownloadClient from 'typings/DownloadClient'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import styles from './ManageDownloadClientsModalRow.css'; @@ -20,8 +22,6 @@ interface ManageDownloadClientsModalRowProps { implementation: string; tags: number[]; columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; } function ManageDownloadClientsModalRow( @@ -29,7 +29,6 @@ function ManageDownloadClientsModalRow( ) { const { id, - isSelected, name, enable, priority, @@ -37,16 +36,20 @@ function ManageDownloadClientsModalRow( removeFailedDownloads, implementation, tags, - onSelectedChange, } = props; - const onSelectedChangeWrapper = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, }); }, - [onSelectedChange] + [toggleSelected] ); return ( @@ -54,7 +57,7 @@ function ManageDownloadClientsModalRow( {name} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx index 155d53a78..176a558a2 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionRow.tsx @@ -1,5 +1,6 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import { useSelect } from 'App/Select/SelectContext'; import IconButton from 'Components/Link/IconButton'; import ConfirmModal from 'Components/Modal/ConfirmModal'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -14,13 +15,26 @@ import translate from 'Utilities/String/translate'; import EditImportListExclusionModal from './EditImportListExclusionModal'; import styles from './ImportListExclusionRow.css'; -interface ImportListExclusionRowProps extends ImportListExclusion { - isSelected: boolean; - onSelectedChange: (options: SelectStateInputProps) => void; -} +type ImportListExclusionRowProps = ImportListExclusion; -function ImportListExclusionRow(props: ImportListExclusionRowProps) { - const { id, tvdbId, title, isSelected, onSelectedChange } = props; +function ImportListExclusionRow({ + id, + tvdbId, + title, +}: ImportListExclusionRowProps) { + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, + }); + }, + [toggleSelected] + ); const dispatch = useDispatch(); @@ -45,7 +59,7 @@ function ImportListExclusionRow(props: ImportListExclusionRowProps) { {title} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx index f8b02e427..f4fd46903 100644 --- a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.tsx @@ -1,6 +1,7 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import AppState from 'App/State/AppState'; import FieldSet from 'Components/FieldSet'; import IconButton from 'Components/Link/IconButton'; @@ -17,7 +18,6 @@ import usePaging from 'Components/Table/usePaging'; import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; import useModalOpenState from 'Helpers/Hooks/useModalOpenState'; import usePrevious from 'Helpers/Hooks/usePrevious'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { icons, kinds } from 'Helpers/Props'; import { SortDirection } from 'Helpers/Props/sortDirections'; import { @@ -28,15 +28,14 @@ import { setImportListExclusionSort, setImportListExclusionTableOption, } from 'Store/Actions/Settings/importListExclusions'; +import ImportListExclusion from 'typings/ImportListExclusion'; import { CheckInputChanged } from 'typings/inputs'; -import { SelectStateInputProps } from 'typings/props'; import { TableOptionsChangePayload } from 'typings/Table'; import { registerPagePopulator, unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import EditImportListExclusionModal from './EditImportListExclusionModal'; import ImportListExclusionRow from './ImportListExclusionRow'; import styles from './ImportListExclusions.css'; @@ -74,7 +73,7 @@ function createImportListExclusionsSelector() { ); } -function ImportListExclusions() { +function ImportListExclusionsContent() { const requestCurrentPage = useCurrentPage(); const { @@ -98,31 +97,24 @@ function ImportListExclusions() { useState(false); const previousIsDeleting = usePrevious(isDeleting); - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); + const { + allSelected, + allUnselected, + anySelected, + getSelectedIds, + selectAll, + unselectAll, + } = useSelect(); const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const handleSelectedChange = useCallback( - ({ id, value, shiftKey = false }: SelectStateInputProps) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const handleDeleteSelectedPress = useCallback(() => { @@ -130,9 +122,9 @@ function ImportListExclusions() { }, [setIsConfirmDeleteModalOpen]); const handleDeleteSelectedConfirmed = useCallback(() => { - dispatch(bulkDeleteImportListExclusions({ ids: selectedIds })); + dispatch(bulkDeleteImportListExclusions({ ids: getSelectedIds() })); setIsConfirmDeleteModalOpen(false); - }, [selectedIds, setIsConfirmDeleteModalOpen, dispatch]); + }, [getSelectedIds, setIsConfirmDeleteModalOpen, dispatch]); const handleConfirmDeleteModalClose = useCallback(() => { setIsConfirmDeleteModalOpen(false); @@ -194,7 +186,7 @@ function ImportListExclusions() { useEffect(() => { if (previousIsDeleting && !isDeleting && !deleteError) { - setSelectState({ type: 'unselectAll', items }); + unselectAll(); dispatch(fetchImportListExclusions()); } @@ -204,7 +196,7 @@ function ImportListExclusions() { deleteError, items, dispatch, - setSelectState, + unselectAll, ]); const [ @@ -238,14 +230,7 @@ function ImportListExclusions() { > {items.map((item) => { - return ( - - ); + return ; })} @@ -253,7 +238,7 @@ function ImportListExclusions() { {translate('Delete')} @@ -301,4 +286,14 @@ function ImportListExclusions() { ); } +function ImportListExclusions() { + const { items } = useSelector(createImportListExclusionsSelector()); + + return ( + + + + ); +} + export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx index cac00e259..f2eb44c58 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalContent.tsx @@ -1,5 +1,6 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import { ImportListAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; @@ -12,27 +13,21 @@ import ModalFooter from 'Components/Modal/ModalFooter'; import ModalHeader from 'Components/Modal/ModalHeader'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; import { bulkDeleteImportLists, bulkEditImportLists, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import ImportList from 'typings/ImportList'; import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import ManageImportListsEditModal from './Edit/ManageImportListsEditModal'; import ManageImportListsModalRow from './ManageImportListsModalRow'; import TagsModal from './Tags/TagsModal'; import styles from './ManageImportListsModalContent.css'; -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageImportListsModalRow ->['onSelectedChange']; - const COLUMNS = [ { name: 'name', @@ -76,8 +71,12 @@ interface ManageImportListsModalContentProps { onModalClose(): void; } -function ManageImportListsModalContent( - props: ManageImportListsModalContentProps +interface ManageImportListsModalContentInnerProps { + onModalClose(): void; +} + +function ManageImportListsModalContentInner( + props: ManageImportListsModalContentInnerProps ) { const { onModalClose } = props; @@ -98,15 +97,16 @@ function ManageImportListsModalContent( const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); - const [selectState, setSelectState] = useSelectState(); + const { + allSelected, + allUnselected, + anySelected, + selectAll, + unselectAll, + useSelectedIds, + } = useSelect(); - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; + const selectedIds = useSelectedIds(); const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); @@ -169,26 +169,16 @@ function ManageImportListsModalContent( const onSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const errorMessage = getErrorMessage(error, 'Unable to load import lists.'); - const anySelected = selectedCount > 0; return ( @@ -216,10 +206,8 @@ function ManageImportListsModalContent( return ( ); })} @@ -288,4 +276,18 @@ function ManageImportListsModalContent( ); } +function ManageImportListsModalContent( + props: ManageImportListsModalContentProps +) { + const { items }: ImportListAppState = useSelector( + createClientSideCollectionSelector('settings.importLists') + ); + + return ( + + + + ); +} + export default ManageImportListsModalContent; diff --git a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalRow.tsx b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalRow.tsx index b9d147c82..2421de7ff 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalRow.tsx +++ b/frontend/src/Settings/ImportLists/ImportLists/Manage/ManageImportListsModalRow.tsx @@ -1,11 +1,13 @@ import React, { useCallback } from 'react'; import { useSelector } from 'react-redux'; +import { useSelect } from 'App/Select/SelectContext'; import SeriesTagList from 'Components/SeriesTagList'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; import { createQualityProfileSelectorForHook } from 'Store/Selectors/createQualityProfileSelector'; +import ImportList from 'typings/ImportList'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import styles from './ManageImportListsModalRow.css'; @@ -19,34 +21,35 @@ interface ManageImportListsModalRowProps { tags: number[]; enableAutomaticAdd: boolean; columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; } function ManageImportListsModalRow(props: ManageImportListsModalRowProps) { const { id, - isSelected, name, rootFolderPath, qualityProfileId, implementation, enableAutomaticAdd, tags, - onSelectedChange, } = props; + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + const qualityProfile = useSelector( createQualityProfileSelectorForHook(qualityProfileId) ); const onSelectedChangeWrapper = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, + ({ id, value, shiftKey }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, }); }, - [onSelectedChange] + [toggleSelected] ); return ( diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx index 974408448..24ea41a28 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalContent.tsx @@ -1,5 +1,6 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import { IndexerAppState } from 'App/State/SettingsAppState'; import Alert from 'Components/Alert'; import Button from 'Components/Link/Button'; @@ -13,7 +14,6 @@ import ModalHeader from 'Components/Modal/ModalHeader'; import Column from 'Components/Table/Column'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { kinds } from 'Helpers/Props'; import { bulkDeleteIndexers, @@ -21,20 +21,15 @@ import { setManageIndexersSort, } from 'Store/Actions/settingsActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import Indexer from 'typings/Indexer'; import { CheckInputChanged } from 'typings/inputs'; import getErrorMessage from 'Utilities/Object/getErrorMessage'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import ManageIndexersEditModal from './Edit/ManageIndexersEditModal'; import ManageIndexersModalRow from './ManageIndexersModalRow'; import TagsModal from './Tags/TagsModal'; import styles from './ManageIndexersModalContent.css'; -// TODO: This feels janky to do, but not sure of a better way currently -type OnSelectedChangeCallback = React.ComponentProps< - typeof ManageIndexersModalRow ->['onSelectedChange']; - const COLUMNS: Column[] = [ { name: 'name', @@ -90,7 +85,13 @@ interface ManageIndexersModalContentProps { onModalClose(): void; } -function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { +interface ManageIndexersModalContentInnerProps { + onModalClose(): void; +} + +function ManageIndexersModalContentInner( + props: ManageIndexersModalContentInnerProps +) { const { onModalClose } = props; const { @@ -112,15 +113,15 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); - const [selectState, setSelectState] = useSelectState(); - - const { allSelected, allUnselected, selectedState } = selectState; - - const selectedIds: number[] = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - - const selectedCount = selectedIds.length; + const { + allSelected, + allUnselected, + anySelected, + getSelectedIds, + selectAll, + unselectAll, + useSelectedIds, + } = useSelect(); const onSortPress = useCallback( (value: string) => { @@ -146,9 +147,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { }, [setIsEditModalOpen]); const onConfirmDelete = useCallback(() => { - dispatch(bulkDeleteIndexers({ ids: selectedIds })); + dispatch(bulkDeleteIndexers({ ids: getSelectedIds() })); setIsDeleteModalOpen(false); - }, [selectedIds, dispatch]); + }, [getSelectedIds, dispatch]); const onSavePress = useCallback( (payload: object) => { @@ -156,12 +157,12 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { dispatch( bulkEditIndexers({ - ids: selectedIds, + ids: getSelectedIds(), ...payload, }) ); }, - [selectedIds, dispatch] + [getSelectedIds, dispatch] ); const onTagsPress = useCallback(() => { @@ -179,37 +180,28 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { dispatch( bulkEditIndexers({ - ids: selectedIds, + ids: getSelectedIds(), tags, applyTags, }) ); }, - [selectedIds, dispatch] + [getSelectedIds, dispatch] ); const onSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const onSelectedChange = useCallback( - ({ id, value, shiftKey = false }) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); + const selectedIds = useSelectedIds(); const errorMessage = getErrorMessage(error, 'Unable to load indexers.'); - const anySelected = selectedCount > 0; return ( @@ -240,10 +232,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { return ( ); })} @@ -312,4 +302,16 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { ); } +function ManageIndexersModalContent(props: ManageIndexersModalContentProps) { + const { items }: IndexerAppState = useSelector( + createClientSideCollectionSelector('settings.indexers', 'manageIndexers') + ); + + return ( + + + + ); +} + export default ManageIndexersModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx index 6c551916d..730b510cd 100644 --- a/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx +++ b/frontend/src/Settings/Indexers/Indexers/Manage/ManageIndexersModalRow.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; import Label from 'Components/Label'; import SeriesTagList from 'Components/SeriesTagList'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; @@ -6,6 +7,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; import { kinds } from 'Helpers/Props'; +import Indexer from 'typings/Indexer'; import { SelectStateInputProps } from 'typings/props'; import translate from 'Utilities/String/translate'; import styles from './ManageIndexersModalRow.css'; @@ -21,14 +23,11 @@ interface ManageIndexersModalRowProps { implementation: string; tags: number[]; columns: Column[]; - isSelected?: boolean; - onSelectedChange(result: SelectStateInputProps): void; } function ManageIndexersModalRow(props: ManageIndexersModalRowProps) { const { id, - isSelected, name, enableRss, enableAutomaticSearch, @@ -37,16 +36,20 @@ function ManageIndexersModalRow(props: ManageIndexersModalRowProps) { seasonSearchMaximumSingleEpisodeAge, implementation, tags, - onSelectedChange, } = props; + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + const onSelectedChangeWrapper = useCallback( - (result: SelectStateInputProps) => { - onSelectedChange({ - ...result, + ({ id, value, shiftKey }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, }); }, - [onSelectedChange] + [toggleSelected] ); return ( diff --git a/frontend/src/Utilities/Table/areAllSelected.ts b/frontend/src/Utilities/Table/areAllSelected.ts deleted file mode 100644 index ffb791ed1..000000000 --- a/frontend/src/Utilities/Table/areAllSelected.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SelectedState } from 'Helpers/Hooks/useSelectState'; - -export default function areAllSelected(selectedState: SelectedState) { - let allSelected = true; - let allUnselected = true; - - Object.values(selectedState).forEach((value) => { - if (value) { - allUnselected = false; - } else { - allSelected = false; - } - }); - - return { - allSelected, - allUnselected, - }; -} diff --git a/frontend/src/Utilities/Table/getSelectedIds.ts b/frontend/src/Utilities/Table/getSelectedIds.ts deleted file mode 100644 index 7c1ea7c3b..000000000 --- a/frontend/src/Utilities/Table/getSelectedIds.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { reduce } from 'lodash'; -import { SelectedState } from 'Helpers/Hooks/useSelectState'; - -function getSelectedIds( - selectedState: SelectedState, - idParser: (id: string) => T = (id) => parseInt(id) as T -): T[] { - return reduce( - selectedState, - (result: T[], value, id) => { - if (value) { - result.push(idParser(id)); - } - - return result; - }, - [] - ); -} - -export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getToggledRange.ts b/frontend/src/Utilities/Table/getToggledRange.ts index 7888a708b..4f13d7f0a 100644 --- a/frontend/src/Utilities/Table/getToggledRange.ts +++ b/frontend/src/Utilities/Table/getToggledRange.ts @@ -1,6 +1,6 @@ -import { SelectStateModel } from 'Helpers/Hooks/useSelectState'; +import { Id, SelectStoreModel } from 'App/Select/useSelectStore'; -function getToggledRange( +function getToggledRange>( items: T[], id: number | string, lastToggled: number | string diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.ts b/frontend/src/Utilities/Table/removeOldSelectedState.ts deleted file mode 100644 index 8edb9e4dc..000000000 --- a/frontend/src/Utilities/Table/removeOldSelectedState.ts +++ /dev/null @@ -1,21 +0,0 @@ -import ModelBase from 'App/ModelBase'; -import { SelectState } from 'Helpers/Hooks/useSelectState'; -import areAllSelected from './areAllSelected'; - -export default function removeOldSelectedState( - state: SelectState, - prevItems: T[] -) { - const selectedState = { - ...state.selectedState, - }; - - prevItems.forEach((item) => { - delete selectedState[item.id]; - }); - - return { - ...areAllSelected(selectedState), - selectedState, - }; -} diff --git a/frontend/src/Utilities/Table/selectAll.ts b/frontend/src/Utilities/Table/selectAll.ts deleted file mode 100644 index 8c60a3d37..000000000 --- a/frontend/src/Utilities/Table/selectAll.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { SelectedState } from 'Helpers/Hooks/useSelectState'; - -function selectAll(selectedState: SelectedState, selected: boolean) { - const newSelectedState = Object.keys(selectedState).reduce< - Record - >((acc, item) => { - acc[item] = selected; - return acc; - }, {}); - - return { - allSelected: selected, - allUnselected: !selected, - lastToggled: null, - selectedState: newSelectedState, - }; -} - -export default selectAll; diff --git a/frontend/src/Utilities/Table/toggleSelected.ts b/frontend/src/Utilities/Table/toggleSelected.ts index da75ee498..c2f39808c 100644 --- a/frontend/src/Utilities/Table/toggleSelected.ts +++ b/frontend/src/Utilities/Table/toggleSelected.ts @@ -1,7 +1,36 @@ -import { SelectState, SelectStateModel } from 'Helpers/Hooks/useSelectState'; -import areAllSelected from './areAllSelected'; import getToggledRange from './getToggledRange'; +interface SelectState { + allSelected: boolean; + allUnselected: boolean; + lastToggled: number | string | null; + selectedState: SelectedState; +} + +type SelectedState = Record; + +interface SelectStateModel { + id: number | string; +} + +function areAllSelected(selectedState: SelectedState) { + let allSelected = true; + let allUnselected = true; + + Object.values(selectedState).forEach((value) => { + if (value) { + allUnselected = false; + } else { + allSelected = false; + } + }); + + return { + allSelected, + allUnselected, + }; +} + function toggleSelected( selectState: SelectState, items: T[], diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx index cc4fab7e9..48270cfa8 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import AppState, { Filter } from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; @@ -17,8 +18,8 @@ import TableBody from 'Components/Table/TableBody'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TablePager from 'Components/Table/TablePager'; import usePaging from 'Components/Table/usePaging'; +import Episode from 'Episode/Episode'; import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { align, icons, kinds } from 'Helpers/Props'; import { executeCommand } from 'Store/Actions/commandActions'; import { @@ -32,7 +33,6 @@ import { } from 'Store/Actions/wantedActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { CheckInputChanged } from 'typings/inputs'; -import { SelectStateInputProps } from 'typings/props'; import { TableOptionsChangePayload } from 'typings/Table'; import getFilterValue from 'Utilities/Filter/getFilterValue'; import { @@ -40,7 +40,6 @@ import { unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import CutoffUnmetRow from './CutoffUnmetRow'; function getMonitoredValue( @@ -50,7 +49,7 @@ function getMonitoredValue( return !!getFilterValue(filters, selectedFilterKey, 'monitored', false); } -function CutoffUnmet() { +function CutoffUnmetContent() { const dispatch = useDispatch(); const requestCurrentPage = useCurrentPage(); @@ -77,8 +76,14 @@ function CutoffUnmet() { createCommandExecutingSelector(commandNames.EPISODE_SEARCH) ); - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; + const { + allSelected, + allUnselected, + anySelected, + getSelectedIds, + selectAll, + unselectAll, + } = useSelect(); const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] = useState(false); @@ -95,50 +100,36 @@ function CutoffUnmet() { gotoPage: gotoCutoffUnmetPage, }); - const selectedIds = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - const isSaving = useMemo(() => { return items.filter((m) => m.isSaving).length > 1; }, [items]); - const itemsSelected = !!selectedIds.length; const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey); const isSearchingForEpisodes = isSearchingForAllEpisodes || isSearchingForSelectedEpisodes; const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const handleSelectedChange = useCallback( - ({ id, value, shiftKey = false }: SelectStateInputProps) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const handleSearchSelectedPress = useCallback(() => { dispatch( executeCommand({ name: commandNames.EPISODE_SEARCH, - episodeIds: selectedIds, + episodeIds: getSelectedIds(), commandFinished: () => { dispatch(fetchCutoffUnmet()); }, }) ); - }, [selectedIds, dispatch]); + }, [getSelectedIds, dispatch]); const handleSearchAllPress = useCallback(() => { setIsConfirmSearchAllModalOpen(true); @@ -164,11 +155,11 @@ function CutoffUnmet() { const handleToggleSelectedPress = useCallback(() => { dispatch( batchToggleCutoffUnmetEpisodes({ - episodeIds: selectedIds, + episodeIds: getSelectedIds(), monitored: !isShowingMonitored, }) ); - }, [isShowingMonitored, selectedIds, dispatch]); + }, [isShowingMonitored, getSelectedIds, dispatch]); const handleFilterSelect = useCallback( (filterKey: number | string) => { @@ -229,15 +220,13 @@ function CutoffUnmet() { @@ -250,7 +239,7 @@ function CutoffUnmet() { : translate('MonitorSelected') } iconName={icons.MONITORED} - isDisabled={!itemsSelected} + isDisabled={!anySelected} isSpinning={isSaving} onPress={handleToggleSelectedPress} /> @@ -306,13 +295,7 @@ function CutoffUnmet() { {items.map((item) => { return ( - + ); })} @@ -356,4 +339,12 @@ function CutoffUnmet() { ); } -export default CutoffUnmet; +export default function CutoffUnmet() { + const { items } = useSelector((state: AppState) => state.wanted.cutoffUnmet); + + return ( + items={items}> + + + ); +} diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.tsx b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.tsx index aa337daf1..bbeecb790 100644 --- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.tsx +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.tsx @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { useCallback } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; +import Episode from 'Episode/Episode'; import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; @@ -28,9 +30,7 @@ interface CutoffUnmetRowProps { airDateUtc?: string; lastSearchTime?: string; title: string; - isSelected?: boolean; columns: Column[]; - onSelectedChange: (options: SelectStateInputProps) => void; } function CutoffUnmetRow({ @@ -47,11 +47,22 @@ function CutoffUnmetRow({ airDateUtc, lastSearchTime, title, - isSelected, columns, - onSelectedChange, }: CutoffUnmetRowProps) { const series = useSeries(seriesId); + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, + }); + }, + [toggleSelected] + ); if (!series || !episodeFileId) { return null; @@ -62,7 +73,7 @@ function CutoffUnmetRow({ {columns.map((column) => { diff --git a/frontend/src/Wanted/Missing/Missing.tsx b/frontend/src/Wanted/Missing/Missing.tsx index ac9a0790e..d8a042ab4 100644 --- a/frontend/src/Wanted/Missing/Missing.tsx +++ b/frontend/src/Wanted/Missing/Missing.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { SelectProvider, useSelect } from 'App/Select/SelectContext'; import AppState, { Filter } from 'App/State/AppState'; import * as commandNames from 'Commands/commandNames'; import Alert from 'Components/Alert'; @@ -17,8 +18,8 @@ import TableBody from 'Components/Table/TableBody'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TablePager from 'Components/Table/TablePager'; import usePaging from 'Components/Table/usePaging'; +import Episode from 'Episode/Episode'; import useCurrentPage from 'Helpers/Hooks/useCurrentPage'; -import useSelectState from 'Helpers/Hooks/useSelectState'; import { align, icons, kinds } from 'Helpers/Props'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import { executeCommand } from 'Store/Actions/commandActions'; @@ -33,7 +34,6 @@ import { } from 'Store/Actions/wantedActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { CheckInputChanged } from 'typings/inputs'; -import { SelectStateInputProps } from 'typings/props'; import { TableOptionsChangePayload } from 'typings/Table'; import getFilterValue from 'Utilities/Filter/getFilterValue'; import { @@ -41,7 +41,6 @@ import { unregisterPagePopulator, } from 'Utilities/pagePopulator'; import translate from 'Utilities/String/translate'; -import getSelectedIds from 'Utilities/Table/getSelectedIds'; import MissingRow from './MissingRow'; function getMonitoredValue( @@ -51,7 +50,7 @@ function getMonitoredValue( return !!getFilterValue(filters, selectedFilterKey, 'monitored', false); } -function Missing() { +function MissingContent() { const dispatch = useDispatch(); const requestCurrentPage = useCurrentPage(); @@ -78,8 +77,14 @@ function Missing() { createCommandExecutingSelector(commandNames.EPISODE_SEARCH) ); - const [selectState, setSelectState] = useSelectState(); - const { allSelected, allUnselected, selectedState } = selectState; + const { + allSelected, + allUnselected, + anySelected, + getSelectedIds, + selectAll, + unselectAll, + } = useSelect(); const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] = useState(false); @@ -99,50 +104,36 @@ function Missing() { gotoPage: gotoMissingPage, }); - const selectedIds = useMemo(() => { - return getSelectedIds(selectedState); - }, [selectedState]); - const isSaving = useMemo(() => { return items.filter((m) => m.isSaving).length > 1; }, [items]); - const itemsSelected = !!selectedIds.length; const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey); const isSearchingForEpisodes = isSearchingForAllEpisodes || isSearchingForSelectedEpisodes; const handleSelectAllChange = useCallback( ({ value }: CheckInputChanged) => { - setSelectState({ type: value ? 'selectAll' : 'unselectAll', items }); + if (value) { + selectAll(); + } else { + unselectAll(); + } }, - [items, setSelectState] - ); - - const handleSelectedChange = useCallback( - ({ id, value, shiftKey = false }: SelectStateInputProps) => { - setSelectState({ - type: 'toggleSelected', - items, - id, - isSelected: value, - shiftKey, - }); - }, - [items, setSelectState] + [selectAll, unselectAll] ); const handleSearchSelectedPress = useCallback(() => { dispatch( executeCommand({ name: commandNames.EPISODE_SEARCH, - episodeIds: selectedIds, + episodeIds: getSelectedIds(), commandFinished: () => { dispatch(fetchMissing()); }, }) ); - }, [selectedIds, dispatch]); + }, [getSelectedIds, dispatch]); const handleSearchAllPress = useCallback(() => { setIsConfirmSearchAllModalOpen(true); @@ -168,11 +159,11 @@ function Missing() { const handleToggleSelectedPress = useCallback(() => { dispatch( batchToggleMissingEpisodes({ - episodeIds: selectedIds, + episodeIds: getSelectedIds(), monitored: !isShowingMonitored, }) ); - }, [isShowingMonitored, selectedIds, dispatch]); + }, [isShowingMonitored, getSelectedIds, dispatch]); const handleInteractiveImportPress = useCallback(() => { setIsInteractiveImportModalOpen(true); @@ -241,15 +232,13 @@ function Missing() { @@ -262,7 +251,7 @@ function Missing() { : translate('MonitorSelected') } iconName={icons.MONITORED} - isDisabled={!itemsSelected} + isDisabled={!anySelected} isSpinning={isSaving} onPress={handleToggleSelectedPress} /> @@ -326,13 +315,7 @@ function Missing() { {items.map((item) => { return ( - + ); })} @@ -380,4 +363,14 @@ function Missing() { ); } +function Missing() { + const { items } = useSelector((state: AppState) => state.wanted.missing); + + return ( + items={items}> + + + ); +} + export default Missing; diff --git a/frontend/src/Wanted/Missing/MissingRow.tsx b/frontend/src/Wanted/Missing/MissingRow.tsx index 1d314524d..b0fd70a7f 100644 --- a/frontend/src/Wanted/Missing/MissingRow.tsx +++ b/frontend/src/Wanted/Missing/MissingRow.tsx @@ -1,9 +1,11 @@ -import React from 'react'; +import React, { useCallback } from 'react'; +import { useSelect } from 'App/Select/SelectContext'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import Column from 'Components/Table/Column'; import TableRow from 'Components/Table/TableRow'; +import Episode from 'Episode/Episode'; import EpisodeSearchCell from 'Episode/EpisodeSearchCell'; import EpisodeStatus from 'Episode/EpisodeStatus'; import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; @@ -27,9 +29,7 @@ interface MissingRowProps { airDateUtc?: string; lastSearchTime?: string; title: string; - isSelected?: boolean; columns: Column[]; - onSelectedChange: (options: SelectStateInputProps) => void; } function MissingRow({ @@ -46,11 +46,22 @@ function MissingRow({ airDateUtc, lastSearchTime, title, - isSelected, columns, - onSelectedChange, }: MissingRowProps) { const series = useSeries(seriesId); + const { toggleSelected, useIsSelected } = useSelect(); + const isSelected = useIsSelected(id); + + const handleSelectedChange = useCallback( + ({ id, value, shiftKey = false }: SelectStateInputProps) => { + toggleSelected({ + id, + isSelected: value, + shiftKey, + }); + }, + [toggleSelected] + ); if (!series) { return null; @@ -61,7 +72,7 @@ function MissingRow({ {columns.map((column) => { diff --git a/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs b/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs index fe45cff96..9ace94c8b 100644 --- a/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs +++ b/src/Sonarr.Api.V3/Episodes/RenameEpisodeResource.cs @@ -25,6 +25,7 @@ namespace Sonarr.Api.V3.Episodes return new RenameEpisodeResource { + Id = model.EpisodeFileId, SeriesId = model.SeriesId, SeasonNumber = model.SeasonNumber, EpisodeNumbers = model.EpisodeNumbers.ToList(),