From f2af7a1b727c5be24e3fda1b58e6ba2c0153b658 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 16 Jul 2024 21:34:43 -0700 Subject: [PATCH] New: Use natural sorting for lists of items in the UI (cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746) Closes #10177 --- .../Components/Filter/Builder/FilterBuilderRow.js | 3 ++- .../Builder/FilterBuilderRowValueConnector.js | 4 ++-- .../Filter/Builder/MovieFilterBuilderRowValue.tsx | 4 ++-- .../CustomFilters/CustomFiltersModalContent.js | 3 ++- .../Form/DownloadClientSelectInputConnector.js | 7 ++++--- .../Components/Form/IndexerSelectInputConnector.js | 4 ++-- .../Form/QualityProfileSelectInputConnector.js | 4 ++-- frontend/src/Components/Menu/FilterMenuContent.js | 3 ++- frontend/src/Components/TagList.js | 3 ++- .../Movie/SelectMovieModalContent.tsx | 5 ++--- frontend/src/Movie/Details/MovieTagsConnector.js | 5 +++-- .../CustomFormats/CustomFormatsConnector.js | 4 ++-- .../DownloadClients/DownloadClientsConnector.js | 4 ++-- .../ImportLists/ImportListsConnector.js | 4 ++-- .../Indexers/Indexers/IndexersConnector.js | 4 ++-- .../Metadata/Metadata/MetadatasConnector.js | 4 ++-- .../Notifications/NotificationsConnector.js | 4 ++-- .../Profiles/Quality/QualityProfileFormatItems.js | 3 ++- .../Profiles/Quality/QualityProfilesConnector.js | 4 ++-- .../src/Settings/Tags/AutoTagging/AutoTaggings.js | 4 ++-- frontend/src/Store/Actions/discoverMovieActions.js | 10 +++++----- .../src/Store/Actions/movieCollectionActions.js | 4 ++-- frontend/src/Store/Actions/movieIndexActions.js | 14 +++++++------- frontend/src/Store/Actions/releaseActions.js | 4 ++-- .../createEnabledDownloadClientsSelector.ts | 8 ++++++-- .../Store/Selectors/createRootFoldersSelector.ts | 5 ++--- ...nSelector.js => createSortedSectionSelector.ts} | 8 ++++++-- .../System/Tasks/Queued/QueuedTaskRowNameCell.tsx | 5 ++--- frontend/src/Utilities/Array/sortByName.js | 5 ----- frontend/src/Utilities/Array/sortByProp.ts | 13 +++++++++++++ frontend/src/typings/Helpers/KeysMatching.ts | 7 +++++++ src/NzbDrone.Core/Localization/Core/en.json | 1 + 32 files changed, 96 insertions(+), 68 deletions(-) rename frontend/src/Store/Selectors/{createSortedSectionSelector.js => createSortedSectionSelector.ts} (68%) delete mode 100644 frontend/src/Utilities/Array/sortByName.js create mode 100644 frontend/src/Utilities/Array/sortByProp.ts create mode 100644 frontend/src/typings/Helpers/KeysMatching.ts diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 3eb7519f30..d2462d454b 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import SelectInput from 'Components/Form/SelectInput'; import IconButton from 'Components/Link/IconButton'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; +import sortByProp from 'Utilities/Array/sortByProp'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; @@ -228,7 +229,7 @@ class FilterBuilderRow extends Component { key: name, value: typeof label === 'function' ? label() : label }; - }).sort((a, b) => a.value.localeCompare(b.value)); + }).sort(sortByProp('value')); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index a7aed80b6b..d1419327a2 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { filterBuilderTypes } from 'Helpers/Props'; import * as filterTypes from 'Helpers/Props/filterTypes'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { @@ -38,7 +38,7 @@ function createTagListSelector() { } return acc; - }, []).sort(sortByName); + }, []).sort(sortByProp('name')); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/Builder/MovieFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/MovieFilterBuilderRowValue.tsx index 846ddda126..509d4e2a21 100644 --- a/frontend/src/Components/Filter/Builder/MovieFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/MovieFilterBuilderRowValue.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import Movie from 'Movie/Movie'; import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; @@ -11,7 +11,7 @@ function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) { const tagList = allMovies .map((movie) => ({ id: movie.id, name: movie.title })) - .sort(sortByName); + .sort(sortByProp('name')); return ; } diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index 6a9620ec40..3b0660040d 100644 --- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js +++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js @@ -5,6 +5,7 @@ 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 sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import CustomFilter from './CustomFilter'; import styles from './CustomFiltersModalContent.css'; @@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) { { customFilters - .sort((a, b) => a.label.localeCompare(b.label)) + .sort((a, b) => sortByProp(a, b, 'label')) .map((customFilter) => { return ( item.protocol === protocolFilter); - const values = _.map(filteredItems.sort(sortByName), (downloadClient) => { + const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => { return { key: downloadClient.id, value: downloadClient.name, @@ -33,7 +34,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: '(Any)' + value: `(${translate('Any')})` }); } diff --git a/frontend/src/Components/Form/IndexerSelectInputConnector.js b/frontend/src/Components/Form/IndexerSelectInputConnector.js index 375cc88585..e027ee3cfd 100644 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -3,7 +3,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchIndexers } from 'Store/Actions/settingsActions'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -18,7 +18,7 @@ function createMapStateToProps() { items } = indexers; - const values = items.sort(sortByName).map((indexer) => ({ + const values = items.sort(sortByProp('name')).map((indexer) => ({ key: indexer.id, value: indexer.name })); diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index 41ec0e0c76..d7719969a7 100644 --- a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js @@ -4,13 +4,13 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByName), + createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js index 4ee406224a..7bc23c066e 100644 --- a/frontend/src/Components/Menu/FilterMenuContent.js +++ b/frontend/src/Components/Menu/FilterMenuContent.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import FilterMenuItem from './FilterMenuItem'; import MenuContent from './MenuContent'; @@ -47,7 +48,7 @@ class FilterMenuContent extends Component { { customFilters - .sort((a, b) => a.label.localeCompare(b.label)) + .sort(sortByProp('label')) .map((filter) => { return ( tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .sort((a, b) => a.label.localeCompare(b.label)); + .sort(sortByProp('label')); return (
diff --git a/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx index 7ddaae57d9..7a00c7cf03 100644 --- a/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx +++ b/frontend/src/InteractiveImport/Movie/SelectMovieModalContent.tsx @@ -21,6 +21,7 @@ import { scrollDirections } from 'Helpers/Props'; import Movie from 'Movie/Movie'; import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector'; import dimensions from 'Styles/Variables/dimensions'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import SelectMovieModalTableHeader from './SelectMovieModalTableHeader'; import SelectMovieRow from './SelectMovieRow'; @@ -162,9 +163,7 @@ function SelectMovieModalContent(props: SelectMovieModalContentProps) { ); const items = useMemo(() => { - const sorted = [...allMovies].sort((a, b) => - a.sortTitle.localeCompare(b.sortTitle) - ); + const sorted = [...allMovies].sort(sortByProp('sortTitle')); return sorted.filter( (item) => diff --git a/frontend/src/Movie/Details/MovieTagsConnector.js b/frontend/src/Movie/Details/MovieTagsConnector.js index de9a67f2b8..f17e5ab267 100644 --- a/frontend/src/Movie/Details/MovieTagsConnector.js +++ b/frontend/src/Movie/Details/MovieTagsConnector.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import MovieTags from './MovieTags'; function createMapStateToProps() { @@ -12,8 +13,8 @@ function createMapStateToProps() { const tags = movie.tags .map((tagId) => tagList.find((tag) => tag.id === tagId)) .filter((tag) => !!tag) - .map((tag) => tag.label) - .sort((a, b) => a.localeCompare(b)); + .sort(sortByProp('label')) + .map((tag) => tag.label); return { tags diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js index 8e828620b2..0417d9b211 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import CustomFormats from './CustomFormats'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.customFormats', sortByName), + createSortedSectionSelector('settings.customFormats', sortByProp('name')), (customFormats) => customFormats ); } diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js index d9e5434691..0dc410fcbc 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import DownloadClients from './DownloadClients'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByName), + createSortedSectionSelector('settings.downloadClients', sortByProp('name')), createTagsSelector(), (downloadClients, tagList) => { return { diff --git a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js index 8cd132ab6e..017467e535 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import ImportLists from './ImportLists'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.importLists', sortByName), + createSortedSectionSelector('settings.importLists', sortByProp('name')), (importLists) => importLists ); } diff --git a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js index cb6e830fda..88c571a608 100644 --- a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js +++ b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Indexers from './Indexers'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.indexers', sortByName), + createSortedSectionSelector('settings.indexers', sortByProp('name')), createTagsSelector(), (indexers, tagList) => { return { diff --git a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js index fb52ac33b7..8675f4742d 100644 --- a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js +++ b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { fetchMetadata } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Metadatas from './Metadatas'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.metadata', sortByName), + createSortedSectionSelector('settings.metadata', sortByProp('name')), (metadata) => metadata ); } diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js index b306f742ab..6351c6f8ae 100644 --- a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import Notifications from './Notifications'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.notifications', sortByName), + createSortedSectionSelector('settings.notifications', sortByProp('name')), createTagsSelector(), (notifications, tagList) => { return { diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js index 7b90dec6c5..61cbefba1b 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileFormatItems.js @@ -20,7 +20,8 @@ function calcOrder(profileFormatItems) { if (b.score !== a.score) { return b.score - a.score; } - return a.name > b.name ? 1 : -1; + + return a.localeCompare(b.name, undefined, { numeric: true }); }).map((x) => items[x.format]); } diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js index 354b73e708..3a85f458c9 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -5,12 +5,12 @@ import { createSelector } from 'reselect'; import { fetchMovieCollections } from 'Store/Actions/movieCollectionActions'; import { cloneQualityProfile, deleteQualityProfile, fetchQualityProfiles } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import QualityProfiles from './QualityProfiles'; function createMapStateToProps() { return createSelector( - createSortedSectionSelector('settings.qualityProfiles', sortByName), + createSortedSectionSelector('settings.qualityProfiles', sortByProp('name')), (qualityProfiles) => qualityProfiles ); } diff --git a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js index 45c8e4b854..f27dc3b5a2 100644 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js +++ b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js @@ -9,7 +9,7 @@ import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { cloneAutoTagging, deleteAutoTagging, fetchAutoTaggings } from 'Store/Actions/settingsActions'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import AutoTagging from './AutoTagging'; import EditAutoTaggingModal from './EditAutoTaggingModal'; @@ -23,7 +23,7 @@ export default function AutoTaggings() { isFetching, isPopulated } = useSelector( - createSortedSectionSelector('settings.autoTaggings', sortByName) + createSortedSectionSelector('settings.autoTaggings', sortByProp('name')) ); const tagList = useSelector(createTagsSelector()); diff --git a/frontend/src/Store/Actions/discoverMovieActions.js b/frontend/src/Store/Actions/discoverMovieActions.js index 0dc53240c4..f113e3d9ee 100644 --- a/frontend/src/Store/Actions/discoverMovieActions.js +++ b/frontend/src/Store/Actions/discoverMovieActions.js @@ -4,7 +4,7 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getNewMovie from 'Utilities/Movie/getNewMovie'; import getSectionState from 'Utilities/State/getSectionState'; @@ -346,7 +346,7 @@ export const defaultState = { return acc; }, []); - return tagList.sort(sortByName); + return tagList.sort(sortByProp('name')); } }, { @@ -365,7 +365,7 @@ export const defaultState = { return acc; }, []); - return collectionList.sort(sortByName); + return collectionList.sort(sortByProp('name')); } }, { @@ -384,7 +384,7 @@ export const defaultState = { return acc; }, []); - return collectionList.sort(sortByName); + return collectionList.sort(sortByProp('name')); } }, { @@ -426,7 +426,7 @@ export const defaultState = { return acc; }, []); - return tagList.sort(sortByName); + return tagList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Actions/movieCollectionActions.js b/frontend/src/Store/Actions/movieCollectionActions.js index 8017229af6..7e912b9fd2 100644 --- a/frontend/src/Store/Actions/movieCollectionActions.js +++ b/frontend/src/Store/Actions/movieCollectionActions.js @@ -3,7 +3,7 @@ import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import getNewMovie from 'Utilities/Movie/getNewMovie'; import translate from 'Utilities/String/translate'; @@ -155,7 +155,7 @@ export const defaultState = { return acc; }, []); - return genreList.sort(sortByName); + return genreList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Actions/movieIndexActions.js b/frontend/src/Store/Actions/movieIndexActions.js index 7fc5e8c52a..e2b914a209 100644 --- a/frontend/src/Store/Actions/movieIndexActions.js +++ b/frontend/src/Store/Actions/movieIndexActions.js @@ -1,6 +1,6 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import createHandleActions from './Creators/createHandleActions'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; @@ -327,7 +327,7 @@ export const defaultState = { return acc; }, []); - return collectionList.sort(sortByName); + return collectionList.sort(sortByProp('name')); } }, { @@ -349,7 +349,7 @@ export const defaultState = { return acc; }, []); - return groupList.sort(sortByName); + return groupList.sort(sortByProp('name')); } }, { @@ -374,7 +374,7 @@ export const defaultState = { return acc; }, []); - return tagList.sort(sortByName); + return tagList.sort(sortByProp('name')); } }, { @@ -393,7 +393,7 @@ export const defaultState = { return acc; }, []); - return collectionList.sort(sortByName); + return collectionList.sort(sortByProp('name')); } }, { @@ -463,7 +463,7 @@ export const defaultState = { return acc; }, []); - return genreList.sort(sortByName); + return genreList.sort(sortByProp('name')); } }, { @@ -512,7 +512,7 @@ export const defaultState = { return acc; }, []); - return certificationList.sort(sortByName); + return certificationList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js index 8c8b151f35..43e9f20e9e 100644 --- a/frontend/src/Store/Actions/releaseActions.js +++ b/frontend/src/Store/Actions/releaseActions.js @@ -1,7 +1,7 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import translate from 'Utilities/String/translate'; import createFetchHandler from './Creators/createFetchHandler'; @@ -198,7 +198,7 @@ export const defaultState = { return acc; }, []); - return genreList.sort(sortByName); + return genreList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts index ac31e5210a..3a581587be 100644 --- a/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts +++ b/frontend/src/Store/Selectors/createEnabledDownloadClientsSelector.ts @@ -2,13 +2,17 @@ import { createSelector } from 'reselect'; import { DownloadClientAppState } from 'App/State/SettingsAppState'; import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import DownloadClient from 'typings/DownloadClient'; +import sortByProp from 'Utilities/Array/sortByProp'; export default function createEnabledDownloadClientsSelector( protocol: DownloadProtocol ) { return createSelector( - createSortedSectionSelector('settings.downloadClients', sortByName), + createSortedSectionSelector( + 'settings.downloadClients', + sortByProp('name') + ), (downloadClients: DownloadClientAppState) => { const { isFetching, isPopulated, error, items } = downloadClients; diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts index 7e01b57ecc..3eb486191d 100644 --- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts +++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts @@ -2,12 +2,11 @@ import { createSelector } from 'reselect'; import RootFolderAppState from 'App/State/RootFolderAppState'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import RootFolder from 'typings/RootFolder'; +import sortByProp from 'Utilities/Array/sortByProp'; export default function createRootFoldersSelector() { return createSelector( - createSortedSectionSelector('rootFolders', (a: RootFolder, b: RootFolder) => - a.path.localeCompare(b.path) - ), + createSortedSectionSelector('rootFolders', sortByProp('path')), (rootFolders: RootFolderAppState) => rootFolders ); } diff --git a/frontend/src/Store/Selectors/createSortedSectionSelector.js b/frontend/src/Store/Selectors/createSortedSectionSelector.ts similarity index 68% rename from frontend/src/Store/Selectors/createSortedSectionSelector.js rename to frontend/src/Store/Selectors/createSortedSectionSelector.ts index 331d890c92..abee01f75e 100644 --- a/frontend/src/Store/Selectors/createSortedSectionSelector.js +++ b/frontend/src/Store/Selectors/createSortedSectionSelector.ts @@ -1,14 +1,18 @@ import { createSelector } from 'reselect'; import getSectionState from 'Utilities/State/getSectionState'; -function createSortedSectionSelector(section, comparer) { +function createSortedSectionSelector( + section: string, + comparer: (a: T, b: T) => number +) { return createSelector( (state) => state, (state) => { const sectionState = getSectionState(state, section, true); + return { ...sectionState, - items: [...sectionState.items].sort(comparer) + items: [...sectionState.items].sort(comparer), }; } ); diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx index 89ff5da1fb..a349e20b74 100644 --- a/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx +++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowNameCell.tsx @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux'; import { CommandBody } from 'Commands/Command'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import createMultiMoviesSelector from 'Store/Selectors/createMultiMoviesSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import styles from './QueuedTaskRowNameCell.css'; @@ -39,9 +40,7 @@ export default function QueuedTaskRowNameCell( } const movies = useSelector(createMultiMoviesSelector(movieIds)); - const sortedMovies = movies.sort((a, b) => - a.sortTitle.localeCompare(b.sortTitle) - ); + const sortedMovies = movies.sort(sortByProp('sortTitle')); return ( diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js deleted file mode 100644 index 1956d3bac3..0000000000 --- a/frontend/src/Utilities/Array/sortByName.js +++ /dev/null @@ -1,5 +0,0 @@ -function sortByName(a, b) { - return a.name.localeCompare(b.name); -} - -export default sortByName; diff --git a/frontend/src/Utilities/Array/sortByProp.ts b/frontend/src/Utilities/Array/sortByProp.ts new file mode 100644 index 0000000000..8fbde08c9f --- /dev/null +++ b/frontend/src/Utilities/Array/sortByProp.ts @@ -0,0 +1,13 @@ +import { StringKey } from 'typings/Helpers/KeysMatching'; + +export function sortByProp< + // eslint-disable-next-line no-use-before-define + T extends Record, + K extends StringKey +>(sortKey: K) { + return (a: T, b: T) => { + return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true }); + }; +} + +export default sortByProp; diff --git a/frontend/src/typings/Helpers/KeysMatching.ts b/frontend/src/typings/Helpers/KeysMatching.ts new file mode 100644 index 0000000000..0e20206ef2 --- /dev/null +++ b/frontend/src/typings/Helpers/KeysMatching.ts @@ -0,0 +1,7 @@ +type KeysMatching = { + [K in keyof T]-?: T[K] extends V ? K : never; +}[keyof T]; + +export type StringKey = KeysMatching; + +export default KeysMatching; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 5e33f065f6..e36a5658f3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -71,6 +71,7 @@ "AnalyticsEnabledHelpText": "Send anonymous usage and error information to {appName}'s servers. This includes information on your browser, which {appName} WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes.", "Announced": "Announced", "AnnouncedMsg": "Movie is announced", + "Any": "Any", "ApiKey": "API Key", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {length} characters long. You can do this via settings or the config file", "AppDataDirectory": "AppData Directory",