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