mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
Improve indexer multiple select functionality
This commit is contained in:
11
frontend/src/Helpers/Hooks/usePrevious.tsx
Normal file
11
frontend/src/Helpers/Hooks/usePrevious.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export default function usePrevious<T>(value: T): T | undefined {
|
||||||
|
const ref = useRef<T>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
@@ -148,15 +148,15 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const jumpBarItems = useMemo(() => {
|
const jumpBarItems = useMemo(() => {
|
||||||
// Reset if not sorting by sortTitle
|
// Reset if not sorting by sortName
|
||||||
if (sortKey !== 'sortTitle') {
|
if (sortKey !== 'sortName') {
|
||||||
return {
|
return {
|
||||||
order: [],
|
order: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const characters = items.reduce((acc, item) => {
|
const characters = items.reduce((acc, item) => {
|
||||||
let char = item.sortTitle.charAt(0);
|
let char = item.sortName.charAt(0);
|
||||||
|
|
||||||
if (!isNaN(char)) {
|
if (!isNaN(char)) {
|
||||||
char = '#';
|
char = '#';
|
||||||
@@ -225,7 +225,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
|
|||||||
label={
|
label={
|
||||||
isSelectMode
|
isSelectMode
|
||||||
? translate('StopSelecting')
|
? translate('StopSelecting')
|
||||||
: translate('SelectIndexer')
|
: translate('SelectIndexers')
|
||||||
}
|
}
|
||||||
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
iconName={isSelectMode ? icons.SERIES_ENDED : icons.CHECK}
|
||||||
isSelectMode={isSelectMode}
|
isSelectMode={isSelectMode}
|
||||||
|
@@ -9,6 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
|||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
|
import { bulkDeleteIndexers } from 'Store/Actions/indexerIndexActions';
|
||||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './DeleteIndexerModalContent.css';
|
import styles from './DeleteIndexerModalContent.css';
|
||||||
|
|
||||||
interface DeleteIndexerModalContentProps {
|
interface DeleteIndexerModalContentProps {
|
||||||
@@ -19,16 +20,16 @@ interface DeleteIndexerModalContentProps {
|
|||||||
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||||
const { indexerIds, onModalClose } = props;
|
const { indexerIds, onModalClose } = props;
|
||||||
|
|
||||||
const allIndexer = useSelector(createAllIndexersSelector());
|
const allIndexers = useSelector(createAllIndexersSelector());
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const indexers = useMemo(() => {
|
const selectedIndexers = useMemo(() => {
|
||||||
const indexers = indexerIds.map((id) => {
|
const indexers = indexerIds.map((id) => {
|
||||||
return allIndexer.find((s) => s.id === id);
|
return allIndexers.find((s) => s.id === id);
|
||||||
});
|
});
|
||||||
|
|
||||||
return orderBy(indexers, ['sortTitle']);
|
return orderBy(indexers, ['sortName']);
|
||||||
}, [indexerIds, allIndexer]);
|
}, [indexerIds, allIndexers]);
|
||||||
|
|
||||||
const onDeleteIndexerConfirmed = useCallback(() => {
|
const onDeleteIndexerConfirmed = useCallback(() => {
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -42,17 +43,19 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Delete Selected Indexer</ModalHeader>
|
<ModalHeader>{translate('DeleteSelectedIndexers')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
{`Are you sure you want to delete ${indexers.length} selected indexers?`}
|
{translate('DeleteSelectedIndexersMessageText', [
|
||||||
|
selectedIndexers.length,
|
||||||
|
])}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{indexers.map((s) => {
|
{selectedIndexers.map((s) => {
|
||||||
return (
|
return (
|
||||||
<li key={s.name}>
|
<li key={s.id}>
|
||||||
<span>{s.name}</span>
|
<span>{s.name}</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
@@ -61,10 +64,10 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>Cancel</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button kind={kinds.DANGER} onPress={onDeleteIndexerConfirmed}>
|
<Button kind={kinds.DANGER} onPress={onDeleteIndexerConfirmed}>
|
||||||
Delete
|
{translate('Delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@@ -67,7 +67,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
setAppProfileId(value);
|
setAppProfileId(value);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn('EditIndexerModalContent Unknown Input');
|
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setEnable]
|
[setEnable]
|
||||||
@@ -81,7 +81,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>{translate('Edit Selected Indexer')}</ModalHeader>
|
<ModalHeader>{translate('EditSelectedIndexers')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
@@ -112,14 +112,14 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
|||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('{0} indexers selected', selectedCount.toString())}
|
{translate('CountIndexersSelected', [selectedCount])}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button onPress={onSavePressWrapper}>
|
<Button onPress={onSavePressWrapper}>
|
||||||
{translate('Apply Changes')}
|
{translate('ApplyChanges')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
@@ -4,6 +4,7 @@ import { createSelector } from 'reselect';
|
|||||||
import { useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
|
import { saveIndexerEditor } from 'Store/Actions/indexerIndexActions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -13,7 +14,7 @@ import EditIndexerModal from './Edit/EditIndexerModal';
|
|||||||
import TagsModal from './Tags/TagsModal';
|
import TagsModal from './Tags/TagsModal';
|
||||||
import styles from './IndexerIndexSelectFooter.css';
|
import styles from './IndexerIndexSelectFooter.css';
|
||||||
|
|
||||||
const seriesEditorSelector = createSelector(
|
const indexersEditorSelector = createSelector(
|
||||||
(state) => state.indexers,
|
(state) => state.indexers,
|
||||||
(indexers) => {
|
(indexers) => {
|
||||||
const { isSaving, isDeleting, deleteError } = indexers;
|
const { isSaving, isDeleting, deleteError } = indexers;
|
||||||
@@ -27,8 +28,9 @@ const seriesEditorSelector = createSelector(
|
|||||||
);
|
);
|
||||||
|
|
||||||
function IndexerIndexSelectFooter() {
|
function IndexerIndexSelectFooter() {
|
||||||
const { isSaving, isDeleting, deleteError } =
|
const { isSaving, isDeleting, deleteError } = useSelector(
|
||||||
useSelector(seriesEditorSelector);
|
indexersEditorSelector
|
||||||
|
);
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ function IndexerIndexSelectFooter() {
|
|||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isSavingIndexer, setIsSavingIndexer] = useState(false);
|
const [isSavingIndexer, setIsSavingIndexer] = useState(false);
|
||||||
const [isSavingTags, setIsSavingTags] = useState(false);
|
const [isSavingTags, setIsSavingTags] = useState(false);
|
||||||
|
const previousIsDeleting = usePrevious(isDeleting);
|
||||||
|
|
||||||
const [selectState, selectDispatch] = useSelect();
|
const [selectState, selectDispatch] = useSelect();
|
||||||
const { selectedState } = selectState;
|
const { selectedState } = selectState;
|
||||||
@@ -110,10 +113,10 @@ function IndexerIndexSelectFooter() {
|
|||||||
}, [isSaving]);
|
}, [isSaving]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDeleting && !deleteError) {
|
if (previousIsDeleting && !isDeleting && !deleteError) {
|
||||||
selectDispatch({ type: 'unselectAll' });
|
selectDispatch({ type: 'unselectAll' });
|
||||||
}
|
}
|
||||||
}, [isDeleting, deleteError, selectDispatch]);
|
}, [previousIsDeleting, isDeleting, deleteError, selectDispatch]);
|
||||||
|
|
||||||
const anySelected = selectedCount > 0;
|
const anySelected = selectedCount > 0;
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@ function IndexerIndexSelectFooter() {
|
|||||||
isDisabled={!anySelected}
|
isDisabled={!anySelected}
|
||||||
onPress={onTagsPress}
|
onPress={onTagsPress}
|
||||||
>
|
>
|
||||||
{translate('Set Tags')}
|
{translate('SetTags')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -151,7 +154,7 @@ function IndexerIndexSelectFooter() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.selected}>
|
<div className={styles.selected}>
|
||||||
{translate('{0} indexers selected', selectedCount.toString())}
|
{translate('CountIndexersSelected', [selectedCount])}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditIndexerModal
|
<EditIndexerModal
|
||||||
|
@@ -59,14 +59,14 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||||||
}, [tags, applyTags, onApplyTagsPress]);
|
}, [tags, applyTags, onApplyTagsPress]);
|
||||||
|
|
||||||
const applyTagsOptions = [
|
const applyTagsOptions = [
|
||||||
{ key: 'add', value: 'Add' },
|
{ key: 'add', value: translate('Add') },
|
||||||
{ key: 'remove', value: 'Remove' },
|
{ key: 'remove', value: translate('Remove') },
|
||||||
{ key: 'replace', value: 'Replace' },
|
{ key: 'replace', value: translate('Replace') },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Tags</ModalHeader>
|
<ModalHeader>{translate('Tags')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form>
|
<Form>
|
||||||
@@ -119,8 +119,8 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||||||
key={tag.id}
|
key={tag.id}
|
||||||
title={
|
title={
|
||||||
removeTag
|
removeTag
|
||||||
? translate('RemoveTagRemovingTag')
|
? translate('RemovingTag')
|
||||||
: translate('RemoveTagExistingTag')
|
: translate('ExistingTag')
|
||||||
}
|
}
|
||||||
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
kind={removeTag ? kinds.INVERSE : kinds.INFO}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
@@ -159,10 +159,10 @@ function TagsModalContent(props: TagsModalContentProps) {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>Cancel</Button>
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
<Button kind={kinds.PRIMARY} onPress={onApplyPress}>
|
||||||
Apply
|
{translate('Apply')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
@@ -29,6 +29,8 @@ export const defaultState = {
|
|||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null,
|
||||||
selectedSchema: {},
|
selectedSchema: {},
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
saveError: null,
|
saveError: null,
|
||||||
|
@@ -14,7 +14,7 @@ function createUnoptimizedSelector(uiSection) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
sortTitle: name
|
sortName: name
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const createMovieEqualSelector = createSelectorCreator(
|
|||||||
function createIndexerClientSideCollectionItemsSelector(uiSection) {
|
function createIndexerClientSideCollectionItemsSelector(uiSection) {
|
||||||
return createMovieEqualSelector(
|
return createMovieEqualSelector(
|
||||||
createUnoptimizedSelector(uiSection),
|
createUnoptimizedSelector(uiSection),
|
||||||
(movies) => movies
|
(indexers) => indexers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -41,6 +41,7 @@
|
|||||||
"ApplicationUrlHelpText": "This application's external URL including http(s)://, port and URL base",
|
"ApplicationUrlHelpText": "This application's external URL including http(s)://, port and URL base",
|
||||||
"Applications": "Applications",
|
"Applications": "Applications",
|
||||||
"Apply": "Apply",
|
"Apply": "Apply",
|
||||||
|
"ApplyChanges": "Apply Changes",
|
||||||
"ApplyTags": "Apply Tags",
|
"ApplyTags": "Apply Tags",
|
||||||
"ApplyTagsHelpTexts1": "How to apply tags to the selected indexers",
|
"ApplyTagsHelpTexts1": "How to apply tags to the selected indexers",
|
||||||
"ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags",
|
"ApplyTagsHelpTexts2": "Add: Add the tags the existing list of tags",
|
||||||
@@ -101,6 +102,7 @@
|
|||||||
"ConnectionLostMessage": "Prowlarr has lost its connection to the backend and will need to be reloaded to restore functionality.",
|
"ConnectionLostMessage": "Prowlarr has lost its connection to the backend and will need to be reloaded to restore functionality.",
|
||||||
"Connections": "Connections",
|
"Connections": "Connections",
|
||||||
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
|
"CouldNotConnectSignalR": "Could not connect to SignalR, UI won't update",
|
||||||
|
"CountIndexersSelected": "{0} indexers selected",
|
||||||
"Custom": "Custom",
|
"Custom": "Custom",
|
||||||
"CustomFilters": "Custom Filters",
|
"CustomFilters": "Custom Filters",
|
||||||
"DBMigration": "DB Migration",
|
"DBMigration": "DB Migration",
|
||||||
@@ -120,6 +122,9 @@
|
|||||||
"DeleteIndexerProxyMessageText": "Are you sure you want to delete the proxy '{0}'?",
|
"DeleteIndexerProxyMessageText": "Are you sure you want to delete the proxy '{0}'?",
|
||||||
"DeleteNotification": "Delete Notification",
|
"DeleteNotification": "Delete Notification",
|
||||||
"DeleteNotificationMessageText": "Are you sure you want to delete the notification '{0}'?",
|
"DeleteNotificationMessageText": "Are you sure you want to delete the notification '{0}'?",
|
||||||
|
"DeleteSelectedIndexer": "Delete Selected Indexer",
|
||||||
|
"DeleteSelectedIndexers": "Delete Selected Indexers",
|
||||||
|
"DeleteSelectedIndexersMessageText": "Are you sure you want to delete {0} selected indexer(s)?",
|
||||||
"DeleteTag": "Delete Tag",
|
"DeleteTag": "Delete Tag",
|
||||||
"DeleteTagMessageText": "Are you sure you want to delete the tag '{0}'?",
|
"DeleteTagMessageText": "Are you sure you want to delete the tag '{0}'?",
|
||||||
"Description": "Description",
|
"Description": "Description",
|
||||||
@@ -140,6 +145,7 @@
|
|||||||
"Duration": "Duration",
|
"Duration": "Duration",
|
||||||
"Edit": "Edit",
|
"Edit": "Edit",
|
||||||
"EditIndexer": "Edit Indexer",
|
"EditIndexer": "Edit Indexer",
|
||||||
|
"EditSelectedIndexers": "Edit Selected Indexers",
|
||||||
"EditSyncProfile": "Edit Sync Profile",
|
"EditSyncProfile": "Edit Sync Profile",
|
||||||
"ElapsedTime": "Elapsed Time",
|
"ElapsedTime": "Elapsed Time",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
@@ -391,7 +397,7 @@
|
|||||||
"Security": "Security",
|
"Security": "Security",
|
||||||
"Seeders": "Seeders",
|
"Seeders": "Seeders",
|
||||||
"SelectAll": "Select All",
|
"SelectAll": "Select All",
|
||||||
"SelectIndexer": "Select Indexer",
|
"SelectIndexers": "Select Indexers",
|
||||||
"SemiPrivate": "Semi-Private",
|
"SemiPrivate": "Semi-Private",
|
||||||
"SendAnonymousUsageData": "Send Anonymous Usage Data",
|
"SendAnonymousUsageData": "Send Anonymous Usage Data",
|
||||||
"SetTags": "Set Tags",
|
"SetTags": "Set Tags",
|
||||||
|
Reference in New Issue
Block a user