diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js
index ca3295ae3..8c9548d5d 100644
--- a/frontend/src/Activity/History/History.js
+++ b/frontend/src/Activity/History/History.js
@@ -12,8 +12,6 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
-import MenuContent from 'Components/Menu/MenuContent';
-import FilterMenuItem from 'Components/Menu/FilterMenuItem';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
@@ -49,8 +47,8 @@ class History extends Component {
error,
items,
columns,
- filterKey,
- filterValue,
+ selectedFilterKey,
+ filters,
totalRecords,
isAlbumsFetching,
isAlbumsPopulated,
@@ -77,67 +75,13 @@ class History extends Component {
-
-
-
- All
-
-
-
- Grabbed
-
-
-
- Imported
-
-
-
- Failed
-
-
-
- Deleted
-
-
-
- Renamed
-
-
-
+
@@ -204,8 +148,8 @@ History.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- filterKey: PropTypes.string,
- filterValue: PropTypes.string,
+ selectedFilterKey: PropTypes.string.isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isAlbumsFetching: PropTypes.bool.isRequired,
isAlbumsPopulated: PropTypes.bool.isRequired,
diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js
index 35da9cc5d..fd606a5ad 100644
--- a/frontend/src/Activity/History/HistoryConnector.js
+++ b/frontend/src/Activity/History/HistoryConnector.js
@@ -105,8 +105,8 @@ class HistoryConnector extends Component {
this.props.setHistorySort({ sortKey });
}
- onFilterSelect = (filterKey, filterValue) => {
- this.props.setHistoryFilter({ filterKey, filterValue });
+ onFilterSelect = (selectedFilterKey) => {
+ this.props.setHistoryFilter({ selectedFilterKey });
}
onTableOptionChange = (payload) => {
diff --git a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css
index 421a55237..f7bc065b5 100644
--- a/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css
+++ b/frontend/src/AddArtist/ImportArtist/Import/SelectArtist/ImportArtistSearchResult.css
@@ -3,6 +3,6 @@
width: 100%;
&:hover {
- background-color: $menuItemHoverColor;
+ background-color: $menuItemHoverBackgroundColor;
}
}
diff --git a/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.css b/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.css
new file mode 100644
index 000000000..8bd4c0f0d
--- /dev/null
+++ b/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.css
@@ -0,0 +1,5 @@
+.filterMenuContainer {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+}
diff --git a/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.js b/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.js
index f12f3d9a0..f849f0912 100644
--- a/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.js
+++ b/frontend/src/Album/Search/InteractiveAlbumSearchModalContent.js
@@ -1,8 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { icons, sortDirections } from 'Helpers/Props';
+import { align, icons, sortDirections } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
+import FilterMenu from 'Components/Menu/FilterMenu';
+import PageMenuButton from 'Components/Menu/PageMenuButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
@@ -10,7 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
+import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveAlbumSearchRow from './InteractiveAlbumSearchRow';
+import styles from './InteractiveAlbumSearchModalContent.css';
const columns = [
{
@@ -81,12 +85,17 @@ class InteractiveAlbumSearchModalContent extends Component {
isFetching,
isPopulated,
error,
+ totalReleasesCount,
items,
+ selectedFilterKey,
+ filters,
+ customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
+ onFilterSelect,
onGrabPress,
onModalClose
} = this.props;
@@ -117,28 +126,59 @@ class InteractiveAlbumSearchModalContent extends Component {
{
isPopulated && hasItems && !error &&
-
@@ -103,9 +107,8 @@ ArtistIndexBannerInfo.propTypes = {
showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string,
added: PropTypes.string,
- albumCount: PropTypes.number.isRequired,
+ statistics: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
- sizeOnDisk: PropTypes.number,
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js
index 331b22019..b05332b28 100644
--- a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js
@@ -1,9 +1,9 @@
-import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { Grid, WindowScroller } from 'react-virtualized';
+import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props';
@@ -116,11 +116,11 @@ class ArtistIndexBanners extends Component {
componentDidUpdate(prevProps) {
const {
items,
- filterKey,
- filterValue,
+ filters,
sortKey,
sortDirection,
- bannerOptions
+ bannerOptions,
+ jumpToCharacter
} = this.props;
const itemsChanged = hasDifferentItems(prevProps.items, items);
@@ -134,44 +134,34 @@ class ArtistIndexBanners extends Component {
}
if (
- prevProps.filterKey !== filterKey ||
- prevProps.filterValue !== filterValue ||
+ prevProps.filters !== filters ||
prevProps.sortKey !== sortKey ||
prevProps.sortDirection !== sortDirection ||
itemsChanged
) {
this._grid.recomputeGridSize();
}
- }
- //
- // Control
+ if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
+ const index = getIndexOfFirstCharacter(items, jumpToCharacter);
- scrollToFirstCharacter(character) {
- const items = this.props.items;
- const {
- columnCount,
- rowHeight
- } = this.state;
+ if (index != null) {
+ const {
+ columnCount,
+ rowHeight
+ } = this.state;
- const index = _.findIndex(items, (item) => {
- const firstCharacter = item.sortName.charAt(0);
+ const row = Math.floor(index / columnCount);
+ const scrollTop = rowHeight * row;
- if (character === '#') {
- return !isNaN(firstCharacter);
+ this.props.onScroll({ scrollTop });
}
-
- return firstCharacter === character;
- });
-
- if (index != null) {
- const row = Math.floor(index / columnCount);
- const scrollTop = rowHeight * row;
-
- this.props.onScroll({ scrollTop });
}
}
+ //
+ // Control
+
setGridRef = (ref) => {
this._grid = ref;
}
@@ -319,12 +309,12 @@ class ArtistIndexBanners extends Component {
ArtistIndexBanners.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
- filterKey: PropTypes.string,
- filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
bannerOptions: PropTypes.object.isRequired,
scrollTop: PropTypes.number.isRequired,
+ jumpToCharacter: PropTypes.string,
contentBody: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js
index 21384039e..25bdf61cc 100644
--- a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js
@@ -28,6 +28,6 @@ export default connectSection(
createMapStateToProps,
undefined,
undefined,
- { withRef: true },
+ undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexBanners);
diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js
index 6b5c9b9e1..cf9b25ea1 100644
--- a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js
+++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js
@@ -2,80 +2,38 @@ import PropTypes from 'prop-types';
import React from 'react';
import { align } from 'Helpers/Props';
import FilterMenu from 'Components/Menu/FilterMenu';
-import MenuContent from 'Components/Menu/MenuContent';
-import FilterMenuItem from 'Components/Menu/FilterMenuItem';
function ArtistIndexFilterMenu(props) {
const {
- filterKey,
- filterValue,
+ selectedFilterKey,
+ filters,
+ customFilters,
isDisabled,
onFilterSelect
} = props;
return (
-
-
- All
-
-
-
- Monitored Only
-
-
-
- Continuing Only
-
-
-
- Ended Only
-
-
-
- Missing Albums
-
-
-
+ isDisabled={isDisabled}
+ selectedFilterKey={selectedFilterKey}
+ filters={filters}
+ customFilters={customFilters}
+ onFilterSelect={onFilterSelect}
+ />
);
}
ArtistIndexFilterMenu.propTypes = {
- filterKey: PropTypes.string,
- filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
+ selectedFilterKey: PropTypes.string.isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
+ArtistIndexFilterMenu.defaultProps = {
+ showCustomFilters: false
+};
+
export default ArtistIndexFilterMenu;
diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js
index b36fbc757..01cfe1598 100644
--- a/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js
+++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverview.js
@@ -78,8 +78,7 @@ class ArtistIndexOverview extends Component {
status,
foreignArtistId,
nextAiring,
- trackCount,
- trackFileCount,
+ statistics,
images,
posterWidth,
posterHeight,
@@ -95,6 +94,12 @@ class ArtistIndexOverview extends Component {
...otherProps
} = this.props;
+ const {
+ trackCount,
+ trackFileCount,
+ totalTrackCount
+ } = statistics;
+
const {
isEditArtistModalOpen,
isDeleteArtistModalOpen
@@ -144,6 +149,7 @@ class ArtistIndexOverview extends Component {
status={status}
trackCount={trackCount}
trackFileCount={trackFileCount}
+ totalTrackCount={totalTrackCount}
posterWidth={posterWidth}
detailedProgressBar={overviewOptions.detailedProgressBar}
/>
@@ -194,6 +200,7 @@ class ArtistIndexOverview extends Component {
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
+ statistics={statistics}
{...overviewOptions}
{...otherProps}
/>
@@ -227,8 +234,7 @@ ArtistIndexOverview.propTypes = {
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
nextAiring: PropTypes.string,
- trackCount: PropTypes.number,
- trackFileCount: PropTypes.number,
+ statistics: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
@@ -245,7 +251,8 @@ ArtistIndexOverview.propTypes = {
ArtistIndexOverview.defaultProps = {
trackCount: 0,
- trackFileCount: 0
+ trackFileCount: 0,
+ albumCount: 0
};
export default ArtistIndexOverview;
diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js
index 7b904bcad..004a48a51 100644
--- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js
+++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewInfo.js
@@ -31,15 +31,19 @@ function ArtistIndexOverviewInfo(props) {
nextAiring,
qualityProfile,
added,
- albumCount,
+ statistics,
path,
- sizeOnDisk,
sortKey,
showRelativeDates,
shortDateFormat,
timeFormat
} = props;
+ const {
+ albumCount,
+ sizeOnDisk
+ } = statistics;
+
let albums = '1 album';
if (albumCount === 0) {
@@ -203,9 +207,8 @@ ArtistIndexOverviewInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired,
previousAiring: PropTypes.string,
added: PropTypes.string,
- albumCount: PropTypes.number.isRequired,
+ statistics: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
- sizeOnDisk: PropTypes.number,
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js
index 43028b32e..93c2c9764 100644
--- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js
+++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviews.js
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { Grid, WindowScroller } from 'react-virtualized';
+import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props';
@@ -76,11 +77,11 @@ class ArtistIndexOverviews extends Component {
componentDidUpdate(prevProps) {
const {
items,
- filterKey,
- filterValue,
+ filters,
sortKey,
sortDirection,
- overviewOptions
+ overviewOptions,
+ jumpToCharacter
} = this.props;
const itemsChanged = hasDifferentItems(prevProps.items, items);
@@ -95,8 +96,7 @@ class ArtistIndexOverviews extends Component {
}
if (
- prevProps.filterKey !== filterKey ||
- prevProps.filterValue !== filterValue ||
+ prevProps.filters !== filters ||
prevProps.sortKey !== sortKey ||
prevProps.sortDirection !== sortDirection ||
itemsChanged ||
@@ -104,6 +104,20 @@ class ArtistIndexOverviews extends Component {
) {
this._grid.recomputeGridSize();
}
+
+ if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
+ const index = getIndexOfFirstCharacter(items, jumpToCharacter);
+
+ if (index != null) {
+ const {
+ rowHeight
+ } = this.state;
+
+ const scrollTop = rowHeight * index;
+
+ this.props.onScroll({ scrollTop });
+ }
+ }
}
//
@@ -115,15 +129,7 @@ class ArtistIndexOverviews extends Component {
rowHeight
} = this.state;
- const index = _.findIndex(items, (item) => {
- const firstCharacter = item.sortTitle.charAt(0);
-
- if (character === '#') {
- return !isNaN(firstCharacter);
- }
-
- return firstCharacter === character;
- });
+ const index = getIndexOfFirstCharacter(items, character);
if (index != null) {
const scrollTop = rowHeight * index;
@@ -263,12 +269,12 @@ class ArtistIndexOverviews extends Component {
ArtistIndexOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
- filterKey: PropTypes.string,
- filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
overviewOptions: PropTypes.object.isRequired,
scrollTop: PropTypes.number.isRequired,
+ jumpToCharacter: PropTypes.string,
contentBody: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js
index a2075416f..3465729cc 100644
--- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js
+++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js
@@ -28,6 +28,6 @@ export default connectSection(
createMapStateToProps,
undefined,
undefined,
- { withRef: true },
+ undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexOverviews);
diff --git a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js
index 6ae1d8993..2134c7ef3 100644
--- a/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js
+++ b/frontend/src/Artist/Index/Overview/Options/ArtistIndexOverviewOptionsModalContent.js
@@ -204,7 +204,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
- Show Season Count
+ Show Album Count
@@ -165,26 +171,28 @@ class ArtistIndexPoster extends Component {
{qualityProfile.name}
}
-
-
@@ -103,9 +107,8 @@ ArtistIndexPosterInfo.propTypes = {
showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string,
added: PropTypes.string,
- albumCount: PropTypes.number,
+ statistics: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
- sizeOnDisk: PropTypes.number,
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js
index 56c3d1ce7..000b14887 100644
--- a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js
+++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js
@@ -1,9 +1,9 @@
-import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { Grid, WindowScroller } from 'react-virtualized';
+import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props';
@@ -116,11 +116,11 @@ class ArtistIndexPosters extends Component {
componentDidUpdate(prevProps) {
const {
items,
- filterKey,
- filterValue,
+ filters,
sortKey,
sortDirection,
- posterOptions
+ posterOptions,
+ jumpToCharacter
} = this.props;
const itemsChanged = hasDifferentItems(prevProps.items, items);
@@ -134,44 +134,34 @@ class ArtistIndexPosters extends Component {
}
if (
- prevProps.filterKey !== filterKey ||
- prevProps.filterValue !== filterValue ||
+ prevProps.filters !== filters ||
prevProps.sortKey !== sortKey ||
prevProps.sortDirection !== sortDirection ||
itemsChanged
) {
this._grid.recomputeGridSize();
}
- }
- //
- // Control
+ if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
+ const index = getIndexOfFirstCharacter(items, jumpToCharacter);
- scrollToFirstCharacter(character) {
- const items = this.props.items;
- const {
- columnCount,
- rowHeight
- } = this.state;
+ if (index != null) {
+ const {
+ columnCount,
+ rowHeight
+ } = this.state;
- const index = _.findIndex(items, (item) => {
- const firstCharacter = item.sortName.charAt(0);
+ const row = Math.floor(index / columnCount);
+ const scrollTop = rowHeight * row;
- if (character === '#') {
- return !isNaN(firstCharacter);
+ this.props.onScroll({ scrollTop });
}
-
- return firstCharacter === character;
- });
-
- if (index != null) {
- const row = Math.floor(index / columnCount);
- const scrollTop = rowHeight * row;
-
- this.props.onScroll({ scrollTop });
}
}
+ //
+ // Control
+
setGridRef = (ref) => {
this._grid = ref;
}
@@ -319,12 +309,12 @@ class ArtistIndexPosters extends Component {
ArtistIndexPosters.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
- filterKey: PropTypes.string,
- filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
posterOptions: PropTypes.object.isRequired,
scrollTop: PropTypes.number.isRequired,
+ jumpToCharacter: PropTypes.string,
contentBody: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js
index 6ec135987..786b187a8 100644
--- a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js
+++ b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js
@@ -28,6 +28,6 @@ export default connectSection(
createMapStateToProps,
undefined,
undefined,
- { withRef: true },
+ undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexPosters);
diff --git a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js
index 27f68f7fb..6be32a46d 100644
--- a/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js
+++ b/frontend/src/Artist/Index/ProgressBar/ArtistIndexProgressBar.js
@@ -11,6 +11,7 @@ function ArtistIndexProgressBar(props) {
status,
trackCount,
trackFileCount,
+ totalTrackCount,
posterWidth,
detailedProgressBar
} = props;
@@ -27,7 +28,7 @@ function ArtistIndexProgressBar(props) {
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
showText={detailedProgressBar}
text={text}
- title={detailedProgressBar ? null : text}
+ title={`${trackFileCount} / ${trackCount} (Total: ${totalTrackCount})`}
width={posterWidth}
/>
);
@@ -38,6 +39,7 @@ ArtistIndexProgressBar.propTypes = {
status: PropTypes.string.isRequired,
trackCount: PropTypes.number.isRequired,
trackFileCount: PropTypes.number.isRequired,
+ totalTrackCount: PropTypes.number.isRequired,
posterWidth: PropTypes.number.isRequired,
detailedProgressBar: PropTypes.bool.isRequired
};
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css
index a42ee7a03..31261d74b 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css
+++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css
@@ -55,7 +55,7 @@
.sizeOnDisk {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
- flex: 0 0 110px;
+ flex: 0 0 115px;
}
.tags {
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js
index f03c0489b..a042752f5 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexRow.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js
@@ -74,18 +74,22 @@ class ArtistIndexRow extends Component {
nextAlbum,
lastAlbum,
added,
- albumCount,
- trackCount,
- trackFileCount,
- totalTrackCount,
+ statistics,
path,
- sizeOnDisk,
tags,
columns,
isRefreshingArtist,
onRefreshArtistPress
} = this.props;
+ const {
+ albumCount,
+ trackCount,
+ trackFileCount,
+ totalTrackCount,
+ sizeOnDisk
+ } = statistics;
+
const {
isEditArtistModalOpen,
isDeleteArtistModalOpen
@@ -367,13 +371,9 @@ ArtistIndexRow.propTypes = {
nextAlbum: PropTypes.object,
lastAlbum: PropTypes.object,
added: PropTypes.string,
- albumCount: PropTypes.number,
- trackCount: PropTypes.number,
- trackFileCount: PropTypes.number,
- totalTrackCount: PropTypes.number,
+ statistics: PropTypes.object.isRequired,
latestAlbum: PropTypes.object,
path: PropTypes.string.isRequired,
- sizeOnDisk: PropTypes.number,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isRefreshingArtist: PropTypes.bool.isRequired,
@@ -382,7 +382,8 @@ ArtistIndexRow.propTypes = {
ArtistIndexRow.defaultProps = {
trackCount: 0,
- trackFileCount: 0
+ trackFileCount: 0,
+ albumCount: 0
};
export default ArtistIndexRow;
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js
index e4ee39d39..eee92a418 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexTable.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js
@@ -1,6 +1,6 @@
-import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
+import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import { sortDirections } from 'Helpers/Props';
import VirtualTable from 'Components/Table/VirtualTable';
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
@@ -9,40 +9,37 @@ import ArtistIndexRow from './ArtistIndexRow';
import styles from './ArtistIndexTable.css';
class ArtistIndexTable extends Component {
- constructor(props, context) {
- super(props, context);
- this._table = null;
- }
//
- // Control
+ // Lifecycle
- /**
- * Sets the reference to the virtual table
- * @param ref
- */
- setTableRef = (ref) => {
- this._table = ref;
- };
+ constructor(props, context) {
+ super(props, context);
- scrollToFirstCharacter(character) {
- const items = this.props.items;
+ this.state = {
+ scrollIndex: null
+ };
+ }
- const row = _.findIndex(items, (item) => {
- const firstCharacter = item.sortName.charAt(0);
+ componentDidUpdate(prevProps) {
+ const jumpToCharacter = this.props.jumpToCharacter;
- if (character === '#') {
- return !isNaN(firstCharacter);
- }
+ if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
+ const items = this.props.items;
- return firstCharacter === character;
- });
+ const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
- if (row != null) {
- this._table.scrollToRow(row);
+ if (scrollIndex != null) {
+ this.setState({ scrollIndex });
+ }
+ } else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
+ this.setState({ scrollIndex: null });
}
}
+ //
+ // Control
+
rowRenderer = ({ key, rowIndex, style }) => {
const {
items,
@@ -72,8 +69,7 @@ class ArtistIndexTable extends Component {
const {
items,
columns,
- filterKey,
- filterValue,
+ filters,
sortKey,
sortDirection,
isSmallScreen,
@@ -86,10 +82,10 @@ class ArtistIndexTable extends Component {
return (
}
columns={columns}
- filterKey={filterKey}
- filterValue={filterValue}
+ filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
onRender={onRender}
@@ -118,11 +113,11 @@ class ArtistIndexTable extends Component {
ArtistIndexTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- filterKey: PropTypes.string,
- filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
scrollTop: PropTypes.number.isRequired,
+ jumpToCharacter: PropTypes.string,
contentBody: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired,
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js
index c49c0cf07..3e4813fcf 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js
@@ -29,6 +29,6 @@ export default connectSection(
createMapStateToProps,
createMapDispatchToProps,
undefined,
- { withRef: true },
+ undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexTable);
diff --git a/frontend/src/Artist/MoveArtist/MoveArtistModal.js b/frontend/src/Artist/MoveArtist/MoveArtistModal.js
index 8d5fa2d91..395e4ff95 100644
--- a/frontend/src/Artist/MoveArtist/MoveArtistModal.js
+++ b/frontend/src/Artist/MoveArtist/MoveArtistModal.js
@@ -25,7 +25,7 @@ function MoveArtistModal(props) {
!destinationPath &&
!destinationRootFolder
) {
- console.error('orginalPath and destinationPath OR destinationRootFolder must be provied');
+ console.error('orginalPath and destinationPath OR destinationRootFolder must be provided');
}
return (
diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js
index b76cde468..6d235f806 100644
--- a/frontend/src/Calendar/CalendarPage.js
+++ b/frontend/src/Calendar/CalendarPage.js
@@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Measure from 'react-measure';
import { align, icons } from 'Helpers/Props';
+import getFilterValue from 'Utilities/Filter/getFilterValue';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
-import MenuContent from 'Components/Menu/MenuContent';
-import FilterMenuItem from 'Components/Menu/FilterMenuItem';
import NoArtist from 'Artist/NoArtist';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import Legend from './Legend/Legend';
@@ -42,10 +41,6 @@ class CalendarPage extends Component {
this.props.onDaysCountChange(days);
}
- onFilterMenuItemPress = (filterKey, unmonitored) => {
- this.props.onUnmonitoredChange(unmonitored);
- }
-
onGetCalendarLinkPress = () => {
this.setState({ isCalendarLinkModalOpen: true });
}
@@ -59,12 +54,15 @@ class CalendarPage extends Component {
render() {
const {
- unmonitored,
+ selectedFilterKey,
+ filters,
hasArtist,
- colorImpairedMode
+ colorImpairedMode,
+ onFilterSelect
} = this.props;
const isMeasured = this.state.width > 0;
+
let PageComponent = 'div';
if (isMeasured) {
@@ -85,30 +83,11 @@ class CalendarPage extends Component {
-
-
- All
-
-
-
- Monitored Only
-
-
-
+ selectedFilterKey={selectedFilterKey}
+ filters={filters}
+ customFilters={[]}
+ onFilterSelect={onFilterSelect}
+ />
@@ -139,11 +118,12 @@ class CalendarPage extends Component {
}
CalendarPage.propTypes = {
- unmonitored: PropTypes.bool.isRequired,
+ selectedFilterKey: PropTypes.string.isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
- onUnmonitoredChange: PropTypes.func.isRequired
+ onFilterSelect: PropTypes.func.isRequired
};
export default CalendarPage;
diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js
index df2d00ed4..fbd6a17f9 100644
--- a/frontend/src/Calendar/CalendarPageConnector.js
+++ b/frontend/src/Calendar/CalendarPageConnector.js
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { setCalendarDaysCount, setCalendarIncludeUnmonitored } from 'Store/Actions/calendarActions';
+import { setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CalendarPage from './CalendarPage';
@@ -12,7 +12,8 @@ function createMapStateToProps() {
createUISettingsSelector(),
(calendar, artistCount, uiSettings) => {
return {
- unmonitored: calendar.unmonitored,
+ filters: calendar.filters,
+ selectedFilterKey: calendar.selectedFilterKey,
showUpcoming: calendar.showUpcoming,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasArtist: !!artistCount
@@ -27,8 +28,8 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setCalendarDaysCount({ dayCount }));
},
- onUnmonitoredChange(unmonitored) {
- dispatch(setCalendarIncludeUnmonitored({ unmonitored }));
+ onFilterSelect(selectedFilterKey) {
+ dispatch(setCalendarFilter({ selectedFilterKey }));
}
};
}
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.css b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.css
new file mode 100644
index 000000000..6cc8fab67
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.css
@@ -0,0 +1,16 @@
+.labelContainer {
+ margin-bottom: 20px;
+}
+
+.label {
+ margin-bottom: 5px;
+ font-weight: bold;
+}
+
+.labelInputContainer {
+ width: 300px;
+}
+
+.rows {
+ margin-bottom: 100px;
+}
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js
new file mode 100644
index 000000000..d2a72b67c
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContent.js
@@ -0,0 +1,192 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { inputTypes } from 'Helpers/Props';
+import FormInputGroup from 'Components/Form/FormInputGroup';
+import Button from 'Components/Link/Button';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import FilterBuilderRow from './FilterBuilderRow';
+import styles from './FilterBuilderModalContent.css';
+
+class FilterBuilderModalContent extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ const filters = [...props.filters];
+
+ // Push an empty filter if there aren't any filters. FilterBuilderRow
+ // will handle initializing the filter.
+
+ if (!filters.length) {
+ filters.push({});
+ }
+
+ this.state = {
+ label: props.label,
+ filters,
+ labelErrors: []
+ };
+ }
+
+ //
+ // Listeners
+
+ onLabelChange = ({ value }) => {
+ this.setState({ label: value });
+ }
+
+ onFilterChange = (index, filter) => {
+ const filters = [...this.state.filters];
+ filters.splice(index, 1, filter);
+
+ this.setState({
+ filters
+ });
+ }
+
+ onAddFilterPress = () => {
+ const filters = [...this.state.filters];
+ filters.push({});
+
+ this.setState({
+ filters
+ });
+ }
+
+ onRemoveFilterPress = (index) => {
+ const filters = [...this.state.filters];
+ filters.splice(index, 1);
+
+ this.setState({
+ filters
+ });
+ }
+
+ onSaveFilterPress = () => {
+ const {
+ customFilterKey: key,
+ onSaveCustomFilterPress,
+ onModalClose
+ } = this.props;
+
+ const {
+ label,
+ filters
+ } = this.state;
+
+ if (!label) {
+ this.setState({
+ labelErrors: [
+ {
+ message: 'Label is required'
+ }
+ ]
+ });
+
+ return;
+ }
+
+ onSaveCustomFilterPress({ key, label, filters });
+ onModalClose();
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ sectionItems,
+ filterBuilderProps,
+ onModalClose
+ } = this.props;
+
+ const {
+ label,
+ filters,
+ labelErrors
+ } = this.state;
+
+ return (
+
+
+ Custom Filter
+
+
+
+
+
+ Filters
+
+
+ {
+ filters.map((filter, index) => {
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+FilterBuilderModalContent.propTypes = {
+ customFilterKey: PropTypes.string,
+ label: PropTypes.string.isRequired,
+ sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onRemoveCustomFilterPress: PropTypes.func.isRequired,
+ onSaveCustomFilterPress: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default FilterBuilderModalContent;
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderModalContentConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderModalContentConnector.js
new file mode 100644
index 000000000..e7f237793
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderModalContentConnector.js
@@ -0,0 +1,28 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import FilterBuilderModalContent from './FilterBuilderModalContent';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state, { customFilters }) => customFilters,
+ (state, { customFilterKey }) => customFilterKey,
+ (customFilters, customFilterKey) => {
+ if (customFilterKey) {
+ const customFilter = customFilters.find((c) => c.key === customFilterKey);
+
+ return {
+ customFilterKey: customFilter.key,
+ label: customFilter.label,
+ filters: customFilter.filters
+ };
+ }
+
+ return {
+ label: '',
+ filters: []
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(FilterBuilderModalContent);
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.css b/frontend/src/Components/Filter/Builder/FilterBuilderRow.css
new file mode 100644
index 000000000..c5471b253
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.css
@@ -0,0 +1,32 @@
+.filterRow {
+ display: flex;
+ margin-bottom: 5px;
+
+ &:hover {
+ background-color: $tableRowHoverBackgroundColor;
+ }
+}
+
+.inputContainer {
+ flex: 0 1 200px;
+ margin-right: 10px;
+}
+
+.valueInputContainer {
+ flex: 0 1 300px;
+ margin-right: 10px;
+}
+
+.actionsContainer {
+ display: flex;
+}
+
+@media only screen and (max-width: $breakpointSmall) {
+ .filterRow {
+ display: block;
+ }
+
+ .inputContainer {
+ margin-bottom: 10px;
+ }
+}
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
new file mode 100644
index 000000000..8541ac9e8
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
@@ -0,0 +1,248 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
+import SelectInput from 'Components/Form/SelectInput';
+import IconButton from 'Components/Link/IconButton';
+import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
+import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
+import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
+import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
+import styles from './FilterBuilderRow.css';
+
+function getselectedFilterBuilderProp(filterBuilderProps, name) {
+ return filterBuilderProps.find((a) => {
+ return a.name === name;
+ });
+}
+
+function getFilterTypeOptions(filterBuilderProps, filterKey) {
+ const selectedFilterBuilderProp = getselectedFilterBuilderProp(filterBuilderProps, filterKey);
+
+ if (!selectedFilterBuilderProp) {
+ return [];
+ }
+
+ return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type];
+}
+
+function getDefaultFilterType(selectedFilterBuilderProp) {
+ return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key;
+}
+
+function getRowValueConnector(selectedFilterBuilderProp) {
+ if (!selectedFilterBuilderProp) {
+ return FilterBuilderRowValueConnector;
+ }
+
+ const valueType = selectedFilterBuilderProp.valueType;
+
+ switch (valueType) {
+ case filterBuilderValueTypes.INDEXER:
+ return IndexerFilterBuilderRowValueConnector;
+
+ case filterBuilderValueTypes.PROTOCOL:
+ return ProtocolFilterBuilderRowValue;
+
+ case filterBuilderValueTypes.QUALITY:
+ return QualityFilterBuilderRowValueConnector;
+
+ default:
+ return FilterBuilderRowValueConnector;
+ }
+}
+
+class FilterBuilderRow extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ selectedFilterBuilderProp: null
+ };
+ }
+
+ componentDidMount() {
+ const {
+ index,
+ filterKey,
+ filterBuilderProps,
+ onFilterChange
+ } = this.props;
+
+ if (filterKey) {
+ const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
+ this.setState({ selectedFilterBuilderProp });
+
+ return;
+ }
+
+ const selectedFilterBuilderProp = filterBuilderProps[0];
+
+ const filter = {
+ key: selectedFilterBuilderProp.name,
+ value: [],
+ type: getDefaultFilterType(selectedFilterBuilderProp)
+ };
+
+ this.setState({ selectedFilterBuilderProp }, () => {
+ onFilterChange(index, filter);
+ });
+ }
+
+ //
+ // Listeners
+
+ onFilterKeyChange = ({ value: key }) => {
+ const {
+ index,
+ filterBuilderProps,
+ onFilterChange
+ } = this.props;
+
+ const selectedFilterBuilderProp = getselectedFilterBuilderProp(filterBuilderProps, key);
+ const type = getDefaultFilterType(selectedFilterBuilderProp);
+
+ const filter = {
+ key,
+ value: [],
+ type
+ };
+
+ this.setState({ selectedFilterBuilderProp }, () => {
+ onFilterChange(index, filter);
+ });
+ }
+
+ onFilterChange = ({ name, value }) => {
+ const {
+ index,
+ filterKey,
+ filterValue,
+ filterType,
+ onFilterChange
+ } = this.props;
+
+ const filter = {
+ key: filterKey,
+ value: filterValue,
+ type: filterType
+ };
+
+ filter[name] = value;
+
+ onFilterChange(index, filter);
+ }
+
+ onAddPress = () => {
+ const {
+ index,
+ onAddPress
+ } = this.props;
+
+ onAddPress(index);
+ }
+
+ onRemovePress = () => {
+ const {
+ index,
+ onRemovePress
+ } = this.props;
+
+ onRemovePress(index);
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ filterKey,
+ filterType,
+ filterValue,
+ filterCount,
+ filterBuilderProps
+ } = this.props;
+
+ const {
+ selectedFilterBuilderProp
+ } = this.state;
+
+ const keyOptions = filterBuilderProps.map((availablePropFilter) => {
+ return {
+ key: availablePropFilter.name,
+ value: availablePropFilter.label
+ };
+ });
+
+ const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
+
+ return (
+
+
+ {
+ filterKey &&
+
+ }
+
+
+
+ {
+ filterType &&
+
+ }
+
+
+
+ {
+ filterValue != null && !!selectedFilterBuilderProp &&
+
+ }
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+FilterBuilderRow.propTypes = {
+ index: PropTypes.number.isRequired,
+ filterKey: PropTypes.string,
+ filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
+ filterType: PropTypes.string,
+ filterCount: PropTypes.number.isRequired,
+ filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onFilterChange: PropTypes.func.isRequired,
+ onAddPress: PropTypes.func.isRequired,
+ onRemovePress: PropTypes.func.isRequired
+};
+
+export default FilterBuilderRow;
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js
new file mode 100644
index 000000000..0104cd4a6
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js
@@ -0,0 +1,100 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { kinds, filterBuilderTypes } from 'Helpers/Props';
+import TagInput, { tagShape } from 'Components/Form/TagInput';
+import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
+
+const NAME = 'value';
+
+class FilterBuilderRowValue extends Component {
+
+ //
+ // Listeners
+
+ onTagAdd = (tag) => {
+ const {
+ filterValue,
+ selectedFilterBuilderProp,
+ onChange
+ } = this.props;
+
+ let id = tag.id;
+
+ if (id == null) {
+ id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ?
+ parseInt(tag.name) :
+ tag.name;
+ }
+
+ onChange({
+ name: NAME,
+ value: [...filterValue, id]
+ });
+ }
+
+ onTagDelete = ({ index }) => {
+ const {
+ filterValue,
+ onChange
+ } = this.props;
+
+ const value = filterValue.filter((v, i) => i !== index);
+
+ onChange({
+ name: NAME,
+ value
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ filterValue,
+ tagList
+ } = this.props;
+
+ const hasItems = !!tagList.length;
+
+ const tags = filterValue.map((id) => {
+ if (hasItems) {
+ const tag = tagList.find((t) => t.id === id);
+
+ return {
+ id,
+ name: tag && tag.name
+ };
+ }
+ return {
+ id,
+ name: id
+ };
+ });
+
+ return (
+
+ );
+ }
+}
+
+FilterBuilderRowValue.propTypes = {
+ filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired,
+ selectedFilterBuilderProp: PropTypes.object.isRequired,
+ tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
+ onChange: PropTypes.func.isRequired
+};
+
+export default FilterBuilderRowValue;
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js
new file mode 100644
index 000000000..fd0832334
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js
@@ -0,0 +1,50 @@
+import _ from 'lodash';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { filterBuilderTypes } from 'Helpers/Props';
+import FilterBuilderRowValue from './FilterBuilderRowValue';
+
+function createTagListSelector() {
+ return createSelector(
+ (state, { sectionItems }) => _.get(state, sectionItems),
+ (state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
+ (sectionItems, selectedFilterBuilderProp) => {
+ if (
+ selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
+ selectedFilterBuilderProp.type === filterBuilderTypes.STRING
+ ) {
+ return [];
+ }
+
+ let items = [];
+
+ if (selectedFilterBuilderProp.optionsSelector) {
+ items = sectionItems.map(selectedFilterBuilderProp.optionsSelector);
+ } else {
+ items = sectionItems.map((item) => {
+ const name = item[selectedFilterBuilderProp.name];
+
+ return {
+ id: name,
+ name
+ };
+ });
+ }
+
+ return _.uniqBy(items, 'id');
+ }
+ );
+}
+
+function createMapStateToProps() {
+ return createSelector(
+ createTagListSelector(),
+ (tagList) => {
+ return {
+ tagList
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(FilterBuilderRowValue);
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.css b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.css
new file mode 100644
index 000000000..1c4c5acf1
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.css
@@ -0,0 +1,19 @@
+.tag {
+ &.isLastTag {
+ .or {
+ display: none;
+ }
+ }
+}
+
+.label {
+ composes: label from 'Components/Label.css';
+
+ border-style: none;
+ font-size: 13px;
+}
+
+.or {
+ margin: 0 3px;
+ color: $themeDarkColor;
+}
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js
new file mode 100644
index 000000000..573e05759
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueTag.js
@@ -0,0 +1,31 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { kinds } from 'Helpers/Props';
+import TagInputTag from 'Components/Form/TagInputTag';
+import styles from './FilterBuilderRowValueTag.css';
+
+function FilterBuilderRowValueTag(props) {
+ return (
+
+
+
+ {
+ !props.isLastTag &&
+
+ or
+
+ }
+
+ );
+}
+
+FilterBuilderRowValueTag.propTypes = {
+ isLastTag: PropTypes.bool.isRequired
+};
+
+export default FilterBuilderRowValueTag;
diff --git a/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js
new file mode 100644
index 000000000..968b26d2c
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/IndexerFilterBuilderRowValueConnector.js
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { fetchIndexers } from 'Store/Actions/settingsActions';
+import { tagShape } from 'Components/Form/TagInput';
+import FilterBuilderRowValue from './FilterBuilderRowValue';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.settings.indexers,
+ (qualityProfiles) => {
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ items
+ } = qualityProfiles;
+
+ const tagList = items.map((item) => {
+ return {
+ id: item.id,
+ name: item.name
+ };
+ });
+
+ return {
+ isFetching,
+ isPopulated,
+ error,
+ tagList
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ dispatchFetchIndexers: fetchIndexers
+};
+
+class IndexerFilterBuilderRowValueConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount = () => {
+ if (!this.props.isPopulated) {
+ this.props.dispatchFetchIndexers();
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+IndexerFilterBuilderRowValueConnector.propTypes = {
+ tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ error: PropTypes.object,
+ dispatchFetchIndexers: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(IndexerFilterBuilderRowValueConnector);
diff --git a/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.js
new file mode 100644
index 000000000..ae63ae0eb
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/ProtocolFilterBuilderRowValue.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import FilterBuilderRowValue from './FilterBuilderRowValue';
+
+const protocols = [
+ { id: 'torrent', name: 'Torrent' },
+ { id: 'usenet', name: 'Usenet' }
+];
+
+function ProtocolFilterBuilderRowValue(props) {
+ return (
+
+ );
+}
+
+export default ProtocolFilterBuilderRowValue;
diff --git a/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js
new file mode 100644
index 000000000..ee1dc732e
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js
@@ -0,0 +1,75 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import getQualities from 'Utilities/Quality/getQualities';
+import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
+import { tagShape } from 'Components/Form/TagInput';
+import FilterBuilderRowValue from './FilterBuilderRowValue';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.settings.qualityProfiles,
+ (qualityProfiles) => {
+ const {
+ isFetchingSchema: isFetching,
+ isSchemaPopulated: isPopulated,
+ schemaError: error,
+ schema
+ } = qualityProfiles;
+
+ const tagList = getQualities(schema.items);
+
+ return {
+ isFetching,
+ isPopulated,
+ error,
+ tagList
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ dispatchFetchQualityProfileSchema: fetchQualityProfileSchema
+};
+
+class QualityFilterBuilderRowValueConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount = () => {
+ if (!this.props.isPopulated) {
+ this.props.dispatchFetchQualityProfileSchema();
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ isFetching,
+ isPopulated,
+ error,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+QualityFilterBuilderRowValueConnector.propTypes = {
+ tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ error: PropTypes.object,
+ dispatchFetchQualityProfileSchema: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(QualityFilterBuilderRowValueConnector);
diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.css b/frontend/src/Components/Filter/CustomFilters/CustomFilter.css
new file mode 100644
index 000000000..7acb69dc7
--- /dev/null
+++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.css
@@ -0,0 +1,17 @@
+.customFilter {
+ display: flex;
+ margin-bottom: 5px;
+ padding: 5px;
+
+ &:hover {
+ background-color: $tableRowHoverBackgroundColor;
+ }
+}
+
+.label {
+ flex: 0 1 300px;
+}
+
+.actions {
+ flex: 0 0 60px;
+}
diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js
new file mode 100644
index 000000000..62551978b
--- /dev/null
+++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { icons } from 'Helpers/Props';
+import IconButton from 'Components/Link/IconButton';
+import styles from './CustomFilter.css';
+
+class CustomFilter extends Component {
+
+ //
+ // Listeners
+
+ onEditPress = () => {
+ const {
+ customFilterKey,
+ onEditPress
+ } = this.props;
+
+ onEditPress(customFilterKey);
+ }
+
+ onRemovePress = () => {
+ const {
+ customFilterKey,
+ onRemovePress
+ } = this.props;
+
+ onRemovePress({ key: customFilterKey });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ label
+ } = this.props;
+
+ return (
+
+
+ {label}
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+CustomFilter.propTypes = {
+ customFilterKey: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ onEditPress: PropTypes.func.isRequired,
+ onRemovePress: PropTypes.func.isRequired
+};
+
+export default CustomFilter;
diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.css b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.css
new file mode 100644
index 000000000..c391764dc
--- /dev/null
+++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.css
@@ -0,0 +1,3 @@
+.addButtonContainer {
+ margin-top: 15px;
+}
diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js
new file mode 100644
index 000000000..ac27bdd23
--- /dev/null
+++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js
@@ -0,0 +1,68 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Button from 'Components/Link/Button';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import CustomFilter from './CustomFilter';
+import styles from './CustomFiltersModalContent.css';
+
+function CustomFiltersModalContent(props) {
+ const {
+ customFilters,
+ onAddCustomFilter,
+ onRemoveCustomFilterPress,
+ onEditCustomFilter,
+ onModalClose
+ } = props;
+
+ return (
+
+
+ Custom Filters
+
+
+
+ {
+ customFilters.map((customFilter, index) => {
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+CustomFiltersModalContent.propTypes = {
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onAddCustomFilter: PropTypes.func.isRequired,
+ onRemoveCustomFilterPress: PropTypes.func.isRequired,
+ onEditCustomFilter: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default CustomFiltersModalContent;
diff --git a/frontend/src/Components/Filter/FilterModal.js b/frontend/src/Components/Filter/FilterModal.js
new file mode 100644
index 000000000..944caf070
--- /dev/null
+++ b/frontend/src/Components/Filter/FilterModal.js
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import Modal from 'Components/Modal/Modal';
+import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
+import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
+
+class FilterModal extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ filterBuilder: !props.customFilters.length,
+ customFilterKey: null
+ };
+ }
+
+ //
+ // Listeners
+
+ onAddCustomFilter = () => {
+ this.setState({
+ filterBuilder: true
+ });
+ }
+
+ onEditCustomFilter = (customFilterKey) => {
+ this.setState({
+ filterBuilder: true,
+ customFilterKey
+ });
+ }
+
+ onModalClose = () => {
+ this.setState({
+ filterBuilder: false,
+ customFilterKey: null
+ }, () => {
+ this.props.onModalClose();
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ isOpen,
+ ...otherProps
+ } = this.props;
+
+ const {
+ filterBuilder,
+ customFilterKey
+ } = this.state;
+
+ return (
+
+ {
+ filterBuilder ?
+ :
+
+ }
+
+ );
+ }
+}
+
+FilterModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default FilterModal;
diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css
index e269a7adf..568e35f40 100644
--- a/frontend/src/Components/Form/EnhancedSelectInput.css
+++ b/frontend/src/Components/Form/EnhancedSelectInput.css
@@ -31,7 +31,6 @@
.isDisabled {
opacity: 0.7;
cursor: not-allowed;
- pointer-events: all !important;
}
.dropdownArrowContainer {
diff --git a/frontend/src/Components/Form/PathInput.css b/frontend/src/Components/Form/PathInput.css
index d3851b204..ce9fd8ebe 100644
--- a/frontend/src/Components/Form/PathInput.css
+++ b/frontend/src/Components/Form/PathInput.css
@@ -58,7 +58,7 @@
}
.pathHighlighted {
- background-color: $menuItemHoverColor;
+ background-color: $menuItemHoverBackgroundColor;
}
.fileBrowserButton {
diff --git a/frontend/src/Components/Form/TagInput.css b/frontend/src/Components/Form/TagInput.css
index 87853ccfb..1112b6e86 100644
--- a/frontend/src/Components/Form/TagInput.css
+++ b/frontend/src/Components/Form/TagInput.css
@@ -1,97 +1,77 @@
-.container {
+.inputContainer {
composes: input from 'Components/Form/Input.css';
- display: flex;
- flex-wrap: wrap;
+ position: relative;
+ padding: 0;
min-height: 35px;
height: auto;
-}
-
-.containerFocused {
- outline: 0;
- border-color: $inputFocusBorderColor;
- box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
-}
-.selectedTagContainer {
- flex: 0 0 auto;
-}
-
-.selectedTag {
- composes: label from 'Components/Label.css';
-
- border-style: none;
- font-size: 13px;
+ &.isFocused {
+ outline: 0;
+ border-color: $inputFocusBorderColor;
+ box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
+ }
}
-/* Selected Tag Kinds */
-
-.info {
- composes: info from 'Components/Label.css';
+.hasError {
+ composes: hasError from 'Components/Form/Input.css';
}
-.success {
- composes: success from 'Components/Label.css';
+.hasWarning {
+ composes: hasWarning from 'Components/Form/Input.css';
}
-.warning {
- composes: warning from 'Components/Label.css';
+.tags {
+ flex: 0 0 auto;
+ max-width: 100%;
}
-.danger {
- composes: danger from 'Components/Label.css';
+.input {
+ flex: 1 1 0%;
+ margin-left: 3px;
+ min-width: 20%;
+ max-width: 100%;
+ width: 0%;
+ border: none;
}
-.searchInputContainer {
- position: relative;
- flex: 1 0 100px;
- margin-top: 1px;
- padding-left: 5px;
+.suggestionsContainer {
+ @add-mixin scrollbar;
+ @add-mixin scrollbarTrack;
+ @add-mixin scrollbarThumb;
}
-.searchInput {
- max-width: 100%;
- font-size: 13px;
-
- input {
- margin: 0;
- padding: 0;
- max-width: 100%;
- outline: none;
- border: 0;
+.containerOpen {
+ .suggestionsContainer {
+ position: absolute;
+ right: -1px;
+ left: -1px;
+ z-index: 1;
+ overflow-y: auto;
+ margin-top: 1px;
+ max-height: 110px;
+ border: 1px solid $inputBorderColor;
+ border-radius: 4px;
+ background-color: $white;
+ box-shadow: inset 0 1px 1px $inputBoxShadowColor;
}
}
-.suggestions {
- position: absolute;
- z-index: 1;
- overflow-y: auto;
- max-height: 200px;
- width: 100%;
- border: 1px solid $inputBorderColor;
- border-radius: 4px;
- background-color: $white;
- box-shadow: inset 0 1px 1px $inputBoxShadowColor;
-
- ul {
- margin: 5px 0;
- padding-left: 0;
- list-style-type: none;
- }
-
- li {
- padding: 0 16px;
- }
+.suggestionsList {
+ margin: 5px 0;
+ padding-left: 0;
+ list-style-type: none;
+}
- li mark {
- font-weight: bold;
- }
+.suggestion {
+ padding: 0 16px;
+ cursor: default;
- li:hover {
- background-color: $menuItemHoverColor;
+ &:hover {
+ background-color: $menuItemHoverBackgroundColor;
}
}
-.suggestionActive {
- background-color: $menuItemHoverColor;
+.suggestionHighlighted {
+ background-color: $menuItemHoverBackgroundColor;
}
diff --git a/frontend/src/Components/Form/TagInput.js b/frontend/src/Components/Form/TagInput.js
index 3ad360308..61f893446 100644
--- a/frontend/src/Components/Form/TagInput.js
+++ b/frontend/src/Components/Form/TagInput.js
@@ -1,11 +1,27 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import ReactTags from 'react-tag-autocomplete';
+import Autosuggest from 'react-autosuggest';
import classNames from 'classnames';
import { kinds } from 'Helpers/Props';
+import TagInputInput from './TagInputInput';
+import TagInputTag from './TagInputTag';
import styles from './TagInput.css';
+function getTag(value, selectedIndex, suggestions, allowNew) {
+ if (selectedIndex == null && value) {
+ const existingTag = _.find(suggestions, { name: value });
+
+ if (existingTag) {
+ return existingTag;
+ } else if (allowNew) {
+ return { name: value };
+ }
+ } else if (selectedIndex != null) {
+ return suggestions[selectedIndex];
+ }
+}
+
class TagInput extends Component {
//
@@ -14,97 +30,240 @@ class TagInput extends Component {
constructor(props, context) {
super(props, context);
- this._tagsRef = null;
- this._inputRef = null;
+ this.state = {
+ value: '',
+ suggestions: [],
+ isFocused: false
+ };
+
+ this._autosuggestRef = null;
}
//
// Control
- _setTagsRef = (ref) => {
- this._tagsRef = ref;
+ _setAutosuggestRef = (ref) => {
+ this._autosuggestRef = ref;
+ }
- if (ref) {
- this._inputRef = this._tagsRef.input.input;
+ getSuggestionValue({ name }) {
+ return name;
+ }
- this._inputRef.addEventListener('blur', this.onInputBlur);
- } else if (this._inputRef) {
- this._inputRef.removeEventListener('blur', this.onInputBlur);
- }
+ shouldRenderSuggestions = (value) => {
+ return value.length >= this.props.minQueryLength;
+ }
+
+ renderSuggestion({ name }, { query }) {
+ return name;
}
//
// Listeners
+ onInputContainerPress = () => {
+ this._autosuggestRef.input.focus();
+ }
+
+ onTagAdd(tag) {
+ this.props.onTagAdd(tag);
+
+ this.setState({
+ value: '',
+ suggestions: []
+ });
+ }
+
+ onInputChange = (event, { newValue, method }) => {
+ const value = _.isObject(newValue) ? newValue.name : newValue;
+
+ if (method === 'type') {
+ this.setState({ value });
+ }
+ }
+
+ onInputKeyDown = (event) => {
+ const {
+ tags,
+ allowNew,
+ delimiters,
+ onTagDelete
+ } = this.props;
+
+ const {
+ value,
+ suggestions
+ } = this.state;
+
+ const keyCode = event.keyCode;
+
+ if (keyCode === 8 && !value.length) {
+ const index = tags.length - 1;
+
+ if (index >= 0) {
+ onTagDelete({ index, id: tags[index].id });
+ }
+
+ setTimeout(() => {
+ this.onSuggestionsFetchRequested({ value: '' });
+ });
+
+ event.preventDefault();
+ }
+
+ if (delimiters.includes(keyCode)) {
+ const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
+ const tag = getTag(value, selectedIndex, suggestions, allowNew);
+
+ if (tag) {
+ this.onTagAdd(tag);
+ }
+
+ event.preventDefault();
+ }
+ }
+
+ onInputFocus = () => {
+ this.setState({ isFocused: true });
+ }
+
onInputBlur = () => {
- if (!this._tagsRef) {
+ this.setState({ isFocused: false });
+
+ if (!this._autosuggestRef) {
return;
}
const {
- tagList,
allowNew
} = this.props;
- const query = this._tagsRef.state.query.trim();
+ const {
+ value,
+ suggestions
+ } = this.state;
- if (query) {
- const existingTag = _.find(tagList, { name: query });
+ const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
+ const tag = getTag(value, selectedIndex, suggestions, allowNew);
- if (existingTag) {
- this._tagsRef.addTag(existingTag);
- } else if (allowNew) {
- this._tagsRef.addTag({ name: query });
- }
+ if (tag) {
+ this.onTagAdd(tag);
}
}
+ onSuggestionsFetchRequested = ({ value }) => {
+ const lowerCaseValue = value.toLowerCase();
+
+ const {
+ tags,
+ tagList
+ } = this.props;
+
+ const suggestions = tagList.filter((tag) => {
+ return (
+ tag.name.toLowerCase().includes(lowerCaseValue) &&
+ !tags.some((t) => t.id === tag.id));
+ });
+
+ this.setState({ suggestions });
+ }
+
+ onSuggestionsClearRequested = () => {
+ // Required because props aren't always rendered, but no-op
+ // because we don't want to reset the paths after a path is selected.
+ }
+
+ onSuggestionSelected = (event, { suggestion }) => {
+ this.onTagAdd(suggestion);
+ }
+
//
// Render
- render() {
+ renderInputComponent = (inputProps) => {
const {
tags,
- tagList,
- allowNew,
kind,
- placeholder,
- onTagAdd,
+ tagComponent,
onTagDelete
} = this.props;
- const tagInputClassNames = {
- root: styles.container,
- rootFocused: styles.containerFocused,
- selected: styles.selectedTagContainer,
- selectedTag: classNames(styles.selectedTag, styles[kind]),
- search: styles.searchInputContainer,
- searchInput: styles.searchInput,
- suggestions: styles.suggestions,
- suggestionActive: styles.suggestionActive,
- suggestionDisabled: styles.suggestionDisabled
+ return (
+
+ );
+ }
+
+ render() {
+ const {
+ placeholder,
+ hasError,
+ hasWarning
+ } = this.props;
+
+ const {
+ value,
+ suggestions,
+ isFocused
+ } = this.state;
+
+ const inputProps = {
+ className: styles.input,
+ name,
+ value,
+ placeholder,
+ autoComplete: 'off',
+ spellCheck: false,
+ onChange: this.onInputChange,
+ onKeyDown: this.onInputKeyDown,
+ onFocus: this.onInputFocus,
+ onBlur: this.onInputBlur
+ };
+
+ const theme = {
+ container: classNames(
+ styles.inputContainer,
+ isFocused && styles.isFocused,
+ hasError && styles.hasError,
+ hasWarning && styles.hasWarning,
+ ),
+ containerOpen: styles.containerOpen,
+ suggestionsContainer: styles.suggestionsContainer,
+ suggestionsList: styles.suggestionsList,
+ suggestion: styles.suggestion,
+ suggestionHighlighted: styles.suggestionHighlighted
};
return (
-
);
}
}
-const tagShape = {
- id: PropTypes.number.isRequired,
- name: PropTypes.string.isRequired
+export const tagShape = {
+ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
+ name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
};
TagInput.propTypes = {
@@ -113,6 +272,11 @@ TagInput.propTypes = {
allowNew: PropTypes.bool.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
placeholder: PropTypes.string.isRequired,
+ delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
+ minQueryLength: PropTypes.number.isRequired,
+ hasError: PropTypes.bool,
+ hasWarning: PropTypes.bool,
+ tagComponent: PropTypes.func.isRequired,
onTagAdd: PropTypes.func.isRequired,
onTagDelete: PropTypes.func.isRequired
};
@@ -120,7 +284,11 @@ TagInput.propTypes = {
TagInput.defaultProps = {
allowNew: true,
kind: kinds.INFO,
- placeholder: ''
+ placeholder: '',
+ // Tab, enter, space and comma
+ delimiters: [9, 13, 32, 188],
+ minQueryLength: 1,
+ tagComponent: TagInputTag
};
export default TagInput;
diff --git a/frontend/src/Components/Form/TagInputConnector.js b/frontend/src/Components/Form/TagInputConnector.js
index 163b36895..5265e9e4f 100644
--- a/frontend/src/Components/Form/TagInputConnector.js
+++ b/frontend/src/Components/Form/TagInputConnector.js
@@ -103,7 +103,7 @@ class TagInputConnector extends Component {
this.props.onChange({ name, value: newValue });
}
- onTagDelete = (index) => {
+ onTagDelete = ({ index }) => {
const {
name,
value
diff --git a/frontend/src/Components/Form/TagInputInput.css b/frontend/src/Components/Form/TagInputInput.css
new file mode 100644
index 000000000..182320b1a
--- /dev/null
+++ b/frontend/src/Components/Form/TagInputInput.css
@@ -0,0 +1,6 @@
+.inputContainer {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 6px 16px;
+ cursor: default;
+}
diff --git a/frontend/src/Components/Form/TagInputInput.js b/frontend/src/Components/Form/TagInputInput.js
new file mode 100644
index 000000000..8bd075774
--- /dev/null
+++ b/frontend/src/Components/Form/TagInputInput.js
@@ -0,0 +1,76 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { kinds } from 'Helpers/Props';
+import { tagShape } from './TagInput';
+import styles from './TagInputInput.css';
+
+class TagInputInput extends Component {
+
+ onMouseDown = (event) => {
+ event.preventDefault();
+
+ const {
+ isFocused,
+ onInputContainerPress
+ } = this.props;
+
+ if (isFocused) {
+ return;
+ }
+
+ onInputContainerPress();
+ }
+
+ render() {
+ const {
+ className,
+ tags,
+ inputProps,
+ kind,
+ tagComponent: TagComponent,
+ onTagDelete
+ } = this.props;
+
+ return (
+
+ {
+ tags.map((tag, index) => {
+ return (
+
+ );
+ })
+ }
+
+
+
+ );
+ }
+}
+
+TagInputInput.propTypes = {
+ className: PropTypes.string.isRequired,
+ tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
+ inputProps: PropTypes.object.isRequired,
+ kind: PropTypes.oneOf(kinds.all).isRequired,
+ isFocused: PropTypes.bool.isRequired,
+ tagComponent: PropTypes.func.isRequired,
+ onTagDelete: PropTypes.func.isRequired,
+ onInputContainerPress: PropTypes.func.isRequired
+};
+
+TagInputInput.defaultProps = {
+ className: styles.inputContainer
+};
+
+export default TagInputInput;
diff --git a/frontend/src/Components/Form/TagInputTag.js b/frontend/src/Components/Form/TagInputTag.js
new file mode 100644
index 000000000..8cb5486bc
--- /dev/null
+++ b/frontend/src/Components/Form/TagInputTag.js
@@ -0,0 +1,52 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { kinds } from 'Helpers/Props';
+import Label from 'Components/Label';
+import Link from 'Components/Link/Link';
+import { tagShape } from './TagInput';
+
+class TagInputTag extends Component {
+
+ //
+ // Listeners
+
+ onDelete = () => {
+ const {
+ index,
+ tag,
+ onDelete
+ } = this.props;
+
+ onDelete({
+ index,
+ id: tag.id
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ tag,
+ kind
+ } = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
+
+TagInputTag.propTypes = {
+ index: PropTypes.number.isRequired,
+ tag: PropTypes.shape(tagShape),
+ kind: PropTypes.oneOf(kinds.all).isRequired,
+ onDelete: PropTypes.func.isRequired
+};
+
+export default TagInputTag;
diff --git a/frontend/src/Components/Form/TextTagInput.js b/frontend/src/Components/Form/TextTagInput.js
deleted file mode 100644
index ae9d35baa..000000000
--- a/frontend/src/Components/Form/TextTagInput.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import ReactTags from 'react-tag-autocomplete';
-import classNames from 'classnames';
-import { kinds } from 'Helpers/Props';
-import styles from './TagInput.css';
-
-class TextTagInput extends Component {
-
- //
- // Render
-
- render() {
- const {
- tags,
- allowNew,
- kind,
- placeholder,
- onTagAdd,
- onTagDelete
- } = this.props;
-
- const tagInputClassNames = {
- root: styles.container,
- rootFocused: styles.containerFocused,
- selected: styles.selectedTagContainer,
- selectedTag: classNames(styles.selectedTag, styles[kind]),
- search: styles.searchInputContainer,
- searchInput: styles.searchInput,
- suggestions: styles.suggestions,
- suggestionActive: styles.suggestionActive,
- suggestionDisabled: styles.suggestionDisabled
- };
-
- return (
-
- );
- }
-}
-
-const tagShape = {
- id: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired
-};
-
-TextTagInput.propTypes = {
- tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
- allowNew: PropTypes.bool.isRequired,
- kind: PropTypes.string.isRequired,
- placeholder: PropTypes.string,
- onTagAdd: PropTypes.func.isRequired,
- onTagDelete: PropTypes.func.isRequired
-};
-
-TextTagInput.defaultProps = {
- allowNew: true,
- kind: kinds.INFO
-};
-
-export default TextTagInput;
diff --git a/frontend/src/Components/Form/TextTagInputConnector.js b/frontend/src/Components/Form/TextTagInputConnector.js
index 2c9e52cbc..03d593b75 100644
--- a/frontend/src/Components/Form/TextTagInputConnector.js
+++ b/frontend/src/Components/Form/TextTagInputConnector.js
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import split from 'Utilities/String/split';
-import TextTagInput from './TextTagInput';
+import TagInput from './TagInput';
function createMapStateToProps() {
return createSelector(
@@ -34,25 +34,27 @@ class TextTagInputConnector extends Component {
onTagAdd = (tag) => {
const {
name,
- value
+ value,
+ onChange
} = this.props;
const newValue = split(value);
newValue.push(tag.name);
- this.props.onChange({ name, value: newValue.join(',') });
+ onChange({ name, value: newValue.join(',') });
}
- onTagDelete = (index) => {
+ onTagDelete = ({ index }) => {
const {
name,
- value
+ value,
+ onChange
} = this.props;
const newValue = split(value);
newValue.splice(index, 1);
- this.props.onChange({
+ onChange({
name,
value: newValue.join(',')
});
@@ -63,7 +65,8 @@ class TextTagInputConnector extends Component {
render() {
return (
-
-
- {children}
-
- );
+class FilterMenu extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isFilterModalOpen: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onCustomFiltersPress = () => {
+ this.setState({ isFilterModalOpen: true });
+ }
+
+ onFiltersModalClose = () => {
+ this.setState({ isFilterModalOpen: false });
+ }
+
+ //
+ // Render
+
+ render(props) {
+ const {
+ className,
+ isDisabled,
+ selectedFilterKey,
+ filters,
+ customFilters,
+ buttonComponent: ButtonComponent,
+ filterModalConnectorComponent: FilterModalConnectorComponent,
+ onFilterSelect,
+ ...otherProps
+ } = this.props;
+
+ const showCustomFilters = !!FilterModalConnectorComponent;
+
+ return (
+
+
+
+ {
+ showCustomFilters &&
+
+ }
+
+ );
+ }
}
FilterMenu.propTypes = {
className: PropTypes.string,
- children: PropTypes.node.isRequired,
- isDisabled: PropTypes.bool.isRequired
+ isDisabled: PropTypes.bool.isRequired,
+ selectedFilterKey: PropTypes.string.isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ buttonComponent: PropTypes.func.isRequired,
+ filterModalConnectorComponent: PropTypes.func,
+ onFilterSelect: PropTypes.func.isRequired
};
FilterMenu.defaultProps = {
className: styles.filterMenu,
- isDisabled: false
+ isDisabled: false,
+ buttonComponent: ToolbarMenuButton
};
export default FilterMenu;
diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js
new file mode 100644
index 000000000..2433e9db1
--- /dev/null
+++ b/frontend/src/Components/Menu/FilterMenuContent.js
@@ -0,0 +1,85 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import MenuContent from './MenuContent';
+import FilterMenuItem from './FilterMenuItem';
+import MenuItem from './MenuItem';
+import MenuItemSeparator from './MenuItemSeparator';
+
+class FilterMenuContent extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ selectedFilterKey,
+ filters,
+ customFilters,
+ showCustomFilters,
+ onFilterSelect,
+ onCustomFiltersPress,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ {
+ filters.map((filter) => {
+ return (
+
+ {filter.label}
+
+ );
+ })
+ }
+
+ {
+ customFilters.map((filter) => {
+ return (
+
+ {filter.label}
+
+ );
+ })
+ }
+
+ {
+ showCustomFilters &&
+
+ }
+
+ {
+ showCustomFilters &&
+
+ }
+
+ );
+ }
+}
+
+FilterMenuContent.propTypes = {
+ selectedFilterKey: PropTypes.string.isRequired,
+ filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ showCustomFilters: PropTypes.bool.isRequired,
+ onFilterSelect: PropTypes.func.isRequired,
+ onCustomFiltersPress: PropTypes.func.isRequired
+};
+
+FilterMenuContent.defaultProps = {
+ showCustomFilters: false
+};
+
+export default FilterMenuContent;
diff --git a/frontend/src/Components/Menu/FilterMenuItem.js b/frontend/src/Components/Menu/FilterMenuItem.js
index 54c293c49..f8afb8364 100644
--- a/frontend/src/Components/Menu/FilterMenuItem.js
+++ b/frontend/src/Components/Menu/FilterMenuItem.js
@@ -9,12 +9,11 @@ class FilterMenuItem extends Component {
onPress = () => {
const {
- name,
- value,
+ filterKey,
onPress
} = this.props;
- onPress(name, value);
+ onPress(filterKey);
}
//
@@ -22,18 +21,14 @@ class FilterMenuItem extends Component {
render() {
const {
- name,
- value,
filterKey,
- filterValue,
+ selectedFilterKey,
...otherProps
} = this.props;
- const isSelected = name === filterKey && value === filterValue;
-
return (
@@ -42,16 +37,9 @@ class FilterMenuItem extends Component {
}
FilterMenuItem.propTypes = {
- name: PropTypes.string,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
- filterKey: PropTypes.string,
- filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
+ filterKey: PropTypes.string.isRequired,
+ selectedFilterKey: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
-FilterMenuItem.defaultProps = {
- name: null,
- value: null
-};
-
export default FilterMenuItem;
diff --git a/frontend/src/Components/Menu/MenuItemSeparator.css b/frontend/src/Components/Menu/MenuItemSeparator.css
new file mode 100644
index 000000000..a867e3153
--- /dev/null
+++ b/frontend/src/Components/Menu/MenuItemSeparator.css
@@ -0,0 +1,5 @@
+.separator {
+ overflow: hidden;
+ height: 1px;
+ background-color: $themeDarkColor;
+}
diff --git a/frontend/src/Components/Menu/MenuItemSeparator.js b/frontend/src/Components/Menu/MenuItemSeparator.js
new file mode 100644
index 000000000..e586670c9
--- /dev/null
+++ b/frontend/src/Components/Menu/MenuItemSeparator.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import styles from './MenuItemSeparator.css';
+
+function MenuItemSeparator() {
+ return (
+
+ );
+}
+
+export default MenuItemSeparator;
diff --git a/frontend/src/Components/Menu/PageMenuButton.css b/frontend/src/Components/Menu/PageMenuButton.css
new file mode 100644
index 000000000..e6954f600
--- /dev/null
+++ b/frontend/src/Components/Menu/PageMenuButton.css
@@ -0,0 +1,11 @@
+.menuButton {
+ composes: menuButton from './MenuButton.css';
+
+ &:hover {
+ color: #666;
+ }
+}
+
+.label {
+ margin-left: 5px;
+}
diff --git a/frontend/src/Components/Menu/PageMenuButton.js b/frontend/src/Components/Menu/PageMenuButton.js
new file mode 100644
index 000000000..abbfc98f8
--- /dev/null
+++ b/frontend/src/Components/Menu/PageMenuButton.js
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Icon from 'Components/Icon';
+import MenuButton from 'Components/Menu/MenuButton';
+import styles from './PageMenuButton.css';
+
+function PageMenuButton(props) {
+ const {
+ iconName,
+ text,
+ ...otherProps
+ } = props;
+
+ return (
+
+
+
+
+ {text}
+
+
+ );
+}
+
+PageMenuButton.propTypes = {
+ iconName: PropTypes.object.isRequired,
+ text: PropTypes.string
+};
+
+export default PageMenuButton;
diff --git a/frontend/src/Components/MonitorToggleButton.css b/frontend/src/Components/MonitorToggleButton.css
index e2c68bed1..794af1e98 100644
--- a/frontend/src/Components/MonitorToggleButton.css
+++ b/frontend/src/Components/MonitorToggleButton.css
@@ -5,9 +5,7 @@
font-size: inherit;
}
-.disabledButton {
- composes: button from 'Components/Link/IconButton.css';
-
+.isDisabled {
color: $disabledColor;
cursor: not-allowed;
}
diff --git a/frontend/src/Components/MonitorToggleButton.js b/frontend/src/Components/MonitorToggleButton.js
index 8802cb1a2..c92db9bc0 100644
--- a/frontend/src/Components/MonitorToggleButton.js
+++ b/frontend/src/Components/MonitorToggleButton.js
@@ -1,10 +1,22 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
+import classNames from 'classnames';
import { icons } from 'Helpers/Props';
-import Icon from 'Components/Icon';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import styles from './MonitorToggleButton.css';
+function getTooltip(monitored, isDisabled) {
+ if (isDisabled) {
+ return 'Cannot toogle monitored state when artist is unmonitored';
+ }
+
+ if (monitored) {
+ return 'Monitored, click to unmonitor';
+ }
+
+ return 'Unmonitored, click to monitor';
+}
+
class MonitorToggleButton extends Component {
//
@@ -29,27 +41,18 @@ class MonitorToggleButton extends Component {
...otherProps
} = this.props;
- const monitoredMessage = 'Monitored, click to unmonitor';
- const unmonitoredMessage = 'Unmonitored, click to monitor';
const iconName = monitored ? icons.MONITORED : icons.UNMONITORED;
- if (isDisabled) {
- return (
-
- );
- }
-
return (
{
+ onSuggestionSelected = (event, { suggestion }) => {
if (suggestion.type === ADD_NEW_TYPE) {
this.props.onGoToAddNewArtist(this.state.value);
} else {
@@ -181,7 +181,7 @@ class ArtistSearchInput extends Component {
});
}
- if (suggestions.length <= 3) {
+ if (value.length >= 3) {
suggestionGroups.push({
title: 'Add New Artist',
suggestions: [
@@ -218,10 +218,7 @@ class ArtistSearchInput extends Component {
return (
-
+
-
+