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:
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
65
frontend/src/InteractiveImport/Episode/SelectEpisodeRow.tsx
Normal file
65
frontend/src/InteractiveImport/Episode/SelectEpisodeRow.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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[],
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user