1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Convert select to SelectContext

This commit is contained in:
Mark McDowall
2025-10-26 08:56:28 -07:00
parent 910b85f37d
commit b5967425f1
32 changed files with 666 additions and 751 deletions

View File

@@ -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<ReactElement | null>(null);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const { allSelected, allUnselected, selectAll, unselectAll, useSelectedIds } =
useSelect<QueueModel>();
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 (
<QueueRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
onQueueRowModalOpenOrClose={
handleQueueRowModalOpenOrClose
}
@@ -342,7 +324,7 @@ function Queue() {
selectedCount={selectedCount}
canChangeCategory={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
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 (
<SelectProvider<QueueModel> items={records}>
<QueueContent />
</SelectProvider>
);
}
export default Queue;

View File

@@ -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<Queue>();
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) {
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
onSelectedChange={handleSelectedChange}
/>
{columns.map((column) => {

View File

@@ -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<number | string, boolean>;
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];
}

View File

@@ -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<Episode>();
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 (
<SelectProvider items={items}>
<SelectEpisodeModalContentInner {...props} />
</SelectProvider>
);
}
export default SelectEpisodeModalContent;

View File

@@ -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 (
<TableRowButton onPress={this.onPress}>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell>
{episodeNumber}
{isAnime ? ` (${absoluteEpisodeNumber})` : ''}
</TableRowCell>
<TableRowCell>
{title}
</TableRowCell>
<TableRowCell>
{airDate}
</TableRowCell>
</TableRowButton>
);
}
}
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;

View File

@@ -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<Episode>();
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 (
<TableRowButton onPress={handlePress}>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={handleSelectedChange}
/>
<TableRowCell>
{episodeNumber}
{isAnime ? ` (${absoluteEpisodeNumber})` : ''}
</TableRowCell>
<TableRowCell>{title}</TableRowCell>
<TableRowCell>{airDate}</TableRowCell>
</TableRowButton>
);
}
export default SelectEpisodeRow;

View File

@@ -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<string | null>(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<InteractiveImport>();
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<OnSelectedChangeCallback>(
({ 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 (
<InteractiveImportRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
allowSeriesChange={allowSeriesChange}
columns={columns}
@@ -1048,4 +1052,18 @@ function InteractiveImportModalContent(
);
}
function InteractiveImportModalContent(
props: InteractiveImportModalContentProps
) {
const { items }: InteractiveImportAppState = useSelector(
createClientSideCollectionSelector('interactiveImport')
);
return (
<SelectProvider<InteractiveImport> items={items}>
<InteractiveImportModalContentInner {...props} />
</SelectProvider>
);
}
export default InteractiveImportModalContent;

View File

@@ -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<InteractiveImport>();
const isSelected = useIsSelected(id);
const isSeriesColumnVisible = useMemo(
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,

View File

@@ -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<OrganizePreviewModel>();
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 (
<SelectProvider<OrganizePreviewModel> items={items}>
<OrganizePreviewModalContentInner
seriesId={seriesId}
seasonNumber={seasonNumber}
onModalClose={onModalClose}
/>
</SelectProvider>
);
}
export default OrganizePreviewModalContent;

View File

@@ -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<OrganizePreviewModel>();
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 (
<div className={styles.row}>

View File

@@ -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<number | string, boolean>
>((acc, item) => {
acc[item] = expandAll;
return acc;
}, {});
setExpandedState({
allExpanded: updated.allSelected,
allCollapsed: updated.allUnselected,
seasons: updated.selectedState,
allExpanded: expandAll,
allCollapsed: !expandAll,
seasons: newSeasons,
});
}, [expandedState]);

View File

@@ -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<CustomFormat>();
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<OnSelectedChangeCallback>(
({ 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 (
<ModalContent onModalClose={onModalClose}>
@@ -184,10 +176,8 @@ function ManageCustomFormatsModalContent(
return (
<ManageCustomFormatsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
@@ -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 (
<SelectProvider items={items}>
<ManageCustomFormatsModalContentInner {...props} />
</SelectProvider>
);
}
export default ManageCustomFormatsModalContent;

View File

@@ -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<CustomFormat>();
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(() => {

View File

@@ -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<DownloadClient>();
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<OnSelectedChangeCallback>(
({ 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 (
<ModalContent onModalClose={onModalClose}>
@@ -239,10 +232,8 @@ function ManageDownloadClientsModalContent(
return (
<ManageDownloadClientsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
@@ -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 (
<SelectProvider items={items}>
<ManageDownloadClientsModalContentInner onModalClose={onModalClose} />
</SelectProvider>
);
}
export default ManageDownloadClientsModalContent;

View File

@@ -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<DownloadClient>();
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(
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
onSelectedChange={handleSelectedChange}
/>
<TableRowCell className={styles.name}>{name}</TableRowCell>

View File

@@ -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<ImportListExclusion>();
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) {
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
onSelectedChange={handleSelectedChange}
/>
<TableRowCell>{title}</TableRowCell>

View File

@@ -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<ImportListExclusion>();
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() {
>
<TableBody>
{items.map((item) => {
return (
<ImportListExclusionRow
key={item.id}
{...item}
isSelected={selectedState[item.id] || false}
onSelectedChange={handleSelectedChange}
/>
);
return <ImportListExclusionRow key={item.id} {...item} />;
})}
<TableRow>
@@ -253,7 +238,7 @@ function ImportListExclusions() {
<SpinnerButton
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedIds.length}
isDisabled={!anySelected}
onPress={handleDeleteSelectedPress}
>
{translate('Delete')}
@@ -301,4 +286,14 @@ function ImportListExclusions() {
);
}
function ImportListExclusions() {
const { items } = useSelector(createImportListExclusionsSelector());
return (
<SelectProvider items={items}>
<ImportListExclusionsContent />
</SelectProvider>
);
}
export default ImportListExclusions;

View File

@@ -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<ImportList>();
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<OnSelectedChangeCallback>(
({ 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 (
<ModalContent onModalClose={onModalClose}>
@@ -216,10 +206,8 @@ function ManageImportListsModalContent(
return (
<ManageImportListsModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
@@ -288,4 +276,18 @@ function ManageImportListsModalContent(
);
}
function ManageImportListsModalContent(
props: ManageImportListsModalContentProps
) {
const { items }: ImportListAppState = useSelector(
createClientSideCollectionSelector('settings.importLists')
);
return (
<SelectProvider items={items}>
<ManageImportListsModalContentInner {...props} />
</SelectProvider>
);
}
export default ManageImportListsModalContent;

View File

@@ -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<ImportList>();
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 (

View File

@@ -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<Indexer>();
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<OnSelectedChangeCallback>(
({ 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 (
<ModalContent onModalClose={onModalClose}>
@@ -240,10 +232,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
return (
<ManageIndexersModalRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
columns={COLUMNS}
onSelectedChange={onSelectedChange}
/>
);
})}
@@ -312,4 +302,16 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
);
}
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
const { items }: IndexerAppState = useSelector(
createClientSideCollectionSelector('settings.indexers', 'manageIndexers')
);
return (
<SelectProvider items={items}>
<ManageIndexersModalContentInner {...props} />
</SelectProvider>
);
}
export default ManageIndexersModalContent;

View File

@@ -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<Indexer>();
const isSelected = useIsSelected(id);
const onSelectedChangeWrapper = useCallback(
(result: SelectStateInputProps) => {
onSelectedChange({
...result,
({ id, value, shiftKey }: SelectStateInputProps) => {
toggleSelected({
id,
isSelected: value,
shiftKey,
});
},
[onSelectedChange]
[toggleSelected]
);
return (

View File

@@ -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,
};
}

View File

@@ -1,21 +0,0 @@
import { reduce } from 'lodash';
import { SelectedState } from 'Helpers/Hooks/useSelectState';
function getSelectedIds<T extends number | string = number>(
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;

View File

@@ -1,6 +1,6 @@
import { SelectStateModel } from 'Helpers/Hooks/useSelectState';
import { Id, SelectStoreModel } from 'App/Select/useSelectStore';
function getToggledRange<T extends SelectStateModel>(
function getToggledRange<T extends SelectStoreModel<Id>>(
items: T[],
id: number | string,
lastToggled: number | string

View File

@@ -1,21 +0,0 @@
import ModelBase from 'App/ModelBase';
import { SelectState } from 'Helpers/Hooks/useSelectState';
import areAllSelected from './areAllSelected';
export default function removeOldSelectedState<T extends ModelBase>(
state: SelectState,
prevItems: T[]
) {
const selectedState = {
...state.selectedState,
};
prevItems.forEach((item) => {
delete selectedState[item.id];
});
return {
...areAllSelected(selectedState),
selectedState,
};
}

View File

@@ -1,19 +0,0 @@
import { SelectedState } from 'Helpers/Hooks/useSelectState';
function selectAll(selectedState: SelectedState, selected: boolean) {
const newSelectedState = Object.keys(selectedState).reduce<
Record<number | string, boolean>
>((acc, item) => {
acc[item] = selected;
return acc;
}, {});
return {
allSelected: selected,
allUnselected: !selected,
lastToggled: null,
selectedState: newSelectedState,
};
}
export default selectAll;

View File

@@ -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<number | string, boolean>;
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<T extends SelectStateModel>(
selectState: SelectState,
items: T[],

View File

@@ -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<Episode>();
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() {
<PageToolbarSection>
<PageToolbarButton
label={
itemsSelected
? translate('SearchSelected')
: translate('SearchAll')
anySelected ? translate('SearchSelected') : translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
anySelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
@@ -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() {
<TableBody>
{items.map((item) => {
return (
<CutoffUnmetRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
<CutoffUnmetRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
@@ -356,4 +339,12 @@ function CutoffUnmet() {
);
}
export default CutoffUnmet;
export default function CutoffUnmet() {
const { items } = useSelector((state: AppState) => state.wanted.cutoffUnmet);
return (
<SelectProvider<Episode> items={items}>
<CutoffUnmetContent />
</SelectProvider>
);
}

View File

@@ -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<Episode>();
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({
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
onSelectedChange={handleSelectedChange}
/>
{columns.map((column) => {

View File

@@ -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<Episode>();
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() {
<PageToolbarSection>
<PageToolbarButton
label={
itemsSelected
? translate('SearchSelected')
: translate('SearchAll')
anySelected ? translate('SearchSelected') : translate('SearchAll')
}
iconName={icons.SEARCH}
isDisabled={isSearchingForEpisodes}
isSpinning={isSearchingForEpisodes}
onPress={
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
anySelected ? handleSearchSelectedPress : handleSearchAllPress
}
/>
@@ -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() {
<TableBody>
{items.map((item) => {
return (
<MissingRow
key={item.id}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
<MissingRow key={item.id} columns={columns} {...item} />
);
})}
</TableBody>
@@ -380,4 +363,14 @@ function Missing() {
);
}
function Missing() {
const { items } = useSelector((state: AppState) => state.wanted.missing);
return (
<SelectProvider<Episode> items={items}>
<MissingContent />
</SelectProvider>
);
}
export default Missing;

View File

@@ -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<Episode>();
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({
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
onSelectedChange={handleSelectedChange}
/>
{columns.map((column) => {

View File

@@ -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(),