mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Use natural sorting for lists of items in the UI
(cherry picked from commit 1a1c8e6c08a6db5fcd2b5d17e65fa1f943d2e746)
This commit is contained in:
@@ -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 AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||
@@ -212,7 +213,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);
|
||||
|
||||
|
@@ -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');
|
||||
|
@@ -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
|
||||
|
@@ -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 SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(appProfiles, includeNoChange, includeMixed) => {
|
||||
|
@@ -3,7 +3,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() {
|
||||
@@ -21,7 +22,7 @@ function createMapStateToProps() {
|
||||
|
||||
const values = items
|
||||
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
|
||||
.sort(sortByName)
|
||||
.sort(sortByProp('name'))
|
||||
.map((downloadClient) => ({
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name,
|
||||
@@ -31,7 +32,7 @@ function createMapStateToProps() {
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -4,14 +4,14 @@ 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 titleCase from 'Utilities/String/titleCase';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||
(value, indexers) => {
|
||||
const values = [];
|
||||
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
|
||||
|
@@ -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
|
||||
|
@@ -1,14 +1,15 @@
|
||||
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';
|
||||
|
||||
function TagList({ tags, tagList }) {
|
||||
const sortedTags = tags
|
||||
.map((tagId) => tagList.find((tag) => tag.id === tagId))
|
||||
.filter((t) => t !== undefined)
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
.filter((tag) => !!tag)
|
||||
.sort(sortByProp('label'));
|
||||
|
||||
return (
|
||||
<div className={styles.tags}>
|
||||
|
@@ -5,12 +5,12 @@ import { createSelector } from 'reselect';
|
||||
import { deleteApplication, fetchApplications } 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 Applications from './Applications';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.applications', sortByName),
|
||||
createSortedSectionSelector('settings.applications', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(applications, tagList) => {
|
||||
return {
|
||||
|
@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
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')),
|
||||
(downloadClients) => downloadClients
|
||||
);
|
||||
}
|
||||
|
@@ -5,13 +5,13 @@ import { createSelector } from 'reselect';
|
||||
import { deleteIndexerProxy, fetchIndexerProxies } 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 IndexerProxies from './IndexerProxies';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.indexerProxies', sortByName),
|
||||
createSortedSectionSelector('indexers', sortByName),
|
||||
createSortedSectionSelector('settings.indexerProxies', sortByProp('name')),
|
||||
createSortedSectionSelector('indexers', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(indexerProxies, indexers, tagList) => {
|
||||
return {
|
||||
|
@@ -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 {
|
||||
|
@@ -4,12 +4,12 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneAppProfile, deleteAppProfile, fetchAppProfiles } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import AppProfiles from './AppProfiles';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.appProfiles', sortByName),
|
||||
createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
|
||||
(appProfiles) => appProfiles
|
||||
);
|
||||
}
|
||||
|
@@ -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<DownloadClient>(
|
||||
'settings.downloadClients',
|
||||
sortByProp('name')
|
||||
),
|
||||
(downloadClients: DownloadClientAppState) => {
|
||||
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||
|
||||
|
@@ -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),
|
||||
};
|
||||
}
|
||||
);
|
@@ -1,5 +0,0 @@
|
||||
function sortByName(a, b) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export default sortByName;
|
13
frontend/src/Utilities/Array/sortByProp.ts
Normal file
13
frontend/src/Utilities/Array/sortByProp.ts
Normal 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;
|
7
frontend/src/typings/Helpers/KeysMatching.ts
Normal file
7
frontend/src/typings/Helpers/KeysMatching.ts
Normal 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;
|
@@ -33,6 +33,7 @@
|
||||
"AllIndexersHiddenDueToFilter": "All indexers are hidden due to applied filter.",
|
||||
"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.",
|
||||
"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",
|
||||
|
Reference in New Issue
Block a user