1
0
Fork 0
mirror of https://github.com/lidarr/Lidarr synced 2024-12-21 23:32:27 +00:00

New: Use natural sorting for lists of items in the UI

(cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746)

Closes #4912
Closes #4921
This commit is contained in:
Mark McDowall 2024-07-16 21:34:43 -07:00 committed by Bogdan
parent a9dd947eed
commit 7f73a2e23a
29 changed files with 94 additions and 63 deletions

View file

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

View file

@ -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 <FilterBuilderRowValue {...props} tagList={tagList} />;
}

View file

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

View file

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

View file

@ -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) {
<ModalBody>
{
customFilters
.sort((a, b) => a.label.localeCompare(b.label))
.sort((a, b) => sortByProp(a, b, 'label'))
.map((customFilter) => {
return (
<CustomFilter

View file

@ -4,7 +4,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } 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() {
@ -22,7 +23,7 @@ function createMapStateToProps() {
const filteredItems = items.filter((item) => 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')})`
});
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import Label from './Label';
import styles from './TagList.css';
@ -8,7 +9,7 @@ function TagList({ tags, tagList }) {
const sortedTags = tags
.map((tagId) => tagList.find((tag) => tag.id === tagId))
.filter((tag) => !!tag)
.sort((a, b) => a.label.localeCompare(b.label));
.sort(sortByProp('label'));
return (
<div className={styles.tags}>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {
>
<div className={styles.metadataProfiles}>
{
items.filter((item) => item.name !== metadataProfileNames.NONE).sort(sortByName).map((item) => {
return (
<MetadataProfile
key={item.id}
{...item}
isDeleting={isDeleting}
onConfirmDeleteMetadataProfile={onConfirmDeleteMetadataProfile}
onCloneMetadataProfilePress={this.onCloneMetadataProfilePress}
/>
);
})
items
.filter((item) => item.name !== metadataProfileNames.NONE)
.sort(sortByProp('name'))
.map((item) => {
return (
<MetadataProfile
key={item.id}
{...item}
isDeleting={isDeleting}
onConfirmDeleteMetadataProfile={onConfirmDeleteMetadataProfile}
onCloneMetadataProfilePress={this.onCloneMetadataProfilePress}
/>
);
})
}
<Card

View file

@ -19,7 +19,8 @@ function calcOrder(profileFormatItems) {
if (b.score !== a.score) {
return b.score - a.score;
}
return a.name > b.name ? 1 : -1;
return a.name.localeCompare(b.name, undefined, { numeric: true });
}).map((x) => items[x.format]);
}

View file

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

View file

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

View file

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

View file

@ -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<RootFolder>('rootFolders', sortByProp('name')),
(rootFolders: RootFolderAppState) => rootFolders
);
}

View file

@ -1,14 +1,18 @@
import { createSelector } from 'reselect';
import getSectionState from 'Utilities/State/getSectionState';
function createSortedSectionSelector(section, comparer) {
function createSortedSectionSelector<T>(
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),
};
}
);

View file

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

View file

@ -1,5 +0,0 @@
function sortByName(a, b) {
return a.name.localeCompare(b.name);
}
export default sortByName;

View file

@ -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, string>,
K extends StringKey<T>
>(sortKey: K) {
return (a: T, b: T) => {
return a[sortKey].localeCompare(b[sortKey], undefined, { numeric: true });
};
}
export default sortByProp;

View file

@ -0,0 +1,7 @@
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
export type StringKey<T> = KeysMatching<T, string>;
export default KeysMatching;

View file

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