mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-13 08:33:58 -07:00
New: Queue custom filters
(cherry picked from commit e357d17b187378b92377f8acb077b12c1e7ea527) Closes #4212 Closes #4234
This commit is contained in:
parent
27e3aa76bd
commit
a356b01efd
15 changed files with 235 additions and 16 deletions
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||||
|
@ -155,11 +157,16 @@ class Queue extends Component {
|
||||||
isAlbumsPopulated,
|
isAlbumsPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
columns,
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
count,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isGrabbing,
|
isGrabbing,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
|
onFilterSelect,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -222,6 +229,15 @@ class Queue extends Component {
|
||||||
iconName={icons.TABLE}
|
iconName={icons.TABLE}
|
||||||
/>
|
/>
|
||||||
</TableOptionsModalWrapper>
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -243,7 +259,11 @@ class Queue extends Component {
|
||||||
{
|
{
|
||||||
isAllPopulated && !hasError && !items.length ?
|
isAllPopulated && !hasError && !items.length ?
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('QueueIsEmpty')}
|
{
|
||||||
|
selectedFilterKey !== 'all' && count > 0 ?
|
||||||
|
translate('QueueFilterHasNoItems') :
|
||||||
|
translate('QueueIsEmpty')
|
||||||
|
}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
@ -329,13 +349,18 @@ Queue.propTypes = {
|
||||||
isAlbumsPopulated: PropTypes.bool.isRequired,
|
isAlbumsPopulated: PropTypes.bool.isRequired,
|
||||||
albumsError: PropTypes.object,
|
albumsError: PropTypes.object,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
count: PropTypes.number.isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
Queue.defaultProps = {
|
Queue.defaultProps = {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
|
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
import * as queueActions from 'Store/Actions/queueActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
@ -19,14 +20,18 @@ function createMapStateToProps() {
|
||||||
(state) => state.albums,
|
(state) => state.albums,
|
||||||
(state) => state.queue.options,
|
(state) => state.queue.options,
|
||||||
(state) => state.queue.paged,
|
(state) => state.queue.paged,
|
||||||
|
(state) => state.queue.status.item,
|
||||||
|
createCustomFiltersSelector('queue'),
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||||
(artist, albums, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
(artist, albums, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||||
return {
|
return {
|
||||||
|
count: options.includeUnknownArtistItems ? status.totalCount : status.count,
|
||||||
isArtistFetching: artist.isFetching,
|
isArtistFetching: artist.isFetching,
|
||||||
isArtistPopulated: artist.isPopulated,
|
isArtistPopulated: artist.isPopulated,
|
||||||
isAlbumsFetching: albums.isFetching,
|
isAlbumsFetching: albums.isFetching,
|
||||||
isAlbumsPopulated: albums.isPopulated,
|
isAlbumsPopulated: albums.isPopulated,
|
||||||
albumsError: albums.error,
|
albumsError: albums.error,
|
||||||
|
customFilters,
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
...options,
|
...options,
|
||||||
...queue
|
...queue
|
||||||
|
@ -125,6 +130,10 @@ class QueueConnector extends Component {
|
||||||
this.props.setQueueSort({ sortKey });
|
this.props.setQueueSort({ sortKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setQueueFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
onTableOptionChange = (payload) => {
|
||||||
this.props.setQueueTableOption(payload);
|
this.props.setQueueTableOption(payload);
|
||||||
|
|
||||||
|
@ -159,6 +168,7 @@ class QueueConnector extends Component {
|
||||||
onLastPagePress={this.onLastPagePress}
|
onLastPagePress={this.onLastPagePress}
|
||||||
onPageSelect={this.onPageSelect}
|
onPageSelect={this.onPageSelect}
|
||||||
onSortPress={this.onSortPress}
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
onRefreshPress={this.onRefreshPress}
|
onRefreshPress={this.onRefreshPress}
|
||||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||||
|
@ -181,6 +191,7 @@ QueueConnector.propTypes = {
|
||||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||||
gotoQueuePage: PropTypes.func.isRequired,
|
gotoQueuePage: PropTypes.func.isRequired,
|
||||||
setQueueSort: PropTypes.func.isRequired,
|
setQueueSort: PropTypes.func.isRequired,
|
||||||
|
setQueueFilter: PropTypes.func.isRequired,
|
||||||
setQueueTableOption: PropTypes.func.isRequired,
|
setQueueTableOption: PropTypes.func.isRequired,
|
||||||
clearQueue: PropTypes.func.isRequired,
|
clearQueue: PropTypes.func.isRequired,
|
||||||
grabQueueItems: PropTypes.func.isRequired,
|
grabQueueItems: PropTypes.func.isRequired,
|
||||||
|
|
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||||
|
|
||||||
|
function createQueueSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createQueueSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'queue';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setQueueFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import { FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
|
@ -20,6 +21,10 @@ export interface PagedAppSectionState {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionFilterState<T> {
|
||||||
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppSectionSchemaState<T> {
|
export interface AppSectionSchemaState<T> {
|
||||||
isSchemaFetching: boolean;
|
isSchemaFetching: boolean;
|
||||||
isSchemaPopulated: boolean;
|
isSchemaPopulated: boolean;
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import ModelBase from 'App/ModelBase';
|
import ModelBase from 'App/ModelBase';
|
||||||
import { QualityModel } from 'Quality/Quality';
|
import { QualityModel } from 'Quality/Quality';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
AppSectionItemState,
|
||||||
|
Error,
|
||||||
|
} from './AppSectionState';
|
||||||
|
|
||||||
export interface StatusMessage {
|
export interface StatusMessage {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -33,7 +37,9 @@ export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
export interface QueuePagedAppState
|
||||||
|
extends AppSectionState<Queue>,
|
||||||
|
AppSectionFilterState<Queue> {
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
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 FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
function ArtistFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
const allArtists: Artist[] = useSelector(createAllArtistSelector());
|
||||||
|
|
||||||
|
const tagList = allArtists
|
||||||
|
.map((artist) => ({ id: artist.id, name: artist.artistName }))
|
||||||
|
.sort(sortByName);
|
||||||
|
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ArtistFilterBuilderRowValue;
|
|
@ -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 ArtistFilterBuilderRowValue from './ArtistFilterBuilderRowValue';
|
||||||
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
|
@ -72,6 +73,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValueConnector;
|
return QualityProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.ARTIST:
|
||||||
|
return ArtistFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.ARTIST_STATUS:
|
case filterBuilderValueTypes.ARTIST_STATUS:
|
||||||
return ArtistStatusFilterBuilderRowValue;
|
return ArtistStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { FilterBuilderProp } from 'App/State/AppState';
|
||||||
|
|
||||||
|
interface FilterBuilderRowOnChangeProps {
|
||||||
|
name: string;
|
||||||
|
value: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterBuilderRowValueProps {
|
||||||
|
filterType?: string;
|
||||||
|
filterValue: string | number | object | string[] | number[] | object[];
|
||||||
|
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||||
|
sectionItem: unknown[];
|
||||||
|
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterBuilderRowValueProps;
|
|
@ -1,14 +1,18 @@
|
||||||
import * as filterTypes from './filterTypes';
|
import * as filterTypes from './filterTypes';
|
||||||
|
|
||||||
export const ARRAY = 'array';
|
export const ARRAY = 'array';
|
||||||
|
export const CONTAINS = 'contains';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
|
export const EQUAL = 'equal';
|
||||||
export const EXACT = 'exact';
|
export const EXACT = 'exact';
|
||||||
export const NUMBER = 'number';
|
export const NUMBER = 'number';
|
||||||
export const STRING = 'string';
|
export const STRING = 'string';
|
||||||
|
|
||||||
export const all = [
|
export const all = [
|
||||||
ARRAY,
|
ARRAY,
|
||||||
|
CONTAINS,
|
||||||
DATE,
|
DATE,
|
||||||
|
EQUAL,
|
||||||
EXACT,
|
EXACT,
|
||||||
NUMBER,
|
NUMBER,
|
||||||
STRING
|
STRING
|
||||||
|
@ -20,6 +24,10 @@ export const possibleFilterTypes = {
|
||||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
[CONTAINS]: [
|
||||||
|
{ key: filterTypes.CONTAINS, value: 'contains' }
|
||||||
|
],
|
||||||
|
|
||||||
[DATE]: [
|
[DATE]: [
|
||||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||||
|
@ -29,6 +37,10 @@ export const possibleFilterTypes = {
|
||||||
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
||||||
],
|
],
|
||||||
|
|
||||||
|
[EQUAL]: [
|
||||||
|
{ key: filterTypes.EQUAL, value: 'is' }
|
||||||
|
],
|
||||||
|
|
||||||
[EXACT]: [
|
[EXACT]: [
|
||||||
{ key: filterTypes.EQUAL, value: 'is' },
|
{ key: filterTypes.EQUAL, value: 'is' },
|
||||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||||
|
|
|
@ -7,5 +7,6 @@ export const METADATA_PROFILE = 'metadataProfile';
|
||||||
export const PROTOCOL = 'protocol';
|
export const PROTOCOL = 'protocol';
|
||||||
export const QUALITY = 'quality';
|
export const QUALITY = 'quality';
|
||||||
export const QUALITY_PROFILE = 'qualityProfile';
|
export const QUALITY_PROFILE = 'qualityProfile';
|
||||||
|
export const ARTIST = 'artist';
|
||||||
export const ARTIST_STATUS = 'artistStatus';
|
export const ARTIST_STATUS = 'artistStatus';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
|
|
|
@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
|
||||||
import { set, updateServerSideCollection } from '../baseActions';
|
import { set, updateServerSideCollection } from '../baseActions';
|
||||||
|
|
||||||
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
||||||
|
const [baseSection] = section.split('.');
|
||||||
|
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
dispatch(set({ section, isFetching: true }));
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
|
@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters
|
||||||
customFilters
|
|
||||||
} = sectionState;
|
} = sectionState;
|
||||||
|
|
||||||
|
const customFilters = getState().customFilters.items.filter((customFilter) => {
|
||||||
|
return customFilter.type === section || customFilter.type === baseSection;
|
||||||
|
});
|
||||||
|
|
||||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||||
|
|
||||||
selectedFilters.forEach((filter) => {
|
selectedFilters.forEach((filter) => {
|
||||||
|
@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url,
|
url,
|
||||||
data
|
data,
|
||||||
|
traditional: true
|
||||||
}).request;
|
}).request;
|
||||||
|
|
||||||
promise.done((response) => {
|
promise.done((response) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
|
@ -158,6 +158,37 @@ export const defaultState = {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'all',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: 'All',
|
||||||
|
filters: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'artistIds',
|
||||||
|
label: () => translate('Artist'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.ARTIST
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: () => translate('Quality'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.QUALITY
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'protocol',
|
||||||
|
label: () => translate('Protocol'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.PROTOCOL
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -167,7 +198,8 @@ export const persistState = [
|
||||||
'queue.paged.pageSize',
|
'queue.paged.pageSize',
|
||||||
'queue.paged.sortKey',
|
'queue.paged.sortKey',
|
||||||
'queue.paged.sortDirection',
|
'queue.paged.sortDirection',
|
||||||
'queue.paged.columns'
|
'queue.paged.columns',
|
||||||
|
'queue.paged.selectedFilterKey'
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -192,6 +224,7 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage';
|
||||||
export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage';
|
export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage';
|
||||||
export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage';
|
export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage';
|
||||||
export const SET_QUEUE_SORT = 'queue/setQueueSort';
|
export const SET_QUEUE_SORT = 'queue/setQueueSort';
|
||||||
|
export const SET_QUEUE_FILTER = 'queue/setQueueFilter';
|
||||||
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
|
export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption';
|
||||||
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
|
export const SET_QUEUE_OPTION = 'queue/setQueueOption';
|
||||||
export const CLEAR_QUEUE = 'queue/clearQueue';
|
export const CLEAR_QUEUE = 'queue/clearQueue';
|
||||||
|
@ -216,6 +249,7 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE);
|
||||||
export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE);
|
export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE);
|
||||||
export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
|
export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE);
|
||||||
export const setQueueSort = createThunk(SET_QUEUE_SORT);
|
export const setQueueSort = createThunk(SET_QUEUE_SORT);
|
||||||
|
export const setQueueFilter = createThunk(SET_QUEUE_FILTER);
|
||||||
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
|
export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION);
|
||||||
export const setQueueOption = createAction(SET_QUEUE_OPTION);
|
export const setQueueOption = createAction(SET_QUEUE_OPTION);
|
||||||
export const clearQueue = createAction(CLEAR_QUEUE);
|
export const clearQueue = createAction(CLEAR_QUEUE);
|
||||||
|
@ -267,7 +301,8 @@ export const actionHandlers = handleThunks({
|
||||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE,
|
||||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE,
|
||||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE,
|
||||||
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT
|
[serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER
|
||||||
},
|
},
|
||||||
fetchDataAugmenter
|
fetchDataAugmenter
|
||||||
),
|
),
|
||||||
|
|
|
@ -108,7 +108,7 @@ function sort(items, state) {
|
||||||
return _.orderBy(items, clauses, orders);
|
return _.orderBy(items, clauses, orders);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCustomFiltersSelector(type, alternateType) {
|
export function createCustomFiltersSelector(type, alternateType) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.customFilters.items,
|
(state) => state.customFilters.items,
|
||||||
(customFilters) => {
|
(customFilters) => {
|
||||||
|
|
|
@ -13,6 +13,7 @@ using NzbDrone.Core.Datastore.Events;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.Pending;
|
using NzbDrone.Core.Download.Pending;
|
||||||
using NzbDrone.Core.Download.TrackedDownloads;
|
using NzbDrone.Core.Download.TrackedDownloads;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
@ -129,15 +130,15 @@ namespace Lidarr.Api.V1.Queue
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownArtistItems = false, bool includeArtist = false, bool includeAlbum = false)
|
public PagingResource<QueueResource> GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownArtistItems = false, bool includeArtist = false, bool includeAlbum = false, [FromQuery] int[] artistIds = null, DownloadProtocol? protocol = null, int? quality = null)
|
||||||
{
|
{
|
||||||
var pagingResource = new PagingResource<QueueResource>(paging);
|
var pagingResource = new PagingResource<QueueResource>(paging);
|
||||||
var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending);
|
var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending);
|
||||||
|
|
||||||
return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownArtistItems), (q) => MapToResource(q, includeArtist, includeAlbum));
|
return pagingSpec.ApplyToPage((spec) => GetQueue(spec, artistIds?.ToHashSet(), protocol, quality, includeUnknownArtistItems), (q) => MapToResource(q, includeArtist, includeAlbum));
|
||||||
}
|
}
|
||||||
|
|
||||||
private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, bool includeUnknownArtistItems)
|
private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec, HashSet<int> artistIds, DownloadProtocol? protocol, int? quality, bool includeUnknownArtistItems)
|
||||||
{
|
{
|
||||||
var ascending = pagingSpec.SortDirection == SortDirection.Ascending;
|
var ascending = pagingSpec.SortDirection == SortDirection.Ascending;
|
||||||
var orderByFunc = GetOrderByFunc(pagingSpec);
|
var orderByFunc = GetOrderByFunc(pagingSpec);
|
||||||
|
@ -145,7 +146,30 @@ namespace Lidarr.Api.V1.Queue
|
||||||
var queue = _queueService.GetQueue();
|
var queue = _queueService.GetQueue();
|
||||||
var filteredQueue = includeUnknownArtistItems ? queue : queue.Where(q => q.Artist != null);
|
var filteredQueue = includeUnknownArtistItems ? queue : queue.Where(q => q.Artist != null);
|
||||||
var pending = _pendingReleaseService.GetPendingQueue();
|
var pending = _pendingReleaseService.GetPendingQueue();
|
||||||
var fullQueue = filteredQueue.Concat(pending).ToList();
|
|
||||||
|
var hasArtistIdFilter = artistIds.Any();
|
||||||
|
var fullQueue = filteredQueue.Concat(pending).Where(q =>
|
||||||
|
{
|
||||||
|
var include = true;
|
||||||
|
|
||||||
|
if (hasArtistIdFilter)
|
||||||
|
{
|
||||||
|
include &= q.Artist != null && artistIds.Contains(q.Artist.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include && protocol.HasValue)
|
||||||
|
{
|
||||||
|
include &= q.Protocol == protocol.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include && quality.HasValue)
|
||||||
|
{
|
||||||
|
include &= q.Quality.Quality.Id == quality.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return include;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
IOrderedEnumerable<NzbDrone.Core.Queue.Queue> ordered;
|
IOrderedEnumerable<NzbDrone.Core.Queue.Queue> ordered;
|
||||||
|
|
||||||
if (pagingSpec.SortKey == "timeleft")
|
if (pagingSpec.SortKey == "timeleft")
|
||||||
|
|
|
@ -780,6 +780,7 @@
|
||||||
"QualityProfiles": "Quality Profiles",
|
"QualityProfiles": "Quality Profiles",
|
||||||
"QualitySettings": "Quality Settings",
|
"QualitySettings": "Quality Settings",
|
||||||
"Queue": "Queue",
|
"Queue": "Queue",
|
||||||
|
"QueueFilterHasNoItems": "Selected queue filter has no items",
|
||||||
"QueueIsEmpty": "Queue is empty",
|
"QueueIsEmpty": "Queue is empty",
|
||||||
"Queued": "Queued",
|
"Queued": "Queued",
|
||||||
"RSSSync": "RSS Sync",
|
"RSSSync": "RSS Sync",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue