Improve indexer multiple select functionality

This commit is contained in:
Bogdan
2023-07-08 03:13:41 +03:00
parent 94c91d4c3f
commit 2ce5618499
9 changed files with 62 additions and 37 deletions

View 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;
}

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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",