From 7f73a2e23aa57696c0fb11cd05e6e8badfd21bd6 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 #4912 Closes #4921 --- .../src/Artist/Details/ArtistTagsConnector.js | 5 ++-- .../Builder/ArtistFilterBuilderRowValue.tsx | 4 +-- .../Filter/Builder/FilterBuilderRow.js | 3 ++- .../Builder/FilterBuilderRowValueConnector.js | 4 +-- .../CustomFiltersModalContent.js | 3 ++- .../DownloadClientSelectInputConnector.js | 7 ++--- .../Form/IndexerSelectInputConnector.js | 7 ++--- .../MetadataProfileSelectInputConnector.js | 4 +-- .../QualityProfileSelectInputConnector.js | 4 +-- .../src/Components/Menu/FilterMenuContent.js | 3 ++- frontend/src/Components/TagList.js | 3 ++- .../CustomFormats/CustomFormatsConnector.js | 4 +-- .../DownloadClientsConnector.js | 4 +-- .../ImportLists/ImportListsConnector.js | 4 +-- .../Indexers/Indexers/IndexersConnector.js | 4 +-- .../Metadata/Metadata/MetadatasConnector.js | 4 +-- .../Notifications/NotificationsConnector.js | 4 +-- .../Profiles/Metadata/MetadataProfiles.js | 27 ++++++++++--------- .../Quality/QualityProfileFormatItems.js | 3 ++- .../Quality/QualityProfilesConnector.js | 4 +-- .../Settings/Tags/AutoTagging/AutoTaggings.js | 4 +-- .../src/Store/Actions/artistIndexActions.js | 4 +-- .../Selectors/createRootFoldersSelector.ts | 5 ++-- ...ctor.js => createSortedSectionSelector.ts} | 8 ++++-- .../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 + 29 files changed, 94 insertions(+), 63 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/Artist/Details/ArtistTagsConnector.js b/frontend/src/Artist/Details/ArtistTagsConnector.js index 33ced5f0d..1d24a5755 100644 --- a/frontend/src/Artist/Details/ArtistTagsConnector.js +++ b/frontend/src/Artist/Details/ArtistTagsConnector.js @@ -2,6 +2,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import sortByProp from 'Utilities/Array/sortByProp'; import ArtistTags from './ArtistTags'; function createMapStateToProps() { @@ -12,8 +13,8 @@ function createMapStateToProps() { const tags = artist.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/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx index 003206961..486027f35 100644 --- a/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx +++ b/frontend/src/Components/Filter/Builder/ArtistFilterBuilderRowValue.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useSelector } from 'react-redux'; import Artist from 'Artist/Artist'; import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; -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 ArtistFilterBuilderRowValue(props: FilterBuilderRowValueProps) { const tagList = allArtists .map((artist) => ({ id: artist.id, name: artist.artistName })) - .sort(sortByName); + .sort(sortByProp('name')); return ; } diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 8be71aaff..3ffebeee8 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 ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue'; import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; @@ -224,7 +225,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 a7aed80b6..d1419327a 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/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js index cd9c07053..d70b97e44 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 cd58270eb..5f62becbb 100644 --- a/frontend/src/Components/Form/IndexerSelectInputConnector.js +++ b/frontend/src/Components/Form/IndexerSelectInputConnector.js @@ -4,7 +4,8 @@ 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 translate from 'Utilities/String/translate'; import EnhancedSelectInput from './EnhancedSelectInput'; function createMapStateToProps() { @@ -19,7 +20,7 @@ function createMapStateToProps() { items } = indexers; - const values = _.map(items.sort(sortByName), (indexer) => { + const values = _.map(items.sort(sortByProp('name')), (indexer) => { return { key: indexer.id, value: indexer.name @@ -29,7 +30,7 @@ function createMapStateToProps() { if (includeAny) { values.unshift({ key: 0, - value: '(Any)' + value: `(${translate('Any')})` }); } diff --git a/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js b/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js index 2cb7a39c1..6e6aad5f9 100644 --- a/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js +++ b/frontend/src/Components/Form/MetadataProfileSelectInputConnector.js @@ -5,13 +5,13 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { metadataProfileNames } from 'Helpers/Props'; 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.metadataProfiles', sortByName), + createSortedSectionSelector('settings.metadataProfiles', sortByProp('name')), (state, { includeNoChange }) => includeNoChange, (state, { includeNoChangeDisabled }) => includeNoChangeDisabled, (state, { includeMixed }) => includeMixed, diff --git a/frontend/src/Components/Form/QualityProfileSelectInputConnector.js b/frontend/src/Components/Form/QualityProfileSelectInputConnector.js index 41ec0e0c7..d7719969a 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 4ee406224..7bc23c066 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/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js index 8e828620b..0417d9b21 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 d9e543469..0dc410fcb 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 5c6bad8e7..5eb47068d 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/ImportListsConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { deleteImportList, fetchImportLists, fetchRootFolders } 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 cb6e830fd..88c571a60 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 fb52ac33b..8675f4742 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 b306f742a..6351c6f8a 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/Metadata/MetadataProfiles.js b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js index d39303ecc..5e719517f 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js @@ -5,7 +5,7 @@ import FieldSet from 'Components/FieldSet'; import Icon from 'Components/Icon'; import PageSectionContent from 'Components/Page/PageSectionContent'; import { icons, metadataProfileNames } from 'Helpers/Props'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import EditMetadataProfileModalConnector from './EditMetadataProfileModalConnector'; import MetadataProfile from './MetadataProfile'; @@ -59,17 +59,20 @@ class MetadataProfiles extends Component { >
{ - items.filter((item) => item.name !== metadataProfileNames.NONE).sort(sortByName).map((item) => { - return ( - - ); - }) + items + .filter((item) => item.name !== metadataProfileNames.NONE) + .sort(sortByProp('name')) + .map((item) => { + return ( + + ); + }) } b.name ? 1 : -1; + + return a.name.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 581882ffd..4cb318463 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -4,12 +4,12 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; 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 0d86721af..005547bb7 100644 --- a/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js +++ b/frontend/src/Settings/Tags/AutoTagging/AutoTaggings.js @@ -13,7 +13,7 @@ import { } 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'; @@ -27,7 +27,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/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js index 72cb20142..055e92662 100644 --- a/frontend/src/Store/Actions/artistIndexActions.js +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -1,6 +1,6 @@ import { createAction } from 'redux-actions'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypePredicates, sortDirections } from 'Helpers/Props'; -import sortByName from 'Utilities/Array/sortByName'; +import sortByProp from 'Utilities/Array/sortByProp'; import translate from 'Utilities/String/translate'; import { filterPredicates, filters, sortPredicates } from './artistActions'; import createHandleActions from './Creators/createHandleActions'; @@ -334,7 +334,7 @@ export const defaultState = { return acc; }, []); - return tagList.sort(sortByName); + return tagList.sort(sortByProp('name')); } }, { diff --git a/frontend/src/Store/Selectors/createRootFoldersSelector.ts b/frontend/src/Store/Selectors/createRootFoldersSelector.ts index a016d7665..ba61e0ec6 100644 --- a/frontend/src/Store/Selectors/createRootFoldersSelector.ts +++ b/frontend/src/Store/Selectors/createRootFoldersSelector.ts @@ -1,11 +1,12 @@ import { createSelector } from 'reselect'; import { RootFolderAppState } from 'App/State/SettingsAppState'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; -import sortByName from 'Utilities/Array/sortByName'; +import RootFolder from 'typings/RootFolder'; +import sortByProp from 'Utilities/Array/sortByProp'; export default function createRootFoldersSelector() { return createSelector( - createSortedSectionSelector('settings.rootFolders', sortByName), + createSortedSectionSelector('rootFolders', sortByProp('name')), (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 331d890c9..abee01f75 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 77142c5a3..41a307d5f 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 createMultiArtistsSelector from 'Store/Selectors/createMultiArtistsSelector'; +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 artists = useSelector(createMultiArtistsSelector(movieIds)); - const sortedArtists = artists.sort((a, b) => - a.sortName.localeCompare(b.sortName) - ); + const sortedArtists = artists.sort(sortByProp('sortName')); return ( diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js deleted file mode 100644 index 1956d3bac..000000000 --- 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 000000000..8fbde08c9 --- /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 000000000..0e20206ef --- /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 642d99a2a..4434790a1 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -89,6 +89,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.", "AnalyticsEnabledHelpTextWarning": "Requires restart to take effect", "AnchorTooltip": "This file is already in your library for a release you are currently importing", + "Any": "Any", "AnyReleaseOkHelpText": "{appName} will automatically switch to the release best matching downloaded tracks", "ApiKeyHelpTextWarning": "Requires restart to take effect", "ApiKeyValidationHealthCheckMessage": "Please update your API key to be at least {0} characters long. You can do this via settings or the config file",