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 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);
|
||||||
|
|
||||||
|
@@ -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');
|
||||||
|
@@ -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
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -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')})`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 }));
|
||||||
|
@@ -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
|
||||||
|
@@ -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}>
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
|
||||||
|
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
@@ -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.",
|
"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",
|
||||||
|
Reference in New Issue
Block a user