1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-23 22:25:56 -04:00

New: Improve 'Select All' in Library Import

Closes #7909
This commit is contained in:
Mark McDowall
2025-10-25 16:13:31 -07:00
parent 08f0a5a960
commit 910b85f37d
32 changed files with 666 additions and 439 deletions
@@ -1,22 +1,19 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DeleteSeriesModalContent from './DeleteSeriesModalContent';
import DeleteSeriesModalContent, {
DeleteSeriesModalContentProps,
} from './DeleteSeriesModalContent';
interface DeleteSeriesModalProps {
interface DeleteSeriesModalProps extends DeleteSeriesModalContentProps {
isOpen: boolean;
seriesIds: number[];
onModalClose(): void;
}
function DeleteSeriesModal(props: DeleteSeriesModalProps) {
const { isOpen, seriesIds, onModalClose } = props;
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<DeleteSeriesModalContent
seriesIds={seriesIds}
onModalClose={onModalClose}
/>
<DeleteSeriesModalContent onModalClose={onModalClose} />
</Modal>
);
}
@@ -2,6 +2,7 @@ import { orderBy } from 'lodash';
import React, { useCallback, useMemo, 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 FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -20,8 +21,7 @@ import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteSeriesModalContent.css';
interface DeleteSeriesModalContentProps {
seriesIds: number[];
export interface DeleteSeriesModalContentProps {
onModalClose(): void;
}
@@ -30,14 +30,15 @@ const selectDeleteOptions = createSelector(
(deleteOptions) => deleteOptions
);
function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
const { seriesIds, onModalClose } = props;
function DeleteSeriesModalContent({
onModalClose,
}: DeleteSeriesModalContentProps) {
const { addImportListExclusion } = useSelector(selectDeleteOptions);
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const dispatch = useDispatch();
const [deleteFiles, setDeleteFiles] = useState(false);
const { useSelectedIds } = useSelect<Series>();
const seriesIds = useSelectedIds();
const series = useMemo((): Series[] => {
const seriesList = seriesIds.map((id) => {
@@ -45,7 +46,7 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
}) as Series[];
return orderBy(seriesList, ['sortTitle']);
}, [seriesIds, allSeries]);
}, [allSeries, seriesIds]);
const onDeleteFilesChange = useCallback(
({ value }: InputChanged<boolean>) => {
@@ -78,10 +79,10 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
onModalClose();
}, [
seriesIds,
deleteFiles,
addImportListExclusion,
setDeleteFiles,
seriesIds,
dispatch,
onModalClose,
]);
@@ -1,21 +1,21 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditSeriesModalContent from './EditSeriesModalContent';
import EditSeriesModalContent, {
EditSeriesModalContentProps,
} from './EditSeriesModalContent';
interface EditSeriesModalProps {
interface EditSeriesModalProps extends EditSeriesModalContentProps {
isOpen: boolean;
seriesIds: number[];
onSavePress(payload: object): void;
onModalClose(): void;
}
function EditSeriesModal(props: EditSeriesModalProps) {
const { isOpen, seriesIds, onSavePress, onModalClose } = props;
function EditSeriesModal({
isOpen,
onSavePress,
onModalClose,
}: EditSeriesModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<EditSeriesModalContent
seriesIds={seriesIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
@@ -1,4 +1,5 @@
import React, { useCallback, useState } from 'react';
import { useSelect } from 'App/Select/SelectContext';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
@@ -24,8 +25,7 @@ interface SavePayload {
moveFiles?: boolean;
}
interface EditSeriesModalContentProps {
seriesIds: number[];
export interface EditSeriesModalContentProps {
onSavePress(payload: object): void;
onModalClose(): void;
}
@@ -77,7 +77,7 @@ const seasonFolderOptions: EnhancedSelectInputValue<string>[] = [
];
function EditSeriesModalContent(props: EditSeriesModalContentProps) {
const { seriesIds, onSavePress, onModalClose } = props;
const { onSavePress, onModalClose } = props;
const [monitored, setMonitored] = useState(NO_CHANGE);
const [monitorNewItems, setMonitorNewItems] = useState(NO_CHANGE);
@@ -88,6 +88,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
const [seasonFolder, setSeasonFolder] = useState(NO_CHANGE);
const [rootFolderPath, setRootFolderPath] = useState(NO_CHANGE);
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const { selectedCount } = useSelect();
const save = useCallback(
(moveFiles: boolean) => {
@@ -193,8 +194,6 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
save(true);
}, [setIsConfirmMoveModalOpen, save]);
const selectedCount = seriesIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('EditSelectedSeries')}</ModalHeader>
@@ -1,11 +1,11 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import OrganizeSeriesModalContent from './OrganizeSeriesModalContent';
import OrganizeSeriesModalContent, {
OrganizeSeriesModalContentProps,
} from './OrganizeSeriesModalContent';
interface OrganizeSeriesModalProps {
interface OrganizeSeriesModalProps extends OrganizeSeriesModalContentProps {
isOpen: boolean;
seriesIds: number[];
onModalClose: () => void;
}
function OrganizeSeriesModal(props: OrganizeSeriesModalProps) {
@@ -1,6 +1,7 @@
import { orderBy } from 'lodash';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import { RENAME_SERIES } from 'Commands/commandNames';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
@@ -16,16 +17,17 @@ import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import translate from 'Utilities/String/translate';
import styles from './OrganizeSeriesModalContent.css';
interface OrganizeSeriesModalContentProps {
seriesIds: number[];
export interface OrganizeSeriesModalContentProps {
onModalClose: () => void;
}
function OrganizeSeriesModalContent(props: OrganizeSeriesModalContentProps) {
const { seriesIds, onModalClose } = props;
function OrganizeSeriesModalContent({
onModalClose,
}: OrganizeSeriesModalContentProps) {
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const dispatch = useDispatch();
const { useSelectedIds } = useSelect<Series>();
const seriesIds = useSelectedIds();
const seriesTitles = useMemo(() => {
const series = seriesIds.reduce((acc: Series[], id) => {
@@ -41,7 +43,7 @@ function OrganizeSeriesModalContent(props: OrganizeSeriesModalContentProps) {
const sorted = orderBy(series, ['sortTitle']);
return sorted.map((s) => s.title);
}, [seriesIds, allSeries]);
}, [allSeries, seriesIds]);
const onOrganizePress = useCallback(() => {
dispatch(
@@ -1,21 +1,21 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ChangeMonitoringModalContent from './ChangeMonitoringModalContent';
import ChangeMonitoringModalContent, {
ChangeMonitoringModalContentProps,
} from './ChangeMonitoringModalContent';
interface ChangeMonitoringModalProps {
interface ChangeMonitoringModalProps extends ChangeMonitoringModalContentProps {
isOpen: boolean;
seriesIds: number[];
onSavePress(monitor: string): void;
onModalClose(): void;
}
function ChangeMonitoringModal(props: ChangeMonitoringModalProps) {
const { isOpen, seriesIds, onSavePress, onModalClose } = props;
function ChangeMonitoringModal({
isOpen,
onSavePress,
onModalClose,
}: ChangeMonitoringModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ChangeMonitoringModalContent
seriesIds={seriesIds}
onSavePress={onSavePress}
onModalClose={onModalClose}
/>
@@ -1,5 +1,6 @@
import React, { useCallback, useState } from 'react';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import { useSelect } from 'App/Select/SelectContext';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -18,19 +19,19 @@ import styles from './ChangeMonitoringModalContent.css';
const NO_CHANGE = 'noChange';
interface ChangeMonitoringModalContentProps {
seriesIds: number[];
export interface ChangeMonitoringModalContentProps {
saveError?: object;
onSavePress(monitor: string): void;
onModalClose(): void;
}
function ChangeMonitoringModalContent(
props: ChangeMonitoringModalContentProps
) {
const { seriesIds, onSavePress, onModalClose, ...otherProps } = props;
function ChangeMonitoringModalContent({
onSavePress,
onModalClose,
...otherProps
}: ChangeMonitoringModalContentProps) {
const [monitor, setMonitor] = useState(NO_CHANGE);
const { selectedCount } = useSelect();
const onInputChange = useCallback(
({ value }: { value: string }) => {
@@ -43,8 +44,6 @@ function ChangeMonitoringModalContent(
onSavePress(monitor);
}, [monitor, onSavePress]);
const selectedCount = seriesIds.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('MonitorEpisodes')}</ModalHeader>
@@ -1,5 +1,5 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import { useSelect } from 'App/Select/SelectContext';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
@@ -14,8 +14,8 @@ function SeriesIndexPosterSelect({
seriesId,
titleSlug,
}: SeriesIndexPosterSelectProps) {
const [selectState, selectDispatch] = useSelect();
const isSelected = selectState.selectedState[seriesId];
const { toggleSelected, useIsSelected } = useSelect();
const isSelected = useIsSelected(seriesId);
const onSelectPress = useCallback(
(event: SyntheticEvent<HTMLElement, PointerEvent>) => {
@@ -26,14 +26,13 @@ function SeriesIndexPosterSelect({
const shiftKey = event.nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',
toggleSelected({
id: seriesId,
isSelected: !isSelected,
shiftKey,
});
},
[seriesId, titleSlug, isSelected, selectDispatch]
[seriesId, titleSlug, isSelected, toggleSelected]
);
return (
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import { useSelect } from 'App/Select/SelectContext';
import PageToolbarButton, {
PageToolbarButtonProps,
} from 'Components/Page/Toolbar/PageToolbarButton';
@@ -13,8 +13,7 @@ interface SeriesIndexSelectAllButtonProps
function SeriesIndexSelectAllButton(props: SeriesIndexSelectAllButtonProps) {
const { isSelectMode, overflowComponent } = props;
const [selectState, selectDispatch] = useSelect();
const { allSelected, allUnselected } = selectState;
const { allSelected, allUnselected, selectAll, unselectAll } = useSelect();
let icon = icons.SQUARE_MINUS;
@@ -25,10 +24,12 @@ function SeriesIndexSelectAllButton(props: SeriesIndexSelectAllButtonProps) {
}
const onPress = useCallback(() => {
selectDispatch({
type: allSelected ? 'unselectAll' : 'selectAll',
});
}, [allSelected, selectDispatch]);
if (allSelected) {
unselectAll();
} else {
selectAll();
}
}, [allSelected, selectAll, unselectAll]);
return isSelectMode ? (
<PageToolbarButton
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import { useSelect } from 'App/Select/SelectContext';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
@@ -13,8 +13,7 @@ function SeriesIndexSelectAllMenuItem(
props: SeriesIndexSelectAllMenuItemProps
) {
const { isSelectMode } = props;
const [selectState, selectDispatch] = useSelect();
const { allSelected, allUnselected } = selectState;
const { allSelected, allUnselected, selectAll, unselectAll } = useSelect();
let iconName = icons.SQUARE_MINUS;
@@ -25,10 +24,12 @@ function SeriesIndexSelectAllMenuItem(
}
const onPressWrapper = useCallback(() => {
selectDispatch({
type: allSelected ? 'unselectAll' : 'selectAll',
});
}, [allSelected, selectDispatch]);
if (allSelected) {
unselectAll();
} else {
selectAll();
}
}, [allSelected, selectAll, unselectAll]);
return isSelectMode ? (
<PageToolbarOverflowMenuItem
@@ -1,13 +1,14 @@
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 { useSelect } from 'App/SelectContext';
import { useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import { RENAME_SERIES } from 'Commands/commandNames';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { kinds } from 'Helpers/Props';
import Series from 'Series/Series';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import {
saveSeriesEditor,
@@ -15,7 +16,6 @@ import {
} from 'Store/Actions/seriesActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import DeleteSeriesModal from './Delete/DeleteSeriesModal';
import EditSeriesModal from './Edit/EditSeriesModal';
import OrganizeSeriesModal from './Organize/OrganizeSeriesModal';
@@ -64,15 +64,8 @@ function SeriesIndexSelectFooter() {
const [isSavingTags, setIsSavingTags] = useState(false);
const [isSavingMonitoring, setIsSavingMonitoring] = useState(false);
const previousIsDeleting = usePrevious(isDeleting);
const [selectState, selectDispatch] = useSelect();
const { selectedState } = selectState;
const seriesIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const selectedCount = seriesIds.length;
const { selectedCount, unselectAll, useSelectedIds } = useSelect<Series>();
const seriesIds = useSelectedIds();
const onEditPress = useCallback(() => {
setIsEditModalOpen(true);
@@ -170,9 +163,9 @@ function SeriesIndexSelectFooter() {
useEffect(() => {
if (previousIsDeleting && !isDeleting && !deleteError) {
selectDispatch({ type: 'unselectAll' });
unselectAll();
}
}, [previousIsDeleting, isDeleting, deleteError, selectDispatch]);
}, [previousIsDeleting, isDeleting, deleteError, unselectAll]);
useEffect(() => {
dispatch(fetchRootFolders());
@@ -236,34 +229,29 @@ function SeriesIndexSelectFooter() {
<EditSeriesModal
isOpen={isEditModalOpen}
seriesIds={seriesIds}
onSavePress={onSavePress}
onModalClose={onEditModalClose}
/>
<TagsModal
isOpen={isTagsModalOpen}
seriesIds={seriesIds}
onApplyTagsPress={onApplyTagsPress}
onModalClose={onTagsModalClose}
/>
<ChangeMonitoringModal
isOpen={isMonitoringModalOpen}
seriesIds={seriesIds}
onSavePress={onMonitoringSavePress}
onModalClose={onMonitoringClose}
/>
<OrganizeSeriesModal
isOpen={isOrganizeModalOpen}
seriesIds={seriesIds}
onModalClose={onOrganizeModalClose}
/>
<DeleteSeriesModal
isOpen={isDeleteModalOpen}
seriesIds={seriesIds}
onModalClose={onDeleteModalClose}
/>
</PageContentFooter>
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import { useSelect } from 'App/Select/SelectContext';
import PageToolbarButton, {
PageToolbarButtonProps,
} from 'Components/Page/Toolbar/PageToolbarButton';
@@ -11,17 +11,15 @@ interface SeriesIndexSelectModeButtonProps extends PageToolbarButtonProps {
function SeriesIndexSelectModeButton(props: SeriesIndexSelectModeButtonProps) {
const { label, iconName, isSelectMode, overflowComponent, onPress } = props;
const [, selectDispatch] = useSelect();
const { reset } = useSelect();
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: 'reset',
});
reset();
}
onPress();
}, [isSelectMode, onPress, selectDispatch]);
}, [isSelectMode, onPress, reset]);
return (
<PageToolbarButton
@@ -1,5 +1,5 @@
import React, { useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import { useSelect } from 'App/Select/SelectContext';
import { IconName } from 'Components/Icon';
import PageToolbarOverflowMenuItem from 'Components/Page/Toolbar/PageToolbarOverflowMenuItem';
@@ -14,17 +14,15 @@ function SeriesIndexSelectModeMenuItem(
props: SeriesIndexSelectModeMenuItemProps
) {
const { label, iconName, isSelectMode, onPress } = props;
const [, selectDispatch] = useSelect();
const { reset } = useSelect();
const onPressWrapper = useCallback(() => {
if (isSelectMode) {
selectDispatch({
type: 'reset',
});
reset();
}
onPress();
}, [isSelectMode, onPress, selectDispatch]);
}, [isSelectMode, onPress, reset]);
return (
<PageToolbarOverflowMenuItem
@@ -1,12 +1,9 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import TagsModalContent from './TagsModalContent';
import TagsModalContent, { TagsModalContentProps } from './TagsModalContent';
interface TagsModalProps {
interface TagsModalProps extends TagsModalContentProps {
isOpen: boolean;
seriesIds: number[];
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModal(props: TagsModalProps) {
@@ -1,6 +1,7 @@
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import { Tag } from 'App/State/TagsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -20,20 +21,22 @@ import createTagsSelector from 'Store/Selectors/createTagsSelector';
import translate from 'Utilities/String/translate';
import styles from './TagsModalContent.css';
interface TagsModalContentProps {
seriesIds: number[];
export interface TagsModalContentProps {
onApplyTagsPress: (tags: number[], applyTags: string) => void;
onModalClose: () => void;
}
function TagsModalContent(props: TagsModalContentProps) {
const { seriesIds, onModalClose, onApplyTagsPress } = props;
function TagsModalContent({
onModalClose,
onApplyTagsPress,
}: TagsModalContentProps) {
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
const { useSelectedIds } = useSelect<Series>();
const seriesIds = useSelectedIds();
const seriesTags = useMemo(() => {
const tags = seriesIds.reduce((acc: number[], id) => {
@@ -47,7 +50,7 @@ function TagsModalContent(props: TagsModalContentProps) {
}, []);
return uniq(tags);
}, [seriesIds, allSeries]);
}, [allSeries, seriesIds]);
const onTagsChange = useCallback(
({ value }: { value: number[] }) => {