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

(cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746)
This commit is contained in:
Mark McDowall
2024-07-16 21:34:43 -07:00
committed by Bogdan
parent ab289b3e42
commit 76f30e7682
19 changed files with 63 additions and 34 deletions

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector'; import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue'; import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
@@ -212,7 +213,7 @@ class FilterBuilderRow extends Component {
key: name, key: name,
value: typeof label === 'function' ? label() : label value: typeof label === 'function' ? label() : label
}; };
}).sort((a, b) => a.value.localeCompare(b.value)); }).sort(sortByProp('value'));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp); const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { filterBuilderTypes } from 'Helpers/Props'; import { filterBuilderTypes } from 'Helpers/Props';
import * as filterTypes from 'Helpers/Props/filterTypes'; import * as filterTypes from 'Helpers/Props/filterTypes';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValue from './FilterBuilderRowValue';
function createTagListSelector() { function createTagListSelector() {
@@ -38,7 +38,7 @@ function createTagListSelector() {
} }
return acc; return acc;
}, []).sort(sortByName); }, []).sort(sortByProp('name'));
} }
return _.uniqBy(items, 'id'); return _.uniqBy(items, 'id');

View File

@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import CustomFilter from './CustomFilter'; import CustomFilter from './CustomFilter';
import styles from './CustomFiltersModalContent.css'; import styles from './CustomFiltersModalContent.css';
@@ -31,7 +32,7 @@ function CustomFiltersModalContent(props) {
<ModalBody> <ModalBody>
{ {
customFilters customFilters
.sort((a, b) => a.label.localeCompare(b.label)) .sort((a, b) => sortByProp(a, b, 'label'))
.map((customFilter) => { .map((customFilter) => {
return ( return (
<CustomFilter <CustomFilter

View File

@@ -4,13 +4,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; 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 translate from 'Utilities/String/translate';
import SelectInput from './SelectInput'; import SelectInput from './SelectInput';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.appProfiles', sortByName), createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
(state, { includeNoChange }) => includeNoChange, (state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed, (state, { includeMixed }) => includeMixed,
(appProfiles, includeNoChange, includeMixed) => { (appProfiles, includeNoChange, includeMixed) => {

View File

@@ -3,7 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions'; 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'; import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() { function createMapStateToProps() {
@@ -21,7 +22,7 @@ function createMapStateToProps() {
const values = items const values = items
.filter((downloadClient) => downloadClient.protocol === protocolFilter) .filter((downloadClient) => downloadClient.protocol === protocolFilter)
.sort(sortByName) .sort(sortByProp('name'))
.map((downloadClient) => ({ .map((downloadClient) => ({
key: downloadClient.id, key: downloadClient.id,
value: downloadClient.name, value: downloadClient.name,
@@ -31,7 +32,7 @@ function createMapStateToProps() {
if (includeAny) { if (includeAny) {
values.unshift({ values.unshift({
key: 0, key: 0,
value: '(Any)' value: `(${translate('Any')})`
}); });
} }

View File

@@ -4,14 +4,14 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { value }) => value, (state, { value }) => value,
createSortedSectionSelector('indexers', sortByName), createSortedSectionSelector('indexers', sortByProp('name')),
(value, indexers) => { (value, indexers) => {
const values = []; const values = [];
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val })); const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import FilterMenuItem from './FilterMenuItem'; import FilterMenuItem from './FilterMenuItem';
import MenuContent from './MenuContent'; import MenuContent from './MenuContent';
@@ -47,7 +48,7 @@ class FilterMenuContent extends Component {
{ {
customFilters customFilters
.sort((a, b) => a.label.localeCompare(b.label)) .sort(sortByProp('label'))
.map((filter) => { .map((filter) => {
return ( return (
<FilterMenuItem <FilterMenuItem

View File

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

View File

@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions'; import { deleteApplication, fetchApplications } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import Applications from './Applications'; import Applications from './Applications';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.applications', sortByName), createSortedSectionSelector('settings.applications', sortByProp('name')),
createTagsSelector(), createTagsSelector(),
(applications, tagList) => { (applications, tagList) => {
return { return {

View File

@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions'; import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import DownloadClients from './DownloadClients'; import DownloadClients from './DownloadClients';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName), createSortedSectionSelector('settings.downloadClients', sortByProp('name')),
(downloadClients) => downloadClients (downloadClients) => downloadClients
); );
} }

View File

@@ -5,13 +5,13 @@ import { createSelector } from 'reselect';
import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions'; import { deleteIndexerProxy, fetchIndexerProxies } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import IndexerProxies from './IndexerProxies'; import IndexerProxies from './IndexerProxies';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.indexerProxies', sortByName), createSortedSectionSelector('settings.indexerProxies', sortByProp('name')),
createSortedSectionSelector('indexers', sortByName), createSortedSectionSelector('indexers', sortByProp('name')),
createTagsSelector(), createTagsSelector(),
(indexerProxies, indexers, tagList) => { (indexerProxies, indexers, tagList) => {
return { return {

View File

@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions'; import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector'; import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import Notifications from './Notifications'; import Notifications from './Notifications';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.notifications', sortByName), createSortedSectionSelector('settings.notifications', sortByProp('name')),
createTagsSelector(), createTagsSelector(),
(notifications, tagList) => { (notifications, tagList) => {
return { return {

View File

@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions'; import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName'; import sortByProp from 'Utilities/Array/sortByProp';
import AppProfiles from './AppProfiles'; import AppProfiles from './AppProfiles';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.appProfiles', sortByName), createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
(appProfiles) => appProfiles (appProfiles) => appProfiles
); );
} }

View File

@@ -2,13 +2,17 @@ import { createSelector } from 'reselect';
import { DownloadClientAppState } from 'App/State/SettingsAppState'; import { DownloadClientAppState } from 'App/State/SettingsAppState';
import DownloadProtocol from 'DownloadClient/DownloadProtocol'; import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; 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( export default function createEnabledDownloadClientsSelector(
protocol: DownloadProtocol protocol: DownloadProtocol
) { ) {
return createSelector( return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName), createSortedSectionSelector<DownloadClient>(
'settings.downloadClients',
sortByProp('name')
),
(downloadClients: DownloadClientAppState) => { (downloadClients: DownloadClientAppState) => {
const { isFetching, isPopulated, error, items } = downloadClients; const { isFetching, isPopulated, error, items } = downloadClients;

View File

@@ -1,14 +1,18 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
function createSortedSectionSelector(section, comparer) { function createSortedSectionSelector<T>(
section: string,
comparer: (a: T, b: T) => number
) {
return createSelector( return createSelector(
(state) => state, (state) => state,
(state) => { (state) => {
const sectionState = getSectionState(state, section, true); const sectionState = getSectionState(state, section, true);
return { return {
...sectionState, ...sectionState,
items: [...sectionState.items].sort(comparer) items: [...sectionState.items].sort(comparer),
}; };
} }
); );

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

@@ -33,6 +33,7 @@
"AllIndexersHiddenDueToFilter": "All indexers are hidden due to applied filter.", "AllIndexersHiddenDueToFilter": "All indexers are hidden due to applied filter.",
"Analytics": "Analytics", "Analytics": "Analytics",
"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.", "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.",
"Any": "Any",
"ApiKey": "API Key", "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", "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", "AppDataDirectory": "AppData Directory",