New: Queue custom filters

(cherry picked from commit e357d17b187378b92377f8acb077b12c1e7ea527)

Closes #4212
Closes #4234
This commit is contained in:
Mark McDowall 2023-05-21 17:51:36 -07:00 committed by Bogdan
parent 27e3aa76bd
commit a356b01efd
15 changed files with 235 additions and 16 deletions

View file

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

View file

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

View 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}
/>
);
}

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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