diff --git a/appveyor.yml b/appveyor.yml index 8413b8ff6..00cb791ce 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -56,4 +56,5 @@ only_commits: - appveyor.yml - build.sh - test.sh + - package.json - appveyor-package.sh diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js index 5faec4c87..50aefcc1a 100644 --- a/frontend/gulp/webpack.js +++ b/frontend/gulp/webpack.js @@ -5,6 +5,7 @@ const path = require('path'); const webpack = require('webpack'); const errorHandler = require('./helpers/errorHandler'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); const uiFolder = 'UI'; const root = path.join(__dirname, '..', 'src'); @@ -27,19 +28,49 @@ const extractCSSPlugin = new ExtractTextPlugin({ ignoreOrder: true }); +const plugins = [ + extractCSSPlugin, + + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor' + }), + + new webpack.DefinePlugin({ + __DEV__: !isProduction, + 'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development') + }) +]; + +if (isProduction) { + plugins.push(new UglifyJSPlugin({ + sourceMap: true, + uglifyOptions: { + mangle: false, + output: { + comments: false, + beautify: true + } + } + })); +} + const config = { devtool: '#source-map', + stats: { children: false }, + watchOptions: { ignored: /node_modules/ }, + entry: { preload: 'preload.js', vendor: 'vendor.js', index: 'index.js' }, + resolve: { modules: [ root, @@ -50,35 +81,21 @@ const config = { jquery: 'jquery/src/jquery' } }, + output: { filename: path.join('_output', uiFolder, '[name].js'), sourceMapFilename: '[file].map' }, - plugins: [ - extractCSSPlugin, - new webpack.optimize.CommonsChunkPlugin({ - name: 'vendor' - }), - - new webpack.DefinePlugin({ - __DEV__: !isProduction, - 'process.env': { - NODE_ENV: isProduction ? JSON.stringify('production') : JSON.stringify('development') - } - }) - ], + + plugins, + resolveLoader: { modules: [ 'node_modules', 'frontend/gulp/webpack/' ] }, - // TODO: Do we need this loader? - // eslint: { - // formatter: function(results) { - // return JSON.stringify(results); - // } - // }, + module: { rules: [ { diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.css b/frontend/src/Activity/Blacklist/BlacklistRow.css index 030dfe98a..b62d1e750 100644 --- a/frontend/src/Activity/Blacklist/BlacklistRow.css +++ b/frontend/src/Activity/Blacklist/BlacklistRow.css @@ -11,8 +11,8 @@ width: 80px; } -.details { +.actions { composes: cell from 'Components/Table/Cells/TableRowCell.css'; - width: 30px; + width: 70px; } diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.js b/frontend/src/Activity/Blacklist/BlacklistRow.js index f67e69723..f766ca1ad 100644 --- a/frontend/src/Activity/Blacklist/BlacklistRow.js +++ b/frontend/src/Activity/Blacklist/BlacklistRow.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { icons } from 'Helpers/Props'; +import { icons, kinds } from 'Helpers/Props'; import IconButton from 'Components/Link/IconButton'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import TableRow from 'Components/Table/TableRow'; @@ -48,7 +48,8 @@ class BlacklistRow extends Component { protocol, indexer, message, - columns + columns, + onRemovePress } = this.props; return ( @@ -129,16 +130,21 @@ class BlacklistRow extends Component { ); } - if (name === 'details') { + if (name === 'actions') { return ( + ); } @@ -171,7 +177,8 @@ BlacklistRow.propTypes = { protocol: PropTypes.string.isRequired, indexer: PropTypes.string, message: PropTypes.string, - columns: PropTypes.arrayOf(PropTypes.object).isRequired + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onRemovePress: PropTypes.func.isRequired }; export default BlacklistRow; diff --git a/frontend/src/Activity/Blacklist/BlacklistRowConnector.js b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js index f4f9217bf..a85f1f78b 100644 --- a/frontend/src/Activity/Blacklist/BlacklistRowConnector.js +++ b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { removeFromBlacklist } from 'Store/Actions/blacklistActions'; import BlacklistRow from './BlacklistRow'; function createMapStateToProps() { @@ -14,4 +15,12 @@ function createMapStateToProps() { ); } -export default connect(createMapStateToProps)(BlacklistRow); +function createMapDispatchToProps(dispatch, props) { + return { + onRemovePress() { + dispatch(removeFromBlacklist({ id: props.id })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow); diff --git a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js index 19cbe682c..c186469e8 100644 --- a/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js +++ b/frontend/src/AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolder.js @@ -106,9 +106,7 @@ class ImportArtistSelectFolder extends Component { { items.length > 0 ?
-
+
diff --git a/frontend/src/Album/SceneInfo.css b/frontend/src/Album/SceneInfo.css index af4908d4d..3efb78509 100644 --- a/frontend/src/Album/SceneInfo.css +++ b/frontend/src/Album/SceneInfo.css @@ -1,3 +1,9 @@ +.descriptionList { + composes: descriptionList from 'Components/DescriptionList/DescriptionList.css'; + + margin-right: 10px; +} + .title { composes: title from 'Components/DescriptionList/DescriptionListItemTitle.css'; diff --git a/frontend/src/Album/SceneInfo.js b/frontend/src/Album/SceneInfo.js index 0c9ffa8cd..ed171248a 100644 --- a/frontend/src/Album/SceneInfo.js +++ b/frontend/src/Album/SceneInfo.js @@ -14,7 +14,7 @@ function SceneInfo(props) { } = props; return ( - + { sceneSeasonNumber !== undefined && diff --git a/frontend/src/Album/Search/InteractiveAlbumSearchModalContentConnector.js b/frontend/src/Album/Search/InteractiveAlbumSearchModalContentConnector.js index 443c6676c..353099a60 100644 --- a/frontend/src/Album/Search/InteractiveAlbumSearchModalContentConnector.js +++ b/frontend/src/Album/Search/InteractiveAlbumSearchModalContentConnector.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import * as releaseActions from 'Store/Actions/releaseActions'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; @@ -10,7 +10,7 @@ import InteractiveAlbumSearchModalContent from './InteractiveAlbumSearchModalCon function createMapStateToProps() { return createSelector( (state) => state.releases.items.length, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('releases'), createUISettingsSelector(), (totalReleasesCount, releases, uiSettings) => { return { @@ -95,10 +95,4 @@ InteractiveAlbumSearchModalContentConnector.propTypes = { dispatchCancelFetchReleases: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - createMapDispatchToProps, - undefined, - undefined, - { section: 'releases' } -)(InteractiveAlbumSearchModalContentConnector); +export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveAlbumSearchModalContentConnector); diff --git a/frontend/src/Album/Search/InteractiveSearchFilterModalConnector.js b/frontend/src/Album/Search/InteractiveSearchFilterModalConnector.js index 92067c526..65f9a2d45 100644 --- a/frontend/src/Album/Search/InteractiveSearchFilterModalConnector.js +++ b/frontend/src/Album/Search/InteractiveSearchFilterModalConnector.js @@ -18,8 +18,8 @@ function createMapStateToProps() { function createMapDispatchToProps(dispatch, props) { return { - onRemoveCustomFilterPress(index) { - dispatch(releaseActions.removeReleasesCustomFilter({ index })); + onRemoveCustomFilterPress(payload) { + dispatch(releaseActions.removeReleasesCustomFilter(payload)); }, onSaveCustomFilterPress(payload) { diff --git a/frontend/src/AlbumStudio/AlbumStudio.js b/frontend/src/AlbumStudio/AlbumStudio.js index ce8e5a908..e48a49527 100644 --- a/frontend/src/AlbumStudio/AlbumStudio.js +++ b/frontend/src/AlbumStudio/AlbumStudio.js @@ -101,6 +101,7 @@ class AlbumStudio extends Component { isFetching, isPopulated, error, + totalItems, items, selectedFilterKey, filters, @@ -178,7 +179,7 @@ class AlbumStudio extends Component { { !error && isPopulated && !items.length && - + } @@ -197,6 +198,7 @@ AlbumStudio.propTypes = { isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, + totalItems: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.oneOf(sortDirections.all), diff --git a/frontend/src/AlbumStudio/AlbumStudioAlbum.js b/frontend/src/AlbumStudio/AlbumStudioAlbum.js index 65bb4cb0c..3022f7d06 100644 --- a/frontend/src/AlbumStudio/AlbumStudioAlbum.js +++ b/frontend/src/AlbumStudio/AlbumStudioAlbum.js @@ -26,7 +26,7 @@ class AlbumStudioAlbum extends Component { id, title, monitored, - statistics, + statistics = {}, isSaving } = this.props; diff --git a/frontend/src/AlbumStudio/AlbumStudioConnector.js b/frontend/src/AlbumStudio/AlbumStudioConnector.js index 3eee418fa..12e863c24 100644 --- a/frontend/src/AlbumStudio/AlbumStudioConnector.js +++ b/frontend/src/AlbumStudio/AlbumStudioConnector.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { setAlbumStudioSort, setAlbumStudioFilter, saveAlbumStudio } from 'Store/Actions/albumStudioActions'; import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions'; @@ -9,7 +9,7 @@ import AlbumStudio from './AlbumStudio'; function createMapStateToProps() { return createSelector( - createClientSideCollectionSelector(), + createClientSideCollectionSelector('artist', 'albumStudio'), (artist) => { return { ...artist @@ -88,10 +88,4 @@ AlbumStudioConnector.propTypes = { saveAlbumStudio: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'artist', uiSection: 'albumStudio' } -)(AlbumStudioConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioConnector); diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index b36b27fbf..7a75d774e 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -26,6 +26,7 @@ import ImportListSettings from 'Settings/ImportLists/ImportListSettings'; import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings'; import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import MetadataSettings from 'Settings/Metadata/MetadataSettings'; +import TagSettings from 'Settings/Tags/TagSettings'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import UISettingsConnector from 'Settings/UI/UISettingsConnector'; import Status from 'System/Status/Status'; @@ -191,6 +192,11 @@ function AppRoutes(props) { component={MetadataSettings} /> + + label, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('albums'), createArtistSelector(), createCommandsSelector(), createDimensionsSelector(), @@ -96,10 +95,4 @@ ArtistDetailsSeasonConnector.propTypes = { executeCommand: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'albums' } -)(ArtistDetailsSeasonConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector); diff --git a/frontend/src/Artist/Editor/ArtistEditor.js b/frontend/src/Artist/Editor/ArtistEditor.js index 2ecf9abf3..714041600 100644 --- a/frontend/src/Artist/Editor/ArtistEditor.js +++ b/frontend/src/Artist/Editor/ArtistEditor.js @@ -13,9 +13,10 @@ import FilterMenu from 'Components/Menu/FilterMenu'; import Table from 'Components/Table/Table'; import TableBody from 'Components/Table/TableBody'; import NoArtist from 'Artist/NoArtist'; +import OrganizeArtistModal from './Organize/OrganizeArtistModal'; import ArtistEditorRowConnector from './ArtistEditorRowConnector'; import ArtistEditorFooter from './ArtistEditorFooter'; -import OrganizeArtistModal from './Organize/OrganizeArtistModal'; +import ArtistEditorFilterModalConnector from './ArtistEditorFilterModalConnector'; function getColumns(showLanguageProfile, showMetadataProfile) { return [ @@ -148,6 +149,7 @@ class ArtistEditor extends Component { isFetching, isPopulated, error, + totalItems, items, selectedFilterKey, filters, @@ -184,6 +186,7 @@ class ArtistEditor extends Component { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} + filterModalConnectorComponent={ArtistEditorFilterModalConnector} onFilterSelect={onFilterSelect} /> @@ -234,7 +237,7 @@ class ArtistEditor extends Component { { !error && isPopulated && !items.length && - + } @@ -266,6 +269,7 @@ ArtistEditor.propTypes = { isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, + totalItems: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, sortKey: PropTypes.string, sortDirection: PropTypes.oneOf(sortDirections.all), diff --git a/frontend/src/Artist/Editor/ArtistEditorConnector.js b/frontend/src/Artist/Editor/ArtistEditorConnector.js index 319a98aac..3190fbc83 100644 --- a/frontend/src/Artist/Editor/ArtistEditorConnector.js +++ b/frontend/src/Artist/Editor/ArtistEditorConnector.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandSelector from 'Store/Selectors/createCommandSelector'; import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions'; @@ -14,7 +14,7 @@ function createMapStateToProps() { return createSelector( (state) => state.settings.languageProfiles, (state) => state.settings.metadataProfiles, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('artist', 'artistEditor'), createCommandSelector(commandNames.RENAME_ARTIST), (languageProfiles, metadataProfiles, artist, isOrganizingArtist) => { return { @@ -89,10 +89,4 @@ ArtistEditorConnector.propTypes = { dispatchExecuteCommand: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'artist', uiSection: 'artistEditor' } -)(ArtistEditorConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistEditorConnector); diff --git a/frontend/src/Artist/Editor/ArtistEditorFilterModalConnector.js b/frontend/src/Artist/Editor/ArtistEditorFilterModalConnector.js new file mode 100644 index 000000000..8b36fa2d2 --- /dev/null +++ b/frontend/src/Artist/Editor/ArtistEditorFilterModalConnector.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as artistEditorActions from 'Store/Actions/artistEditorActions'; +import FilterModal from 'Components/Filter/FilterModal'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artist.items, + (state) => state.artistEditor.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onRemoveCustomFilterPress(payload) { + dispatch(artistEditorActions.removeArtistEditorCustomFilter(payload)); + }, + + onSaveCustomFilterPress(payload) { + dispatch(artistEditorActions.saveArtistEditorCustomFilter(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal); diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js index 213c8860f..de3f1a31a 100644 --- a/frontend/src/Artist/Index/ArtistIndex.js +++ b/frontend/src/Artist/Index/ArtistIndex.js @@ -185,6 +185,7 @@ class ArtistIndex extends Component { isFetching, isPopulated, error, + totalItems, items, selectedFilterKey, filters, @@ -341,7 +342,7 @@ class ArtistIndex extends Component { { !error && isPopulated && !items.length && - + } @@ -379,6 +380,7 @@ ArtistIndex.propTypes = { isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, + totalItems: PropTypes.number.isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired, selectedFilterKey: PropTypes.string.isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired, diff --git a/frontend/src/Artist/Index/ArtistIndexConnector.js b/frontend/src/Artist/Index/ArtistIndexConnector.js index 0f0186e3c..3140861e1 100644 --- a/frontend/src/Artist/Index/ArtistIndexConnector.js +++ b/frontend/src/Artist/Index/ArtistIndexConnector.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import dimensions from 'Styles/Variables/dimensions'; import createCommandSelector from 'Store/Selectors/createCommandSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -45,18 +46,21 @@ function getScrollTop(view, scrollTop, isSmallScreen) { function createMapStateToProps() { return createSelector( - (state) => state.artist, - (state) => state.artistIndex, + createClientSideCollectionSelector('artist', 'artistIndex'), createCommandSelector(commandNames.REFRESH_ARTIST), createCommandSelector(commandNames.RSS_SYNC), createDimensionsSelector(), - (artist, artistIndex, isRefreshingArtist, isRssSyncExecuting, dimensionsState) => { + ( + artist, + isRefreshingArtist, + isRssSyncExecuting, + dimensionsState + ) => { return { + ...artist, isRefreshingArtist, isRssSyncExecuting, - isSmallScreen: dimensionsState.isSmallScreen, - ...artist, - ...artistIndex + isSmallScreen: dimensionsState.isSmallScreen }; } ); diff --git a/frontend/src/Artist/Index/ArtistIndexFilterModalConnector.js b/frontend/src/Artist/Index/ArtistIndexFilterModalConnector.js new file mode 100644 index 000000000..0f77138b2 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFilterModalConnector.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import * as artistIndexActions from 'Store/Actions/artistIndexActions'; +import FilterModal from 'Components/Filter/FilterModal'; + +function createMapStateToProps() { + return createSelector( + (state) => state.artist.items, + (state) => state.artistIndex.filterBuilderProps, + (sectionItems, filterBuilderProps) => { + return { + sectionItems, + filterBuilderProps + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onRemoveCustomFilterPress(payload) { + dispatch(artistIndexActions.removeArtistCustomFilter(payload)); + }, + + onSaveCustomFilterPress(payload) { + dispatch(artistIndexActions.saveArtistCustomFilter(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal); diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.js b/frontend/src/Artist/Index/ArtistIndexFooter.js index 4d4aed44d..2e3f1e5d5 100644 --- a/frontend/src/Artist/Index/ArtistIndexFooter.js +++ b/frontend/src/Artist/Index/ArtistIndexFooter.js @@ -15,8 +15,16 @@ function ArtistIndexFooter({ artist }) { let totalFileSize = 0; artist.forEach((s) => { - tracks += s.statistics.trackCount || 0; - trackFiles += s.statistics.trackFileCount || 0; + const { statistics = {} } = s; + + const { + trackCount = 0, + trackFileCount = 0, + sizeOnDisk = 0 + } = statistics; + + tracks += trackCount; + trackFiles += trackFileCount; if (s.status === 'ended') { ended++; @@ -28,7 +36,7 @@ function ArtistIndexFooter({ artist }) { monitored++; } - totalFileSize += s.statistics.sizeOnDisk || 0; + totalFileSize += sizeOnDisk; }); return ( diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js index 25bdf61cc..70e3f049c 100644 --- a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js +++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js @@ -1,5 +1,5 @@ +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -8,7 +8,7 @@ import ArtistIndexBanners from './ArtistIndexBanners'; function createMapStateToProps() { return createSelector( (state) => state.artistIndex.bannerOptions, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('artist', 'artistIndex'), createUISettingsSelector(), createDimensionsSelector(), (bannerOptions, artist, uiSettings, dimensions) => { @@ -24,10 +24,4 @@ function createMapStateToProps() { ); } -export default connectSection( - createMapStateToProps, - undefined, - undefined, - undefined, - { section: 'artist', uiSection: 'artistIndex' } -)(ArtistIndexBanners); +export default connect(createMapStateToProps)(ArtistIndexBanners); diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js index cf9b25ea1..dc5540410 100644 --- a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js +++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { align } from 'Helpers/Props'; import FilterMenu from 'Components/Menu/FilterMenu'; +import ArtistIndexFilterModalConnector from 'Artist/Index/ArtistIndexFilterModalConnector'; function ArtistIndexFilterMenu(props) { const { @@ -19,6 +20,7 @@ function ArtistIndexFilterMenu(props) { selectedFilterKey={selectedFilterKey} filters={filters} customFilters={customFilters} + filterModalConnectorComponent={ArtistIndexFilterModalConnector} onFilterSelect={onFilterSelect} /> ); diff --git a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js index 3465729cc..95908079c 100644 --- a/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js +++ b/frontend/src/Artist/Index/Overview/ArtistIndexOverviewsConnector.js @@ -1,5 +1,5 @@ +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -8,7 +8,7 @@ import ArtistIndexOverviews from './ArtistIndexOverviews'; function createMapStateToProps() { return createSelector( (state) => state.artistIndex.overviewOptions, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('artist', 'artistIndex'), createUISettingsSelector(), createDimensionsSelector(), (overviewOptions, artist, uiSettings, dimensions) => { @@ -24,10 +24,4 @@ function createMapStateToProps() { ); } -export default connectSection( - createMapStateToProps, - undefined, - undefined, - undefined, - { section: 'artist', uiSection: 'artistIndex' } -)(ArtistIndexOverviews); +export default connect(createMapStateToProps)(ArtistIndexOverviews); diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js index 786b187a8..25ef5b4ed 100644 --- a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js @@ -1,5 +1,5 @@ +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; @@ -8,7 +8,7 @@ import ArtistIndexPosters from './ArtistIndexPosters'; function createMapStateToProps() { return createSelector( (state) => state.artistIndex.posterOptions, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('artist', 'artistIndex'), createUISettingsSelector(), createDimensionsSelector(), (posterOptions, artist, uiSettings, dimensions) => { @@ -24,10 +24,4 @@ function createMapStateToProps() { ); } -export default connectSection( - createMapStateToProps, - undefined, - undefined, - undefined, - { section: 'artist', uiSection: 'artistIndex' } -)(ArtistIndexPosters); +export default connect(createMapStateToProps)(ArtistIndexPosters); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css index 299ca4855..bffcb5fd8 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css @@ -20,7 +20,8 @@ .nextAlbum, .lastAlbum, -.added { +.added, +.genres { composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; flex: 0 0 180px; @@ -58,6 +59,12 @@ flex: 0 0 120px; } +.ratings { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 80px; +} + .tags { composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css b/frontend/src/Artist/Index/Table/ArtistIndexRow.css index 4c869d02e..0e051087c 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.css +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css @@ -20,7 +20,8 @@ .nextAlbum, .lastAlbum, -.added { +.added, +.genres { composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; flex: 0 0 180px; @@ -60,6 +61,12 @@ flex: 0 0 120px; } +.ratings { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 80px; +} + .tags { composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js index c1907693d..87609c37b 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.js +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import getProgressBarKind from 'Utilities/Artist/getProgressBarKind'; import formatBytes from 'Utilities/Number/formatBytes'; import { icons } from 'Helpers/Props'; +import HeartRating from 'Components/HeartRating'; import IconButton from 'Components/Link/IconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import ProgressBar from 'Components/ProgressBar'; @@ -75,6 +76,8 @@ class ArtistIndexRow extends Component { lastAlbum, added, statistics, + genres, + ratings, path, tags, columns, @@ -303,6 +306,34 @@ class ArtistIndexRow extends Component { ); } + if (name === 'genres') { + const joinedGenres = genres.join(', '); + + return ( + + + {joinedGenres} + + + ); + } + + if (name === 'ratings') { + return ( + + + + ); + } + if (name === 'tags') { return ( state.app.dimensions, - createClientSideCollectionSelector(), + createClientSideCollectionSelector('artist', 'artistIndex'), (dimensions, artist) => { return { isSmallScreen: dimensions.isSmallScreen, @@ -25,10 +25,4 @@ function createMapDispatchToProps(dispatch, props) { }; } -export default connectSection( - createMapStateToProps, - createMapDispatchToProps, - undefined, - undefined, - { section: 'artist', uiSection: 'artistIndex' } -)(ArtistIndexTable); +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexTable); diff --git a/frontend/src/Artist/NoArtist.js b/frontend/src/Artist/NoArtist.js index b6e90cf63..b869a8d58 100644 --- a/frontend/src/Artist/NoArtist.js +++ b/frontend/src/Artist/NoArtist.js @@ -1,9 +1,22 @@ +import PropTypes from 'prop-types'; import React from 'react'; import { kinds } from 'Helpers/Props'; import Button from 'Components/Link/Button'; import styles from './NoArtist.css'; -function NoArtist() { +function NoArtist(props) { + const { totalItems } = props; + + if (totalItems > 0) { + return ( +
+
+ All artists are hidden due to the applied filter. +
+
+ ); + } + return (
@@ -31,4 +44,8 @@ function NoArtist() { ); } +NoArtist.propTypes = { + totalItems: PropTypes.number.isRequired +}; + export default NoArtist; diff --git a/frontend/src/Calendar/Day/CalendarDayConnector.js b/frontend/src/Calendar/Day/CalendarDayConnector.js index 5eb1ebf51..30ad0cb61 100644 --- a/frontend/src/Calendar/Day/CalendarDayConnector.js +++ b/frontend/src/Calendar/Day/CalendarDayConnector.js @@ -2,14 +2,15 @@ import _ from 'lodash'; import moment from 'moment'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import CalendarDay from './CalendarDay'; function createCalendarEventsConnector() { return createSelector( (state, { date }) => date, - (state) => state.calendar, + createClientSideCollectionSelector('calendar'), (date, calendar) => { const filtered = _.filter(calendar.items, (item) => { return moment(date).isSame(moment(item.releaseDate), 'day'); @@ -52,10 +53,4 @@ CalendarDayConnector.propTypes = { date: PropTypes.string.isRequired }; -export default connectSection( - createMapStateToProps, - undefined, - undefined, - undefined, - { section: 'calendar' } -)(CalendarDayConnector); +export default connect(createMapStateToProps)(CalendarDayConnector); diff --git a/frontend/src/Components/DescriptionList/DescriptionList.css b/frontend/src/Components/DescriptionList/DescriptionList.css index 94cd75ba9..230347f80 100644 --- a/frontend/src/Components/DescriptionList/DescriptionList.css +++ b/frontend/src/Components/DescriptionList/DescriptionList.css @@ -1,4 +1,4 @@ .descriptionList { margin-top: 0; - margin-bottom: 20px; + margin-bottom: 0; } diff --git a/frontend/src/Components/DescriptionList/DescriptionList.js b/frontend/src/Components/DescriptionList/DescriptionList.js index b7a1d1634..be2c87c55 100644 --- a/frontend/src/Components/DescriptionList/DescriptionList.js +++ b/frontend/src/Components/DescriptionList/DescriptionList.js @@ -9,11 +9,12 @@ class DescriptionList extends Component { render() { const { + className, children } = this.props; return ( -
+
{children}
); @@ -21,7 +22,12 @@ class DescriptionList extends Component { } DescriptionList.propTypes = { + className: PropTypes.string.isRequired, children: PropTypes.node }; +DescriptionList.defaultProps = { + className: styles.descriptionList +}; + export default DescriptionList; diff --git a/frontend/src/Components/Filter/Builder/ArtistStatusFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/ArtistStatusFilterBuilderRowValue.js new file mode 100644 index 000000000..28070d200 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/ArtistStatusFilterBuilderRowValue.js @@ -0,0 +1,18 @@ +import React from 'react'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +const protocols = [ + { id: 'continuing', name: 'Continuing' }, + { id: 'ended', name: 'Ended' } +]; + +function ArtistStatusFilterBuilderRowValue(props) { + return ( + + ); +} + +export default ArtistStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/BoolFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/BoolFilterBuilderRowValue.js new file mode 100644 index 000000000..eea574dd1 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/BoolFilterBuilderRowValue.js @@ -0,0 +1,18 @@ +import React from 'react'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +const protocols = [ + { id: true, name: 'true' }, + { id: false, name: 'false' } +]; + +function BoolFilterBuilderRowValue(props) { + return ( + + ); +} + +export default BoolFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.css b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.css new file mode 100644 index 000000000..12047c80d --- /dev/null +++ b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.css @@ -0,0 +1,15 @@ +.container { + display: flex; +} + +.numberInput { + composes: text from 'Components/Form/TextInput.css'; + + margin-right: 3px; +} + +.selectInput { + composes: select from 'Components/Form/SelectInput.css'; + + margin-left: 3px; +} diff --git a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js new file mode 100644 index 000000000..f0c2d3626 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js @@ -0,0 +1,171 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import isString from 'Utilities/String/isString'; +import { IN_LAST, IN_NEXT } from 'Helpers/Props/filterTypes'; +import NumberInput from 'Components/Form/NumberInput'; +import SelectInput from 'Components/Form/SelectInput'; +import TextInput from 'Components/Form/TextInput'; +import { NAME } from './FilterBuilderRowValue'; +import styles from './DateFilterBuilderRowValue.css'; + +const timeOptions = [ + { key: 'seconds', value: 'seconds' }, + { key: 'minutes', value: 'minutes' }, + { key: 'hours', value: 'hours' }, + { key: 'days', value: 'days' }, + { key: 'weeks', value: 'weeks' }, + { key: 'months', value: 'months' } +]; + +function isInFilter(filterType) { + return filterType === IN_LAST || filterType === IN_NEXT; +} + +class DateFilterBuilderRowValue extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + filterType, + filterValue, + onChange + } = this.props; + + if (isInFilter(filterType) && isString(filterValue)) { + onChange({ + name: NAME, + value: { + time: timeOptions[0].key, + value: null + } + }); + } + } + + componentDidUpdate(prevProps) { + const { + filterType, + filterValue, + onChange + } = this.props; + + if (prevProps.filterType === filterType) { + return; + } + + if (isInFilter(filterType) && isString(filterValue)) { + onChange({ + name: NAME, + value: { + time: timeOptions[0].key, + value: null + } + }); + + return; + } + + if (!isInFilter(filterType) && !isString(filterValue)) { + onChange({ + name: NAME, + value: '' + }); + } + } + + // + // Listeners + + onValueChange = ({ value }) => { + const { + filterValue, + onChange + } = this.props; + + let newValue = value; + + if (!isString(value)) { + newValue = { + time: filterValue.time, + value + }; + } + + onChange({ + name: NAME, + value: newValue + }); + } + + onTimeChange = ({ value }) => { + const { + filterValue, + onChange + } = this.props; + + onChange({ + name: NAME, + value: { + time: value, + value: filterValue.value + } + }); + } + + // + // Render + + render() { + const { + filterType, + filterValue + } = this.props; + + if ( + (isInFilter(filterType) && isString(filterValue)) || + (!isInFilter(filterType) && !isString(filterValue)) + ) { + return null; + } + + if (isInFilter(filterType)) { + return ( +
+ + + +
+ ); + } + + return ( + + ); + } +} + +DateFilterBuilderRowValue.propTypes = { + filterType: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, + onChange: PropTypes.func.isRequired +}; + +export default DateFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index 8541ac9e8..a068f0d0c 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -3,10 +3,17 @@ 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 BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; +import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; +import LanguageProfileFilterBuilderRowValueConnector from './LanguageProfileFilterBuilderRowValueConnector'; +import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; +import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; +import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue'; +import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import styles from './FilterBuilderRow.css'; function getselectedFilterBuilderProp(filterBuilderProps, name) { @@ -29,6 +36,14 @@ function getDefaultFilterType(selectedFilterBuilderProp) { return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key; } +function getDefaultFilterValue(selectedFilterBuilderProp) { + if (selectedFilterBuilderProp.type === filterBuilderTypes.DATE) { + return ''; + } + + return []; +} + function getRowValueConnector(selectedFilterBuilderProp) { if (!selectedFilterBuilderProp) { return FilterBuilderRowValueConnector; @@ -37,15 +52,36 @@ function getRowValueConnector(selectedFilterBuilderProp) { const valueType = selectedFilterBuilderProp.valueType; switch (valueType) { + case filterBuilderValueTypes.BOOL: + return BoolFilterBuilderRowValue; + + case filterBuilderValueTypes.DATE: + return DateFilterBuilderRowValue; + case filterBuilderValueTypes.INDEXER: return IndexerFilterBuilderRowValueConnector; + case filterBuilderValueTypes.LANGUAGE_PROFILE: + return LanguageProfileFilterBuilderRowValueConnector; + + case filterBuilderValueTypes.METADATA_PROFILE: + return MetadataProfileFilterBuilderRowValueConnector; + case filterBuilderValueTypes.PROTOCOL: return ProtocolFilterBuilderRowValue; case filterBuilderValueTypes.QUALITY: return QualityFilterBuilderRowValueConnector; + case filterBuilderValueTypes.QUALITY_PROFILE: + return QualityProfileFilterBuilderRowValueConnector; + + case filterBuilderValueTypes.ARTIST_STATUS: + return ArtistStatusFilterBuilderRowValue; + + case filterBuilderValueTypes.TAG: + return TagFilterBuilderRowValueConnector; + default: return FilterBuilderRowValueConnector; } @@ -59,9 +95,15 @@ class FilterBuilderRow extends Component { constructor(props, context) { super(props, context); - this.state = { - selectedFilterBuilderProp: null - }; + const { + filterKey, + filterBuilderProps + } = props; + + if (filterKey) { + const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey); + this.selectedFilterBuilderProp = selectedFilterBuilderProp; + } } componentDidMount() { @@ -74,7 +116,7 @@ class FilterBuilderRow extends Component { if (filterKey) { const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey); - this.setState({ selectedFilterBuilderProp }); + this.selectedFilterBuilderProp = selectedFilterBuilderProp; return; } @@ -83,13 +125,12 @@ class FilterBuilderRow extends Component { const filter = { key: selectedFilterBuilderProp.name, - value: [], + value: getDefaultFilterValue(selectedFilterBuilderProp), type: getDefaultFilterType(selectedFilterBuilderProp) }; - this.setState({ selectedFilterBuilderProp }, () => { - onFilterChange(index, filter); - }); + this.selectedFilterBuilderProp = selectedFilterBuilderProp; + onFilterChange(index, filter); } // @@ -107,13 +148,12 @@ class FilterBuilderRow extends Component { const filter = { key, - value: [], + value: getDefaultFilterValue(selectedFilterBuilderProp), type }; - this.setState({ selectedFilterBuilderProp }, () => { - onFilterChange(index, filter); - }); + this.selectedFilterBuilderProp = selectedFilterBuilderProp; + onFilterChange(index, filter); } onFilterChange = ({ name, value }) => { @@ -163,12 +203,11 @@ class FilterBuilderRow extends Component { filterType, filterValue, filterCount, - filterBuilderProps + filterBuilderProps, + sectionItems } = this.props; - const { - selectedFilterBuilderProp - } = this.state; + const selectedFilterBuilderProp = this.selectedFilterBuilderProp; const keyOptions = filterBuilderProps.map((availablePropFilter) => { return { @@ -209,8 +248,10 @@ class FilterBuilderRow extends Component { { filterValue != null && !!selectedFilterBuilderProp && } @@ -236,10 +277,11 @@ class FilterBuilderRow extends Component { FilterBuilderRow.propTypes = { index: PropTypes.number.isRequired, filterKey: PropTypes.string, - filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]), + filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]), filterType: PropTypes.string, filterCount: PropTypes.number.isRequired, filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired, + sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired, onFilterChange: PropTypes.func.isRequired, onAddPress: PropTypes.func.isRequired, onRemovePress: PropTypes.func.isRequired diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js index 0104cd4a6..58f4f2a5b 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js @@ -4,7 +4,7 @@ import { kinds, filterBuilderTypes } from 'Helpers/Props'; import TagInput, { tagShape } from 'Components/Form/TagInput'; import FilterBuilderRowValueTag from './FilterBuilderRowValueTag'; -const NAME = 'value'; +export const NAME = 'value'; class FilterBuilderRowValue extends Component { @@ -91,7 +91,7 @@ class FilterBuilderRowValue extends Component { } FilterBuilderRowValue.propTypes = { - filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, + filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number])).isRequired, selectedFilterBuilderProp: PropTypes.object.isRequired, tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, onChange: PropTypes.func.isRequired diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js index fd0832334..ac74240e4 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js @@ -1,12 +1,13 @@ import _ from 'lodash'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; +import sortByName from 'Utilities/Array/sortByName'; import { filterBuilderTypes } from 'Helpers/Props'; import FilterBuilderRowValue from './FilterBuilderRowValue'; function createTagListSelector() { return createSelector( - (state, { sectionItems }) => _.get(state, sectionItems), + (state, { sectionItems }) => sectionItems, (state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp, (sectionItems, selectedFilterBuilderProp) => { if ( @@ -19,16 +20,20 @@ function createTagListSelector() { let items = []; if (selectedFilterBuilderProp.optionsSelector) { - items = sectionItems.map(selectedFilterBuilderProp.optionsSelector); + items = selectedFilterBuilderProp.optionsSelector(sectionItems); } else { - items = sectionItems.map((item) => { + items = sectionItems.reduce((acc, item) => { const name = item[selectedFilterBuilderProp.name]; - return { - id: name, - name - }; - }); + if (name) { + acc.push({ + id: name, + name + }); + } + + return acc; + }, []).sort(sortByName); } return _.uniqBy(items, 'id'); diff --git a/frontend/src/Components/Filter/Builder/LanguageProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/LanguageProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..31b1e952a --- /dev/null +++ b/frontend/src/Components/Filter/Builder/LanguageProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.languageProfiles, + (languageProfiles) => { + const tagList = languageProfiles.items.map((languageProfile) => { + const { + id, + name + } = languageProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..89d6c06b3 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/MetadataProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.metadataProfiles, + (metadataProfiles) => { + const tagList = metadataProfiles.items.map((metadataProfile) => { + const { + id, + name + } = metadataProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..4a8b82283 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/QualityProfileFilterBuilderRowValueConnector.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityProfiles, + (qualityProfiles) => { + const tagList = qualityProfiles.items.map((qualityProfile) => { + const { + id, + name + } = qualityProfile; + + return { + id, + name + }; + }); + + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValueConnector.js new file mode 100644 index 000000000..60e04c446 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/TagFilterBuilderRowValueConnector.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; + +function createMapStateToProps() { + return createSelector( + createTagsSelector(), + (tagList) => { + return { + tagList: tagList.map((tag) => { + const { + id, + label: name + } = tag; + + return { + id, + name + }; + }) + }; + } + ); +} + +export default connect(createMapStateToProps)(FilterBuilderRowValue); diff --git a/frontend/src/Components/Form/FormInputGroup.css b/frontend/src/Components/Form/FormInputGroup.css index ef7a29809..acdeb772f 100644 --- a/frontend/src/Components/Form/FormInputGroup.css +++ b/frontend/src/Components/Form/FormInputGroup.css @@ -9,9 +9,28 @@ } .inputContainer { + position: relative; flex: 1 1 auto; } +.inputUnit { + position: absolute; + top: 0; + right: 20px; + margin-top: 7px; + width: 75px; + color: #c6c6c6; + text-align: right; + pointer-events: none; + user-select: none; +} + +.inputUnitNumber { + composes: inputUnit; + + right: 40px; +} + .pendingChangesContainer { display: flex; justify-content: flex-end; diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js index f1ed0db24..c9348acdb 100644 --- a/frontend/src/Components/Form/FormInputGroup.js +++ b/frontend/src/Components/Form/FormInputGroup.js @@ -83,6 +83,7 @@ function FormInputGroup(props) { containerClassName, inputClassName, type, + unit, buttons, helpText, helpTexts, @@ -115,6 +116,19 @@ function FormInputGroup(props) { hasButton={hasButton} {...otherProps} /> + + { + unit && +
+ {unit} +
+ }
{ @@ -219,6 +233,7 @@ FormInputGroup.propTypes = { containerClassName: PropTypes.string.isRequired, inputClassName: PropTypes.string, type: PropTypes.string.isRequired, + unit: PropTypes.string, buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), helpText: PropTypes.string, helpTexts: PropTypes.arrayOf(PropTypes.string), diff --git a/frontend/src/Components/Form/NumberInput.js b/frontend/src/Components/Form/NumberInput.js index f850bcabf..20b6fd0a1 100644 --- a/frontend/src/Components/Form/NumberInput.js +++ b/frontend/src/Components/Form/NumberInput.js @@ -48,12 +48,14 @@ class NumberInput extends Component { render() { const { + value, ...otherProps } = this.props; return ( - - Start OAuth - + {label} +
); } OAuthInput.propTypes = { + label: PropTypes.string.isRequired, authorizing: PropTypes.bool.isRequired, + error: PropTypes.object, onPress: PropTypes.func.isRequired }; +OAuthInput.defaultProps = { + label: 'Start OAuth' +}; + export default OAuthInput; diff --git a/frontend/src/Components/Form/OAuthInputConnector.js b/frontend/src/Components/Form/OAuthInputConnector.js index 6e9ad110c..d51fa0124 100644 --- a/frontend/src/Components/Form/OAuthInputConnector.js +++ b/frontend/src/Components/Form/OAuthInputConnector.js @@ -26,18 +26,17 @@ class OAuthInputConnector extends Component { componentDidUpdate(prevProps) { const { - accessToken, - accessTokenSecret, + result, onChange } = this.props; - if (accessToken && - accessToken !== prevProps.accessToken && - accessTokenSecret && - accessTokenSecret !== prevProps.accessTokenSecret) { - onChange({ name: 'AccessToken', value: accessToken }); - onChange({ name: 'AccessTokenSecret', value: accessTokenSecret }); + if (!result || result === prevProps.result) { + return; } + + Object.keys(result).forEach((key) => { + onChange({ name: key, value: result[key] }); + }); } componentWillUnmount = () => { @@ -70,8 +69,7 @@ class OAuthInputConnector extends Component { } OAuthInputConnector.propTypes = { - accessToken: PropTypes.string, - accessTokenSecret: PropTypes.string, + result: PropTypes.object, provider: PropTypes.string.isRequired, providerData: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js index c7b422251..a041a7f65 100644 --- a/frontend/src/Components/Form/ProviderFieldFormGroup.js +++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js @@ -7,19 +7,6 @@ import FormLabel from 'Components/Form/FormLabel'; import FormInputGroup from 'Components/Form/FormInputGroup'; function getType(type) { - // Textbox, - // Password, - // Checkbox, - // Select, - // Path, - // FilePath, - // Hidden, - // Tag, - // Action, - // Url, - // Captcha - // OAuth - switch (type) { case 'captcha': return inputTypes.CAPTCHA; @@ -27,6 +14,8 @@ function getType(type) { return inputTypes.CHECK; case 'password': return inputTypes.PASSWORD; + case 'number': + return inputTypes.NUMBER; case 'path': return inputTypes.PATH; case 'select': @@ -83,6 +72,7 @@ function ProviderFieldFormGroup(props) { { - this._autosuggestRef.input.focus(); - } - - onTagAdd(tag) { + addTag = _.debounce((tag) => { this.props.onTagAdd(tag); this.setState({ value: '', suggestions: [] }); + }, 250, { leading: true, trailing: false }) + + // + // Listeners + + onInputContainerPress = () => { + this._autosuggestRef.input.focus(); } onInputChange = (event, { newValue, method }) => { @@ -116,10 +116,9 @@ class TagInput extends Component { const tag = getTag(value, selectedIndex, suggestions, allowNew); if (tag) { - this.onTagAdd(tag); + this.addTag(tag); + event.preventDefault(); } - - event.preventDefault(); } } @@ -147,7 +146,7 @@ class TagInput extends Component { const tag = getTag(value, selectedIndex, suggestions, allowNew); if (tag) { - this.onTagAdd(tag); + this.addTag(tag); } } @@ -174,7 +173,7 @@ class TagInput extends Component { } onSuggestionSelected = (event, { suggestion }) => { - this.onTagAdd(suggestion); + this.addTag(suggestion); } // @@ -262,7 +261,7 @@ class TagInput extends Component { } export const tagShape = { - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + id: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]).isRequired, name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired }; diff --git a/frontend/src/Components/Label.css b/frontend/src/Components/Label.css index 62f4af6c2..6d8bce4ca 100644 --- a/frontend/src/Components/Label.css +++ b/frontend/src/Components/Label.css @@ -6,7 +6,6 @@ color: $white; text-align: center; white-space: nowrap; - font-weight: bold; line-height: 1; cursor: default; } @@ -92,6 +91,7 @@ .large { padding: 3px 7px; + font-weight: bold; font-size: 14px; } diff --git a/frontend/src/Components/Menu/Menu.js b/frontend/src/Components/Menu/Menu.js index 06e38dcf7..da778bb7a 100644 --- a/frontend/src/Components/Menu/Menu.js +++ b/frontend/src/Components/Menu/Menu.js @@ -48,6 +48,10 @@ class Menu extends Component { this.setMaxHeight(); } + componentWillUnmount() { + this._removeListener(); + } + // // Control diff --git a/frontend/src/Components/Modal/Modal.css b/frontend/src/Components/Modal/Modal.css index 2d2a7b9c5..a9b2a27ae 100644 --- a/frontend/src/Components/Modal/Modal.css +++ b/frontend/src/Components/Modal/Modal.css @@ -54,7 +54,7 @@ .extraLarge { composes: modal; - width: 1440px; + width: 1280px; } @media only screen and (max-width: $breakpointExtraLarge) { diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js index 1978cd684..79980a44f 100644 --- a/frontend/src/Components/Page/Sidebar/PageSidebar.js +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -123,6 +123,10 @@ const links = [ title: 'Metadata', to: '/settings/metadata' }, + { + title: 'Tags', + to: '/settings/tags' + }, { title: 'General', to: '/settings/general' diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js index ee158a339..1c7439aa6 100644 --- a/frontend/src/Components/SignalRConnector.js +++ b/frontend/src/Components/SignalRConnector.js @@ -5,6 +5,7 @@ import { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { repopulatePage } from 'Utilities/pagePopulator'; +import titleCase from 'Utilities/String/titleCase'; import { updateCommand, finishCommand } from 'Store/Actions/commandActions'; import { setAppValue, setVersion } from 'Store/Actions/appActions'; import { update, updateItem, removeItem } from 'Store/Actions/baseActions'; @@ -34,6 +35,13 @@ function isAppDisconnected(disconnectedTime) { return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180; } +function getHandlerName(name) { + name = titleCase(name); + name = name.replace('/', ''); + + return `handle${name}`; +} + function createMapStateToProps() { return createSelector( (state) => state.app.isReconnecting, @@ -91,6 +99,10 @@ class SignalRConnector extends Component { } componentWillUnmount() { + if (this.retryTimeoutId) { + this.retryTimeoutId = clearTimeout(this.retryTimeoutId); + } + this.signalRconnection.stop(); this.signalRconnection = null; } @@ -106,6 +118,11 @@ class SignalRConnector extends Component { } this.retryTimeoutId = setTimeout(() => { + if (!this.signalRconnection) { + console.error('signalR: Connection was disposed'); + return; + } + this.signalRconnection.start(this.signalRconnectionOptions); this.retryInterval = Math.min(this.retryInterval + 1, 10); }, this.retryInterval * 1000); @@ -117,70 +134,14 @@ class SignalRConnector extends Component { body } = message; - if (name === 'calendar') { - this.handleCalendar(body); - return; - } - - if (name === 'command') { - this.handleCommand(body); - return; - } - - if (name === 'album') { - this.handleAlbum(body); - return; - } + const handler = this[getHandlerName(name)]; - if (name === 'track') { - this.handleTrack(body); + if (handler) { + handler(body); return; } - if (name === 'trackfile') { - this.handleTrackFile(body); - return; - } - - if (name === 'health') { - this.handleHealth(body); - return; - } - - if (name === 'artist') { - this.handleArtist(body); - return; - } - - if (name === 'queue') { - this.handleQueue(body); - return; - } - - if (name === 'queue/details') { - this.handleQueueDetails(body); - return; - } - - if (name === 'queue/status') { - this.handleQueueStatus(body); - return; - } - - if (name === 'version') { - this.handleVersion(body); - return; - } - - if (name === 'wanted/cutoff') { - this.handleWantedCutoff(body); - return; - } - - if (name === 'wanted/missing') { - this.handleWantedMissing(body); - return; - } + console.error(`signalR: Unable to find handler for ${name}`); } handleCalendar = (body) => { @@ -237,7 +198,7 @@ class SignalRConnector extends Component { } } - handleHealth = (body) => { + handleHealth = () => { this.props.fetchHealth(); } @@ -252,13 +213,13 @@ class SignalRConnector extends Component { } } - handleQueue = (body) => { + handleQueue = () => { if (this.props.isQueuePopulated) { this.props.fetchQueue(); } } - handleQueueDetails = (body) => { + handleQueueDetails = () => { this.props.fetchQueueDetails(); } @@ -292,12 +253,16 @@ class SignalRConnector extends Component { } } + handleSystemTask = () => { + // No-op for now, we may want this later + } + // // Listeners onStateChanged = (change) => { const state = getState(change.newState); - console.log(`SignalR: ${state}`); + console.log(`signalR: ${state}`); if (state === 'connected') { // Clear disconnected time @@ -326,7 +291,7 @@ class SignalRConnector extends Component { } onReceived = (message) => { - console.debug('SignalR: received', message.name, message.body); + console.debug('signalR: received', message.name, message.body); this.handleMessage(message); } diff --git a/frontend/src/Components/Table/TableRow.js b/frontend/src/Components/Table/TableRow.js index 06bbbaee9..c76083183 100644 --- a/frontend/src/Components/Table/TableRow.js +++ b/frontend/src/Components/Table/TableRow.js @@ -6,6 +6,7 @@ function TableRow(props) { const { className, children, + overlayContent, ...otherProps } = props; @@ -21,7 +22,8 @@ function TableRow(props) { TableRow.propTypes = { className: PropTypes.string.isRequired, - children: PropTypes.node + children: PropTypes.node, + overlayContent: PropTypes.bool }; TableRow.defaultProps = { diff --git a/frontend/src/Components/Tooltip/Popover.css b/frontend/src/Components/Tooltip/Popover.css index cb742eca0..852ea167f 100644 --- a/frontend/src/Components/Tooltip/Popover.css +++ b/frontend/src/Components/Tooltip/Popover.css @@ -100,5 +100,5 @@ } .body { - padding: 20px; + padding: 10px; } diff --git a/frontend/src/Components/Tooltip/Popover.js b/frontend/src/Components/Tooltip/Popover.js index bcd97854e..adb6c64d6 100644 --- a/frontend/src/Components/Tooltip/Popover.js +++ b/frontend/src/Components/Tooltip/Popover.js @@ -89,6 +89,7 @@ class Popover extends Component { render() { const { + className, anchor, title, body, @@ -103,6 +104,7 @@ class Popover extends Component { {...tetherOptions[position]} > v === filterValue); + } + + return itemValue.toLowerCase().contains(filterValue.toLowerCase()); + }, + + [filterTypes.EQUAL]: function(itemValue, filterValue) { + return itemValue === filterValue; + }, + + [filterTypes.GREATER_THAN]: function(itemValue, filterValue) { + return itemValue > filterValue; + }, + + [filterTypes.GREATER_THAN_OR_EQUAL]: function(itemValue, filterValue) { + return itemValue >= filterValue; + }, + + [filterTypes.LESS_THAN]: function(itemValue, filterValue) { + return itemValue < filterValue; + }, + + [filterTypes.LESS_THAN_OR_EQUAL]: function(itemValue, filterValue) { + return itemValue <= filterValue; + }, + + [filterTypes.NOT_CONTAINS]: function(itemValue, filterValue) { + if (Array.isArray(itemValue)) { + return !itemValue.some((v) => v === filterValue); + } + + return !itemValue.toLowerCase().contains(filterValue.toLowerCase()); + }, + + [filterTypes.NOT_EQUAL]: function(itemValue, filterValue) { + return itemValue !== filterValue; + } +}; + +export default filterTypePredicates; diff --git a/frontend/src/Helpers/Props/filterTypes.js b/frontend/src/Helpers/Props/filterTypes.js index 68663ebfc..77809b8ce 100644 --- a/frontend/src/Helpers/Props/filterTypes.js +++ b/frontend/src/Helpers/Props/filterTypes.js @@ -2,8 +2,11 @@ export const CONTAINS = 'contains'; export const EQUAL = 'equal'; export const GREATER_THAN = 'greaterThan'; export const GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual'; +export const IN_LAST = 'inLast'; +export const IN_NEXT = 'inNext'; export const LESS_THAN = 'lessThan'; export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual'; +export const NOT_CONTAINS = 'notContains'; export const NOT_EQUAL = 'notEqual'; export const all = [ @@ -13,5 +16,6 @@ export const all = [ GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, + NOT_CONTAINS, NOT_EQUAL ]; diff --git a/frontend/src/Helpers/Props/index.js b/frontend/src/Helpers/Props/index.js index 0127dff3c..3f4f94f6f 100644 --- a/frontend/src/Helpers/Props/index.js +++ b/frontend/src/Helpers/Props/index.js @@ -2,6 +2,7 @@ import * as align from './align'; import * as inputTypes from './inputTypes'; import * as filterBuilderTypes from './filterBuilderTypes'; import * as filterBuilderValueTypes from './filterBuilderValueTypes'; +import filterTypePredicates from './filterTypePredicates'; import * as filterTypes from './filterTypes'; import * as icons from './icons'; import * as kinds from './kinds'; @@ -16,6 +17,7 @@ export { inputTypes, filterBuilderTypes, filterBuilderValueTypes, + filterTypePredicates, filterTypes, icons, kinds, diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.css b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.css new file mode 100644 index 000000000..d297be072 --- /dev/null +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.css @@ -0,0 +1,18 @@ +.modalBody { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + +.filterInput { + composes: text from 'Components/Form/TextInput.css'; + + flex: 0 0 auto; + margin-bottom: 20px; +} + +.scroller { + flex: 1 1 auto; +} diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js index 95ed0869a..20115214e 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContent.js @@ -1,15 +1,62 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Button from 'Components/Link/Button'; +import { scrollDirections } from 'Helpers/Props'; 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 Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import Scroller from 'Components/Scroller/Scroller'; +import TextInput from 'Components/Form/TextInput'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import SelectAlbumRow from './SelectAlbumRow'; +import styles from './SelectAlbumModalContent.css'; + +const columns = [ + { + name: 'title', + label: 'Album Title', + isVisible: true + }, + { + name: 'albumType', + label: 'Album Type', + isVisible: true + }, + { + name: 'releaseDate', + label: 'Release Date', + isVisible: true + }, + { + name: 'status', + label: 'Album Status', + isVisible: true + } +]; class SelectAlbumModalContent extends Component { + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + filter: '' + }; + } + + // + // Listeners + + onFilterChange = ({ value }) => { + this.setState({ filter: value.toLowerCase() }); + } + // // Render @@ -18,33 +65,60 @@ class SelectAlbumModalContent extends Component { items, onAlbumSelect, onModalClose, - isFetching + isFetching, + ...otherProps } = this.props; + const filter = this.state.filter; + return ( Manual Import - Select Album - + { isFetching && } - { - items.map((item) => { - return ( - - ); - }) - } + + + + { +
+ + { + items.map((item) => { + return item.title.toLowerCase().includes(filter) ? + ( + + ) : + null; + }) + } + +
+ } + diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js index ee69f06c8..6a448be6e 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumModalContentConnector.js @@ -1,8 +1,8 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import { updateInteractiveImportItem, fetchInteractiveImportAlbums, @@ -14,7 +14,7 @@ import SelectAlbumModalContent from './SelectAlbumModalContent'; function createMapStateToProps() { return createSelector( - createClientSideCollectionSelector(), + createClientSideCollectionSelector('interactiveImport.albums'), (albums) => { return albums; } @@ -92,10 +92,4 @@ SelectAlbumModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'interactiveImport.albums' } -)(SelectAlbumModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(SelectAlbumModalContentConnector); diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumRow.css b/frontend/src/InteractiveImport/Album/SelectAlbumRow.css deleted file mode 100644 index c43d879f4..000000000 --- a/frontend/src/InteractiveImport/Album/SelectAlbumRow.css +++ /dev/null @@ -1,4 +0,0 @@ -.season { - padding: 8px; - border-bottom: 1px solid $borderColor; -} diff --git a/frontend/src/InteractiveImport/Album/SelectAlbumRow.js b/frontend/src/InteractiveImport/Album/SelectAlbumRow.js index b5a3ab3a3..5bd106f75 100644 --- a/frontend/src/InteractiveImport/Album/SelectAlbumRow.js +++ b/frontend/src/InteractiveImport/Album/SelectAlbumRow.js @@ -1,7 +1,23 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import Label from 'Components/Label'; import Link from 'Components/Link/Link'; -import styles from './SelectAlbumRow.css'; + +function getTrackCountKind(monitored, trackFileCount, trackCount) { + if (trackFileCount === trackCount && trackCount > 0) { + return kinds.SUCCESS; + } + + if (!monitored) { + return kinds.WARNING; + } + + return kinds.DANGER; +} class SelectAlbumRow extends Component { @@ -16,14 +32,87 @@ class SelectAlbumRow extends Component { // Render render() { + const { + title, + albumType, + releaseDate, + statistics, + monitored, + columns + } = this.props; + + const { + trackCount, + trackFileCount, + totalTrackCount + } = statistics; + return ( - - {this.props.title} ({this.props.albumType}) - + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'title') { + return ( + + + {title} + + + ); + } + + if (name === 'albumType') { + return ( + + {albumType} + + ); + } + + if (name === 'releaseDate') { + return ( + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + return null; + }) + } + + ); } } @@ -32,7 +121,18 @@ SelectAlbumRow.propTypes = { id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, albumType: PropTypes.string.isRequired, - onAlbumSelect: PropTypes.func.isRequired + releaseDate: PropTypes.string.isRequired, + onAlbumSelect: PropTypes.func.isRequired, + statistics: PropTypes.object.isRequired, + monitored: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +SelectAlbumRow.defaultProps = { + statistics: { + trackCount: 0, + trackFileCount: 0 + } }; export default SelectAlbumRow; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js index cabd33d7c..78df1f53e 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js @@ -21,6 +21,10 @@ const recentFoldersColumns = [ { name: 'lastUsed', label: 'Last Used' + }, + { + name: 'actions', + label: '' } ]; @@ -62,6 +66,7 @@ class InteractiveImportSelectFolderModalContent extends Component { render() { const { recentFolders, + onRemoveRecentFolderPress, onModalClose } = this.props; @@ -95,6 +100,7 @@ class InteractiveImportSelectFolderModalContent extends Component { folder={recentFolder.folder} lastUsed={recentFolder.lastUsed} onPress={this.onRecentPathPress} + onRemoveRecentFolderPress={onRemoveRecentFolderPress} /> ); }) @@ -155,6 +161,7 @@ InteractiveImportSelectFolderModalContent.propTypes = { recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired, onQuickImportPress: PropTypes.func.isRequired, onInteractiveImportPress: PropTypes.func.isRequired, + onRemoveRecentFolderPress: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js index c838f3bab..8a6c58fb0 100644 --- a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import { addRecentFolder } from 'Store/Actions/interactiveImportActions'; +import { addRecentFolder, removeRecentFolder } from 'Store/Actions/interactiveImportActions'; import { executeCommand } from 'Store/Actions/commandActions'; import * as commandNames from 'Commands/commandNames'; import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent'; @@ -20,6 +20,7 @@ function createMapStateToProps() { const mapDispatchToProps = { addRecentFolder, + removeRecentFolder, executeCommand }; @@ -44,6 +45,10 @@ class InteractiveImportSelectFolderModalContentConnector extends Component { this.props.onFolderSelect(folder); } + onRemoveRecentFolderPress = (folder) => { + this.props.removeRecentFolder({ folder }); + } + // // Render @@ -57,6 +62,7 @@ class InteractiveImportSelectFolderModalContentConnector extends Component { {...this.props} onQuickImportPress={this.onQuickImportPress} onInteractiveImportPress={this.onInteractiveImportPress} + onRemoveRecentFolderPress={this.onRemoveRecentFolderPress} /> ); } @@ -67,6 +73,7 @@ InteractiveImportSelectFolderModalContentConnector.propTypes = { onFolderSelect: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired, addRecentFolder: PropTypes.func.isRequired, + removeRecentFolder: PropTypes.func.isRequired, executeCommand: PropTypes.func.isRequired }; diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.css b/frontend/src/InteractiveImport/Folder/RecentFolderRow.css new file mode 100644 index 000000000..edb55b075 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.css @@ -0,0 +1,5 @@ +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 40px; +} diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js index bc32f5749..0d3be6341 100644 --- a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js @@ -1,8 +1,11 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; import TableRowButton from 'Components/Table/TableRowButton'; import TableRowCell from 'Components/Table/Cells/TableRowCell'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import styles from './RecentFolderRow.css'; class RecentFolderRow extends Component { @@ -13,6 +16,17 @@ class RecentFolderRow extends Component { this.props.onPress(this.props.folder); } + onRemovePress = (event) => { + event.stopPropagation(); + + const { + folder, + onRemoveRecentFolderPress + } = this.props; + + onRemoveRecentFolderPress(folder); + } + // // Render @@ -27,6 +41,14 @@ class RecentFolderRow extends Component { {folder} + + + + ); } @@ -35,7 +57,8 @@ class RecentFolderRow extends Component { RecentFolderRow.propTypes = { folder: PropTypes.string.isRequired, lastUsed: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired + onPress: PropTypes.func.isRequired, + onRemoveRecentFolderPress: PropTypes.func.isRequired }; export default RecentFolderRow; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js index 0be91d2d4..c06a13f0d 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -1,8 +1,8 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import { executeCommand } from 'Store/Actions/commandActions'; @@ -11,7 +11,7 @@ import InteractiveImportModalContent from './InteractiveImportModalContent'; function createMapStateToProps() { return createSelector( - createClientSideCollectionSelector(), + createClientSideCollectionSelector('interactiveImport'), (interactiveImport) => { return interactiveImport; } @@ -125,8 +125,19 @@ class InteractiveImportModalContentConnector extends Component { return false; } + if (!quality) { + this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' }); + return false; + } + + if (!language) { + this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' }); + return false; + } + files.push({ path: item.path, + folderName: item.folderName, artistId: artist.id, albumId: album.id, trackIds: _.map(tracks, 'id'), @@ -190,10 +201,4 @@ InteractiveImportModalContentConnector.defaultProps = { filterExistingFiles: true }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'interactiveImport' } -)(InteractiveImportModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js index ab99b3e73..7a463ec61 100644 --- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -191,6 +191,8 @@ class InteractiveImportRow extends Component { const showArtistPlaceholder = isSelected && !artist; const showAlbumNumberPlaceholder = isSelected && !!artist && !album; const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length; + const showQualityPlaceholder = isSelected && !quality; + const showLanguagePlaceholder = isSelected && !language; return ( @@ -237,20 +239,36 @@ class InteractiveImportRow extends Component { className={styles.quality} onPress={this.onSelectQualityPress} > - + { + showQualityPlaceholder && + + } + + { + !showQualityPlaceholder && !!quality && + + } - + { + showLanguagePlaceholder && + + } + + { + !showLanguagePlaceholder && !!language && + + } @@ -310,16 +328,16 @@ class InteractiveImportRow extends Component { 1} - real={quality.revision.real > 0} + qualityId={quality ? quality.quality.id : 0} + proper={quality ? quality.revision.version > 1 : false} + real={quality ? quality.revision.real > 0 : false} onModalClose={this.onSelectQualityModalClose} /> diff --git a/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js index 7b73c22ee..454be7e37 100644 --- a/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js +++ b/frontend/src/InteractiveImport/Language/SelectLanguageModalContentConnector.js @@ -22,7 +22,7 @@ function createMapStateToProps() { isFetching, isPopulated, error, - items: schema.languages || [] + items: schema.languages ? [...schema.languages].reverse() : [] }; } ); diff --git a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js index 62bc61b12..a1d6f454d 100644 --- a/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js +++ b/frontend/src/InteractiveImport/Track/SelectTrackModalContentConnector.js @@ -1,8 +1,8 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; -import connectSection from 'Store/connectSection'; import { fetchTracks, setTracksSort, clearTracks } from 'Store/Actions/trackActions'; import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; @@ -10,7 +10,7 @@ import SelectTrackModalContent from './SelectTrackModalContent'; function createMapStateToProps() { return createSelector( - createClientSideCollectionSelector(), + createClientSideCollectionSelector('tracks'), (tracks) => { return tracks; } @@ -94,10 +94,4 @@ SelectTrackModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'tracks' } -)(SelectTrackModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(SelectTrackModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js index fe5371e4f..029845025 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js @@ -59,9 +59,7 @@ class DownloadClients extends Component { } = this.state; return ( -
+
diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js index bf9acb98c..75f6f0bc3 100644 --- a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js @@ -1,15 +1,15 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, testDownloadClient } from 'Store/Actions/settingsActions'; -import connectSection from 'Store/connectSection'; import EditDownloadClientModalContent from './EditDownloadClientModalContent'; function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - createProviderSettingsSelector(), + createProviderSettingsSelector('downloadClients'), (advancedSettings, downloadClient) => { return { advancedSettings, @@ -85,10 +85,4 @@ EditDownloadClientModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'downloadClients' } -)(EditDownloadClientModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditDownloadClientModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js index 2ec9b417d..c345feb5b 100644 --- a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js @@ -33,9 +33,7 @@ function DownloadClientOptions(props) { { hasSettings && !isFetching && !error &&
-
+
Enable diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js index 4dd67f356..ef97de47f 100644 --- a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js @@ -1,16 +1,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import { fetchDownloadClientOptions, setDownloadClientOptionsValue, saveDownloadClientOptions } from 'Store/Actions/settingsActions'; import { clearPendingChanges } from 'Store/Actions/baseActions'; -import connectSection from 'Store/connectSection'; import DownloadClientOptions from './DownloadClientOptions'; +const SECTION = 'downloadClientOptions'; + function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), (advancedSettings, sectionSettings) => { return { advancedSettings, @@ -62,7 +64,7 @@ class DownloadClientOptionsConnector extends Component { } componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: this.props.section }); + this.props.dispatchClearPendingChanges({ section: SECTION }); } // @@ -86,7 +88,6 @@ class DownloadClientOptionsConnector extends Component { } DownloadClientOptionsConnector.propTypes = { - section: PropTypes.string.isRequired, isSaving: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired, dispatchFetchDownloadClientOptions: PropTypes.func.isRequired, @@ -97,10 +98,4 @@ DownloadClientOptionsConnector.propTypes = { onChildStateChange: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'settings.downloadClientOptions' } -)(DownloadClientOptionsConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientOptionsConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js index 5ba30d614..f66113619 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { sizes } from 'Helpers/Props'; import Modal from 'Components/Modal/Modal'; import EditRemotePathMappingModalContentConnector from './EditRemotePathMappingModalContentConnector'; function EditRemotePathMappingModal({ isOpen, onModalClose, ...otherProps }) { return ( diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js index 93d022e02..f633a3279 100644 --- a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js @@ -44,9 +44,7 @@ class RemotePathMappings extends Component { } = this.props; return ( -
+
@@ -64,7 +65,8 @@ function BackupSettings(props) { diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js index de4a03632..78f3b0943 100644 --- a/frontend/src/Settings/General/GeneralSettingsConnector.js +++ b/frontend/src/Settings/General/GeneralSettingsConnector.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; @@ -9,14 +10,15 @@ import { setGeneralSettingsValue, saveGeneralSettings, fetchGeneralSettings } fr import { clearPendingChanges } from 'Store/Actions/baseActions'; import { executeCommand } from 'Store/Actions/commandActions'; import { restart } from 'Store/Actions/systemActions'; -import connectSection from 'Store/connectSection'; import * as commandNames from 'Commands/commandNames'; import GeneralSettings from './GeneralSettings'; +const SECTION = 'general'; + function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), createCommandsSelector(), createSystemStatusSelector(), (advancedSettings, sectionSettings, commands, systemStatus) => { @@ -59,7 +61,7 @@ class GeneralSettingsConnector extends Component { } componentWillUnmount() { - this.props.clearPendingChanges({ section: this.props.section }); + this.props.clearPendingChanges({ section: SECTION }); } // @@ -98,7 +100,6 @@ class GeneralSettingsConnector extends Component { } GeneralSettingsConnector.propTypes = { - section: PropTypes.string.isRequired, isResettingApiKey: PropTypes.bool.isRequired, setGeneralSettingsValue: PropTypes.func.isRequired, saveGeneralSettings: PropTypes.func.isRequired, @@ -108,10 +109,4 @@ GeneralSettingsConnector.propTypes = { clearPendingChanges: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'settings.general' } -)(GeneralSettingsConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(GeneralSettingsConnector); diff --git a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js index e98b1dbf2..40fb07505 100644 --- a/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js +++ b/frontend/src/Settings/ImportLists/ImportLists/EditImportListModalContentConnector.js @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import { setImportListValue, setImportListFieldValue, saveImportList, testImportList } from 'Store/Actions/settingsActions'; -import connectSection from 'Store/connectSection'; import EditImportListModalContent from './EditImportListModalContent'; function createMapStateToProps() { @@ -11,7 +11,7 @@ function createMapStateToProps() { (state) => state.settings.advancedSettings, (state) => state.settings.languageProfiles, (state) => state.settings.metadataProfiles, - createProviderSettingsSelector(), + createProviderSettingsSelector('importLists'), (advancedSettings, languageProfiles, metadataProfiles, importList) => { return { advancedSettings, @@ -89,10 +89,4 @@ EditImportListModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'importLists' } -)(EditImportListModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js index fef70e29f..d7401b95f 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { sizes } from 'Helpers/Props'; import Modal from 'Components/Modal/Modal'; import EditIndexerModalContentConnector from './EditIndexerModalContentConnector'; function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) { return ( diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js index c267994ea..a0c690dad 100644 --- a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -96,7 +96,7 @@ function EditIndexerModalContent(props) { state.settings.advancedSettings, - createProviderSettingsSelector(), + createProviderSettingsSelector('indexers'), (advancedSettings, indexer) => { return { advancedSettings, @@ -85,10 +85,4 @@ EditIndexerModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'indexers' } -)(EditIndexerModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.js b/frontend/src/Settings/Indexers/Indexers/Indexers.js index 8b7d37a84..f5fea9aac 100644 --- a/frontend/src/Settings/Indexers/Indexers/Indexers.js +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.js @@ -59,9 +59,7 @@ class Indexers extends Component { } = this.state; return ( -
+
+
{ isFetching && @@ -42,6 +40,7 @@ function IndexerOptions(props) { type={inputTypes.NUMBER} name="minimumAge" min={0} + unit="minutes" helpText="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider." onChange={onInputChange} {...settings.minimumAge} @@ -55,6 +54,7 @@ function IndexerOptions(props) { type={inputTypes.NUMBER} name="maximumSize" min={0} + unit="MB" helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited." onChange={onInputChange} {...settings.maximumSize} @@ -68,6 +68,7 @@ function IndexerOptions(props) { type={inputTypes.NUMBER} name="retention" min={0} + unit="days" helpText="Usenet only: Set to zero to set for unlimited retention" onChange={onInputChange} {...settings.retention} @@ -84,6 +85,7 @@ function IndexerOptions(props) { type={inputTypes.NUMBER} name="rssSyncInterval" min={0} + unit="minutes" helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)" helpTextWarning="This will apply to all indexers, please follow the rules set forth by them" helpLink="https://github.com/Lidarr/Lidarr/wiki/RSS-Sync" diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js index 20e86f8e4..28d07c77e 100644 --- a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js +++ b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js @@ -1,16 +1,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import { fetchIndexerOptions, setIndexerOptionsValue, saveIndexerOptions } from 'Store/Actions/settingsActions'; import { clearPendingChanges } from 'Store/Actions/baseActions'; -import connectSection from 'Store/connectSection'; import IndexerOptions from './IndexerOptions'; +const SECTION = 'indexerOptions'; + function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), (advancedSettings, sectionSettings) => { return { advancedSettings, @@ -62,7 +64,7 @@ class IndexerOptionsConnector extends Component { } componentWillUnmount() { - this.props.dispatchClearPendingChanges({ section: this.props.section }); + this.props.dispatchClearPendingChanges({ section: SECTION }); } // @@ -86,7 +88,6 @@ class IndexerOptionsConnector extends Component { } IndexerOptionsConnector.propTypes = { - section: PropTypes.string.isRequired, isSaving: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired, dispatchFetchIndexerOptions: PropTypes.func.isRequired, @@ -97,10 +98,4 @@ IndexerOptionsConnector.propTypes = { onChildStateChange: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'settings.indexerOptions' } -)(IndexerOptionsConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js index e9f42df98..31c36ea6a 100644 --- a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { sizes } from 'Helpers/Props'; import Modal from 'Components/Modal/Modal'; import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector'; function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) { return ( diff --git a/frontend/src/Settings/Indexers/Restrictions/Restrictions.js b/frontend/src/Settings/Indexers/Restrictions/Restrictions.js index 411b95ea8..3c9493b39 100644 --- a/frontend/src/Settings/Indexers/Restrictions/Restrictions.js +++ b/frontend/src/Settings/Indexers/Restrictions/Restrictions.js @@ -45,9 +45,7 @@ class Restrictions extends Component { } = this.props; return ( -
+
+
state.settings.advancedSettings, (state) => state.settings.naming, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), createSystemStatusSelector(), (advancedSettings, namingSettings, sectionSettings, systemStatus) => { return { @@ -44,7 +46,7 @@ class MediaManagementConnector extends Component { } componentWillUnmount() { - this.props.clearPendingChanges({ section: this.props.section }); + this.props.clearPendingChanges({ section: SECTION }); } // @@ -74,7 +76,6 @@ class MediaManagementConnector extends Component { } MediaManagementConnector.propTypes = { - section: PropTypes.string.isRequired, fetchMediaManagementSettings: PropTypes.func.isRequired, setMediaManagementSettingsValue: PropTypes.func.isRequired, saveMediaManagementSettings: PropTypes.func.isRequired, @@ -82,10 +83,4 @@ MediaManagementConnector.propTypes = { clearPendingChanges: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'settings.mediaManagement' } -)(MediaManagementConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(MediaManagementConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js index 1bb2e3c0c..e207f5361 100644 --- a/frontend/src/Settings/MediaManagement/Naming/Naming.js +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -113,9 +113,7 @@ class Naming extends Component { } return ( -
+
{ isFetching && diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js index f66c3b5cc..d8210317e 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js @@ -1,18 +1,20 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import { fetchNamingSettings, setNamingSettingsValue, fetchNamingExamples } from 'Store/Actions/settingsActions'; import { clearPendingChanges } from 'Store/Actions/baseActions'; -import connectSection from 'Store/connectSection'; import Naming from './Naming'; +const SECTION = 'naming'; + function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, (state) => state.settings.namingExamples, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), (advancedSettings, examples, sectionSettings) => { return { advancedSettings, @@ -48,7 +50,7 @@ class NamingConnector extends Component { } componentWillUnmount() { - this.props.clearPendingChanges({ section: this.props.section }); + this.props.clearPendingChanges({ section: SECTION }); } // @@ -86,17 +88,10 @@ class NamingConnector extends Component { } NamingConnector.propTypes = { - section: PropTypes.string.isRequired, fetchNamingSettings: PropTypes.func.isRequired, setNamingSettingsValue: PropTypes.func.isRequired, fetchNamingExamples: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'settings.naming' } -)(NamingConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(NamingConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js index 21dcce3a5..c080e12b9 100644 --- a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -163,8 +163,8 @@ class NamingModal extends Component { ]; const qualityTokens = [ - { token: '{Quality Full}', example: 'HDTV 720p Proper' }, - { token: '{Quality Title}', example: 'HDTV 720p' } + { token: '{Quality Full}', example: 'FLAC Proper' }, + { token: '{Quality Title}', example: 'FLAC' } ]; const mediaInfoTokens = [ diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js index 98631932a..24c0237cd 100644 --- a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { sizes } from 'Helpers/Props'; import Modal from 'Components/Modal/Modal'; import EditMetadataModalContentConnector from './EditMetadataModalContentConnector'; function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) { return ( diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.js b/frontend/src/Settings/Metadata/Metadata/Metadatas.js index 9f015369d..faf7e9613 100644 --- a/frontend/src/Settings/Metadata/Metadata/Metadatas.js +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.js @@ -13,9 +13,7 @@ function Metadatas(props) { } = props; return ( -
+
{ advancedSettings && -
+
state.settings.advancedSettings, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), (advancedSettings, sectionSettings) => { return { advancedSettings, @@ -43,7 +45,7 @@ class MetadataProviderConnector extends Component { } componentWillUnmount() { - this.props.clearPendingChanges({ section: this.props.section }); + this.props.clearPendingChanges({ section: SECTION }); } // @@ -74,7 +76,6 @@ class MetadataProviderConnector extends Component { } MetadataProviderConnector.propTypes = { - section: PropTypes.string.isRequired, hasPendingChanges: PropTypes.bool.isRequired, setMetadataProviderValue: PropTypes.func.isRequired, saveMetadataProvider: PropTypes.func.isRequired, @@ -83,10 +84,4 @@ MetadataProviderConnector.propTypes = { onHasPendingChange: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - { withRef: true }, - { section: 'settings.metadataProvider' } -)(MetadataProviderConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(MetadataProviderConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js index 91d9f67cc..27e41d062 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js @@ -1,11 +1,13 @@ import PropTypes from 'prop-types'; import React from 'react'; +import { sizes } from 'Helpers/Props'; import Modal from 'Components/Modal/Modal'; import EditNotificationModalContentConnector from './EditNotificationModalContentConnector'; function EditNotificationModal({ isOpen, onModalClose, ...otherProps }) { return ( diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js index bca296315..104f1897a 100644 --- a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js @@ -1,15 +1,15 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import { setNotificationValue, setNotificationFieldValue, saveNotification, testNotification } from 'Store/Actions/settingsActions'; -import connectSection from 'Store/connectSection'; import EditNotificationModalContent from './EditNotificationModalContent'; function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - createProviderSettingsSelector(), + createProviderSettingsSelector('notifications'), (advancedSettings, notification) => { return { advancedSettings, @@ -85,10 +85,4 @@ EditNotificationModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'notifications' } -)(EditNotificationModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditNotificationModalContentConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/Notifications.js b/frontend/src/Settings/Notifications/Notifications/Notifications.js index 1fad8ef5f..0296c2ed4 100644 --- a/frontend/src/Settings/Notifications/Notifications/Notifications.js +++ b/frontend/src/Settings/Notifications/Notifications/Notifications.js @@ -59,9 +59,7 @@ class Notifications extends Component { } = this.state; return ( -
+
-
+
diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js index 8dbee2837..9db7c58c5 100644 --- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -85,6 +85,7 @@ function EditDelayProfileModalContent(props) { diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js index c513a7fe9..3cad2bb3c 100644 --- a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js @@ -1,16 +1,16 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import { fetchLanguageProfileSchema, setLanguageProfileValue, saveLanguageProfile } from 'Store/Actions/settingsActions'; -import connectSection from 'Store/connectSection'; import EditLanguageProfileModalContent from './EditLanguageProfileModalContent'; function createLanguagesSelector() { return createSelector( - createProviderSettingsSelector(), + createProviderSettingsSelector('languageProfiles'), (languageProfile) => { const languages = languageProfile.item.languages; if (!languages || !languages.value) { @@ -33,7 +33,7 @@ function createLanguagesSelector() { function createMapStateToProps() { return createSelector( - createProviderSettingsSelector(), + createProviderSettingsSelector('languageProfiles'), createLanguagesSelector(), createProfileInUseSelector('languageProfileId'), (languageProfile, languages, isInUse) => { @@ -186,10 +186,4 @@ EditLanguageProfileModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'languageProfiles' } -)(EditLanguageProfileModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditLanguageProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfiles.js b/frontend/src/Settings/Profiles/Language/LanguageProfiles.js index 9db96db44..30dddbcf6 100644 --- a/frontend/src/Settings/Profiles/Language/LanguageProfiles.js +++ b/frontend/src/Settings/Profiles/Language/LanguageProfiles.js @@ -51,9 +51,7 @@ class LanguageProfiles extends Component { } = this.props; return ( -
+
diff --git a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js index 8506a2b8a..d7966aaa7 100644 --- a/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Metadata/EditMetadataProfileModalContentConnector.js @@ -1,16 +1,16 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import { fetchMetadataProfileSchema, setMetadataProfileValue, saveMetadataProfile } from 'Store/Actions/settingsActions'; -import connectSection from 'Store/connectSection'; import EditMetadataProfileModalContent from './EditMetadataProfileModalContent'; function createPrimaryAlbumTypesSelector() { return createSelector( - createProviderSettingsSelector(), + createProviderSettingsSelector('metadataProfiles'), (metadataProfile) => { const primaryAlbumTypes = metadataProfile.item.primaryAlbumTypes; if (!primaryAlbumTypes || !primaryAlbumTypes.value) { @@ -79,7 +79,7 @@ function createReleaseStatusesSelector() { function createMapStateToProps() { return createSelector( - createProviderSettingsSelector(), + createProviderSettingsSelector('metadataProfiles'), createPrimaryAlbumTypesSelector(), createSecondaryAlbumTypesSelector(), createReleaseStatusesSelector(), @@ -210,10 +210,4 @@ EditMetadataProfileModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'metadataProfiles' } -)(EditMetadataProfileModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js index 260d2a3e0..feccfb77f 100644 --- a/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js +++ b/frontend/src/Settings/Profiles/Metadata/MetadataProfiles.js @@ -51,9 +51,7 @@ class MetadataProfiles extends Component { } = this.props; return ( -
+
- + Name @@ -158,7 +158,7 @@ class EditQualityProfileModalContent extends Component { - + Cutoff diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js index 010b6a9d0..2decf2198 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js @@ -1,11 +1,11 @@ import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile } from 'Store/Actions/settingsActions'; -import connectSection from 'Store/connectSection'; import EditQualityProfileModalContent from './EditQualityProfileModalContent'; function getQualityItemGroupId(qualityProfile) { @@ -33,7 +33,7 @@ function parseIndex(index) { function createQualitiesSelector() { return createSelector( - createProviderSettingsSelector(), + createProviderSettingsSelector('qualityProfiles'), (qualityProfile) => { const items = qualityProfile.item.items; if (!items || !items.value) { @@ -63,7 +63,7 @@ function createQualitiesSelector() { function createMapStateToProps() { return createSelector( - createProviderSettingsSelector(), + createProviderSettingsSelector('qualityProfiles'), createQualitiesSelector(), createProfileInUseSelector('qualityProfileId'), (qualityProfile, qualities, isInUse) => { @@ -439,10 +439,4 @@ EditQualityProfileModalContentConnector.propTypes = { onModalClose: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'qualityProfiles' } -)(EditQualityProfileModalContentConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(EditQualityProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js index a2ad1f75a..2e9f123c4 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js @@ -51,9 +51,7 @@ class QualityProfiles extends Component { } = this.props; return ( -
+
+
+ + Tags + + +
+ Manage artist, profile, restriction, and notification tags +
+ + + + ); +} + +TagDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default TagDetailsModal; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.css b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.css new file mode 100644 index 000000000..3488b0509 --- /dev/null +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.css @@ -0,0 +1,26 @@ +.items { + display: flex; + flex-wrap: wrap; +} + +.item { + flex: 0 0 100%; +} + +.restriction { + margin-bottom: 5px; + padding-bottom: 5px; + border-bottom: 1px solid $borderColor; + + &:last-child { + margin: 0; + padding: 0; + border-bottom: none; + } +} + +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js new file mode 100644 index 000000000..ea6fe8a5e --- /dev/null +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContent.js @@ -0,0 +1,163 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import split from 'Utilities/String/split'; +import { kinds } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Button from 'Components/Link/Button'; +import Label from 'Components/Label'; +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 styles from './TagDetailsModalContent.css'; + +function TagDetailsModalContent(props) { + const { + label, + isTagUsed, + artist, + delayProfiles, + notifications, + restrictions, + onModalClose, + onDeleteTagPress + } = props; + + return ( + + + Tag Details - {label} + + + + { + !isTagUsed && +
Tag is not used and can be deleted
+ } + + { + !!artist.length && +
+ { + artist.map((item) => { + return ( +
+ {item.artistName} +
+ ); + }) + } +
+ } + + { + !!delayProfiles.length && +
+ { + delayProfiles.map((item) => { + return ( +
+ {item.name} +
+ ); + }) + } +
+ } + + { + !!notifications.length && +
+ { + notifications.map((item) => { + return ( +
+ {item.name} +
+ ); + }) + } +
+ } + + { + !!restrictions.length && +
+ { + restrictions.map((item) => { + return ( +
+
+ { + split(item.required).map((r) => { + return ( + + ); + }) + } +
+ +
+ { + split(item.ignored).map((i) => { + return ( + + ); + }) + } +
+
+ ); + }) + } +
+ } +
+ + + { + + } + + + +
+ ); +} + +TagDetailsModalContent.propTypes = { + label: PropTypes.string.isRequired, + isTagUsed: PropTypes.bool.isRequired, + artist: PropTypes.arrayOf(PropTypes.object).isRequired, + delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, + notifications: PropTypes.arrayOf(PropTypes.object).isRequired, + restrictions: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteTagPress: PropTypes.func.isRequired +}; + +export default TagDetailsModalContent; diff --git a/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js new file mode 100644 index 000000000..ead36b433 --- /dev/null +++ b/frontend/src/Settings/Tags/Details/TagDetailsModalContentConnector.js @@ -0,0 +1,61 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector'; +import TagDetailsModalContent from './TagDetailsModalContent'; + +function findMatchingItems(ids, items) { + return items.filter((s) => { + return ids.includes(s.id); + }); +} + +function createMatchingArtistSelector() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllArtistSelector(), + findMatchingItems + ); +} + +function createMatchingDelayProfilesSelector() { + return createSelector( + (state, { delayProfileIds }) => delayProfileIds, + (state) => state.settings.delayProfiles.items, + findMatchingItems + ); +} + +function createMatchingNotificationsSelector() { + return createSelector( + (state, { notificationIds }) => notificationIds, + (state) => state.settings.notifications.items, + findMatchingItems + ); +} + +function createMatchingRestrictionsSelector() { + return createSelector( + (state, { restrictionIds }) => restrictionIds, + (state) => state.settings.restrictions.items, + findMatchingItems + ); +} + +function createMapStateToProps() { + return createSelector( + createMatchingArtistSelector(), + createMatchingDelayProfilesSelector(), + createMatchingNotificationsSelector(), + createMatchingRestrictionsSelector(), + (artist, delayProfiles, notifications, restrictions) => { + return { + artist, + delayProfiles, + notifications, + restrictions + }; + } + ); +} + +export default connect(createMapStateToProps)(TagDetailsModalContent); diff --git a/frontend/src/Settings/Tags/Tag.css b/frontend/src/Settings/Tags/Tag.css new file mode 100644 index 000000000..ee425e309 --- /dev/null +++ b/frontend/src/Settings/Tags/Tag.css @@ -0,0 +1,11 @@ +.tag { + composes: card from 'Components/Card.css'; + + width: 150px; +} + +.label { + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} diff --git a/frontend/src/Settings/Tags/Tag.js b/frontend/src/Settings/Tags/Tag.js new file mode 100644 index 000000000..ebe5460eb --- /dev/null +++ b/frontend/src/Settings/Tags/Tag.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import TagDetailsModal from './Details/TagDetailsModal'; +import styles from './Tag.css'; + +class Tag extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false, + isDeleteTagModalOpen: false + }; + } + + // + // Listeners + + onShowDetailsPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + onDeleteTagPress = () => { + this.setState({ + isDetailsModalOpen: false, + isDeleteTagModalOpen: true + }); + } + + onDeleteTagModalClose= () => { + this.setState({ isDeleteTagModalOpen: false }); + } + + onConfirmDeleteTag = () => { + this.props.onConfirmDeleteTag({ id: this.props.id }); + } + + // + // Render + + render() { + const { + label, + delayProfileIds, + notificationIds, + restrictionIds, + artistIds + } = this.props; + + const { + isDetailsModalOpen, + isDeleteTagModalOpen + } = this.state; + + const isTagUsed = !!( + delayProfileIds.length || + notificationIds.length || + restrictionIds.length || + artistIds.length + ); + + return ( + +
+ {label} +
+ + { + isTagUsed && +
+ { + !!artistIds.length && +
+ {artistIds.length} artists +
+ } + + { + !!delayProfileIds.length && +
+ {delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'} +
+ } + + { + !!notificationIds.length && +
+ {notificationIds.length} connection{notificationIds.length > 1 && 's'} +
+ } + + { + !!restrictionIds.length && +
+ {restrictionIds.length} restriction{restrictionIds.length > 1 && 's'} +
+ } +
+ } + + { + !isTagUsed && +
+ No links +
+ } + + + + +
+ ); + } +} + +Tag.propTypes = { + id: PropTypes.number.isRequired, + label: PropTypes.string.isRequired, + delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired, + notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, + restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired, + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + onConfirmDeleteTag: PropTypes.func.isRequired +}; + +Tag.defaultProps = { + delayProfileIds: [], + notificationIds: [], + restrictionIds: [], + artistIds: [] +}; + +export default Tag; diff --git a/frontend/src/Settings/Tags/TagConnector.js b/frontend/src/Settings/Tags/TagConnector.js new file mode 100644 index 000000000..50f610153 --- /dev/null +++ b/frontend/src/Settings/Tags/TagConnector.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTagDetailsSelector from 'Store/Selectors/createTagDetailsSelector'; +import { deleteTag } from 'Store/Actions/tagActions'; +import Tag from './Tag'; + +function createMapStateToProps() { + return createSelector( + createTagDetailsSelector(), + (tagDetails) => { + return { + ...tagDetails + }; + } + ); +} + +const mapStateToProps = { + onConfirmDeleteTag: deleteTag +}; + +export default connect(createMapStateToProps, mapStateToProps)(Tag); diff --git a/frontend/src/Settings/Tags/TagSettings.js b/frontend/src/Settings/Tags/TagSettings.js new file mode 100644 index 000000000..56ef92b49 --- /dev/null +++ b/frontend/src/Settings/Tags/TagSettings.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import TagsConnector from './TagsConnector'; + +function TagSettings() { + return ( + + + + + + + + ); +} + +export default TagSettings; diff --git a/frontend/src/Settings/Tags/Tags.css b/frontend/src/Settings/Tags/Tags.css new file mode 100644 index 000000000..5a44f8331 --- /dev/null +++ b/frontend/src/Settings/Tags/Tags.css @@ -0,0 +1,4 @@ +.tags { + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Tags/Tags.js b/frontend/src/Settings/Tags/Tags.js new file mode 100644 index 000000000..8aed3c0a9 --- /dev/null +++ b/frontend/src/Settings/Tags/Tags.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import TagConnector from './TagConnector'; +import Link from 'Components/Link/Link'; +import styles from './Tags.css'; + +function Tags(props) { + const { + items, + ...otherProps + } = props; + + if (!items.length) { + return ( +
No tags have been added yet. Add tags to link artists with delay profiles, restrictions, or notifications. Click here to find out more about tags in Lidarr.
+ ); + } + + return ( +
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+
+
+ ); +} + +Tags.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Tags; diff --git a/frontend/src/Settings/Tags/TagsConnector.js b/frontend/src/Settings/Tags/TagsConnector.js new file mode 100644 index 000000000..dccf9b43d --- /dev/null +++ b/frontend/src/Settings/Tags/TagsConnector.js @@ -0,0 +1,72 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchTagDetails } from 'Store/Actions/tagActions'; +import { fetchDelayProfiles, fetchNotifications, fetchRestrictions } from 'Store/Actions/settingsActions'; +import Tags from './Tags'; + +function createMapStateToProps() { + return createSelector( + (state) => state.tags, + (tags) => { + const isFetching = tags.isFetching || tags.details.isFetching; + const error = tags.error || tags.details.error; + const isPopulated = tags.isPopulated && tags.details.isPopulated; + + return { + ...tags, + isFetching, + error, + isPopulated + }; + } + ); +} + +const mapDispatchToProps = { + dispatchFetchTagDetails: fetchTagDetails, + dispatchFetchDelayProfiles: fetchDelayProfiles, + dispatchFetchNotifications: fetchNotifications, + dispatchFetchRestrictions: fetchRestrictions +}; + +class MetadatasConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + dispatchFetchTagDetails, + dispatchFetchDelayProfiles, + dispatchFetchNotifications, + dispatchFetchRestrictions + } = this.props; + + dispatchFetchTagDetails(); + dispatchFetchDelayProfiles(); + dispatchFetchNotifications(); + dispatchFetchRestrictions(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MetadatasConnector.propTypes = { + dispatchFetchTagDetails: PropTypes.func.isRequired, + dispatchFetchDelayProfiles: PropTypes.func.isRequired, + dispatchFetchNotifications: PropTypes.func.isRequired, + dispatchFetchRestrictions: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); diff --git a/frontend/src/Settings/UI/UISettings.js b/frontend/src/Settings/UI/UISettings.js index c443e4819..90dc2f97a 100644 --- a/frontend/src/Settings/UI/UISettings.js +++ b/frontend/src/Settings/UI/UISettings.js @@ -82,9 +82,7 @@ class UISettings extends Component { id="uiSettings" {...otherProps} > -
+
First Day of Week diff --git a/frontend/src/Settings/UI/UISettingsConnector.js b/frontend/src/Settings/UI/UISettingsConnector.js index c5ca4a051..24b55b6f0 100644 --- a/frontend/src/Settings/UI/UISettingsConnector.js +++ b/frontend/src/Settings/UI/UISettingsConnector.js @@ -1,16 +1,18 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import { setUISettingsValue, saveUISettings, fetchUISettings } from 'Store/Actions/settingsActions'; import { clearPendingChanges } from 'Store/Actions/baseActions'; -import connectSection from 'Store/connectSection'; import UISettings from './UISettings'; +const SECTION = 'ui'; + function createMapStateToProps() { return createSelector( (state) => state.settings.advancedSettings, - createSettingsSectionSelector(), + createSettingsSectionSelector(SECTION), (advancedSettings, sectionSettings) => { return { advancedSettings, @@ -37,7 +39,7 @@ class UISettingsConnector extends Component { } componentWillUnmount() { - this.props.clearPendingChanges({ section: this.props.section }); + this.props.clearPendingChanges({ section: SECTION }); } // @@ -66,17 +68,10 @@ class UISettingsConnector extends Component { } UISettingsConnector.propTypes = { - section: PropTypes.string.isRequired, setUISettingsValue: PropTypes.func.isRequired, saveUISettings: PropTypes.func.isRequired, fetchUISettings: PropTypes.func.isRequired, clearPendingChanges: PropTypes.func.isRequired }; -export default connectSection( - createMapStateToProps, - mapDispatchToProps, - undefined, - undefined, - { section: 'settings.ui' } -)(UISettingsConnector); +export default connect(createMapStateToProps, mapDispatchToProps)(UISettingsConnector); diff --git a/frontend/src/Store/Actions/Creators/Reducers/createSetSettingValueReducer.js b/frontend/src/Store/Actions/Creators/Reducers/createSetSettingValueReducer.js index 33ac23044..474eb7bb2 100644 --- a/frontend/src/Store/Actions/Creators/Reducers/createSetSettingValueReducer.js +++ b/frontend/src/Store/Actions/Creators/Reducers/createSetSettingValueReducer.js @@ -14,7 +14,7 @@ function createSetSettingValueReducer(section) { let parsedValue = null; - if (_.isNumber(currentValue)) { + if (_.isNumber(currentValue) && value != null) { parsedValue = parseInt(value); } else { parsedValue = value; diff --git a/frontend/src/Store/Actions/albumStudioActions.js b/frontend/src/Store/Actions/albumStudioActions.js index 45dd2dbdb..9221e8ba5 100644 --- a/frontend/src/Store/Actions/albumStudioActions.js +++ b/frontend/src/Store/Actions/albumStudioActions.js @@ -9,6 +9,7 @@ import createSetClientSideCollectionFilterReducer from './Creators/Reducers/crea import createHandleActions from './Creators/createHandleActions'; import { set } from './baseActions'; import { fetchAlbums } from './albumActions'; +import { fetchArtist, filters, filterPredicates } from './artistActions'; // // Variables @@ -26,9 +27,9 @@ export const defaultState = { secondarySortKey: 'sortName', secondarySortDirection: sortDirections.ASCENDING, selectedFilterKey: 'all', - // filters come from artistActions + filters, + filterPredicates, customFilters: [] - // filterPredicates come from artistActions }; export const persistState = [ diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index d8087242a..d96fb6370 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -2,7 +2,8 @@ import _ from 'lodash'; import $ from 'jquery'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; -import { filterTypes, sortDirections } from 'Helpers/Props'; +import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate'; +import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer'; import createFetchHandler from './Creators/createFetchHandler'; @@ -16,6 +17,95 @@ import { updateItem } from './baseActions'; export const section = 'artist'; +export const filters = [ + { + key: 'all', + label: 'All', + filters: [] + }, + { + key: 'monitored', + label: 'Monitored Only', + filters: [ + { + key: 'monitored', + value: true, + type: filterTypes.EQUAL + } + ] + }, + { + key: 'unmonitored', + label: 'Unmonitored Only', + filters: [ + { + key: 'monitored', + value: false, + type: filterTypes.EQUAL + } + ] + }, + { + key: 'continuing', + label: 'Continuing Only', + filters: [ + { + key: 'status', + value: 'continuing', + type: filterTypes.EQUAL + } + ] + }, + { + key: 'ended', + label: 'Ended Only', + filters: [ + { + key: 'status', + value: 'ended', + type: filterTypes.EQUAL + } + ] + }, + { + key: 'missing', + label: 'Missing Tracks', + filters: [ + { + key: 'missing', + value: true, + type: filterTypes.EQUAL + } + ] + } +]; + +export const filterPredicates = { + missing: function(item) { + const { statistics = {} } = item; + + return statistics.trackCount - statistics.trackFileCount > 0; + }, + + nextAlbum: function(item, filterValue, type) { + return dateFilterPredicate(item.nextAlbum, filterValue, type); + }, + + lastAlbum: function(item, filterValue, type) { + return dateFilterPredicate(item.lastAlbum, filterValue, type); + }, + + added: function(item, filterValue, type) { + return dateFilterPredicate(item.added, filterValue, type); + }, + + ratings: function(item, filterValue, type) { + const predicate = filterTypePredicates[type]; + + return predicate(item.ratings.value * 10, filterValue); + } +}; + // // State @@ -28,75 +118,6 @@ export const defaultState = { items: [], sortKey: 'sortName', sortDirection: sortDirections.ASCENDING, - filters: [ - { - key: 'all', - label: 'All', - filters: [] - }, - { - key: 'monitored', - label: 'Monitored Only', - filters: [ - { - key: 'monitored', - value: true, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'unmonitored', - label: 'Unmonitored Only', - filters: [ - { - key: 'monitored', - value: false, - type: filterTypes.EQUAL - } - ] - }, - { - key: 'continuing', - label: 'Continuing Only', - filters: [ - { - key: 'status', - value: 'continuing', - type: filterTypes.EQUAL - } - ] - }, - { - key: 'ended', - label: 'Ended Only', - filters: [ - { - key: 'status', - value: 'ended', - type: filterTypes.EQUAL - } - ] - }, - { - key: 'missing', - label: 'Missing Albums', - filters: [ - { - key: 'missing', - value: true, - type: filterTypes.EQUAL - } - ] - } - ], - - filterPredicates: { - missing: function(item) { - return item.statistics.trackCount - item.statistics.trackFileCount > 0; - } - }, - pendingChanges: {} }; diff --git a/frontend/src/Store/Actions/artistEditorActions.js b/frontend/src/Store/Actions/artistEditorActions.js index 694947a97..af786b1b4 100644 --- a/frontend/src/Store/Actions/artistEditorActions.js +++ b/frontend/src/Store/Actions/artistEditorActions.js @@ -1,12 +1,15 @@ import $ from 'jquery'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; -import { sortDirections } from 'Helpers/Props'; +import customFilterHandlers from 'Utilities/customFilterHandlers'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers'; import createHandleActions from './Creators/createHandleActions'; import { set, updateItem } from './baseActions'; +import { filters, filterPredicates } from './artistActions'; // // Variables @@ -26,9 +29,58 @@ export const defaultState = { secondarySortKey: 'sortName', secondarySortDirection: sortDirections.ASCENDING, selectedFilterKey: 'all', - // filters come from artistActions + filters, + filterPredicates, + + filterBuilderProps: [ + { + name: 'monitored', + label: 'Monitored', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'status', + label: 'Status', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.ARTIST_STATUS + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.QUALITY_PROFILE + }, + { + name: 'languageProfileId', + label: 'Language Profile', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.LANGUAGE_PROFILE + }, + { + name: 'metadataProfileId', + label: 'Metadata Profile', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.METADATA_PROFILE + }, + { + name: 'path', + label: 'Path', + type: filterBuilderTypes.STRING + }, + { + name: 'rootFolderPath', + label: 'Root Folder Path', + type: filterBuilderTypes.EXACT + }, + { + name: 'tags', + label: 'Tags', + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.TAG + } + ], customFilters: [] - // filterPredicates come from artistActions }; export const persistState = [ @@ -45,6 +97,8 @@ export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort'; export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter'; export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor'; export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist'; +export const REMOVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/removeArtistEditorCustomFilter'; +export const SAVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/saveArtistEditorCustomFilter'; // // Action Creators @@ -53,6 +107,8 @@ export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT); export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER); export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR); export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST); +export const removeArtistEditorCustomFilter = createAction(REMOVE_ARTIST_EDITOR_CUSTOM_FILTER); +export const saveArtistEditorCustomFilter = createAction(SAVE_ARTIST_EDITOR_CUSTOM_FILTER); // // Action Handlers @@ -137,6 +193,11 @@ export const actionHandlers = handleThunks({ export const reducers = createHandleActions({ [SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section), - [SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section) + [SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section), + + ...createCustomFilterReducers(section, { + [customFilterHandlers.REMOVE]: REMOVE_ARTIST_EDITOR_CUSTOM_FILTER, + [customFilterHandlers.SAVE]: SAVE_ARTIST_EDITOR_CUSTOM_FILTER + }) }, defaultState, section); diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js index 0021d85e5..9c081f3ed 100644 --- a/frontend/src/Store/Actions/artistIndexActions.js +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -1,10 +1,14 @@ import moment from 'moment'; import { createAction } from 'redux-actions'; -import { sortDirections } from 'Helpers/Props'; +import customFilterHandlers from 'Utilities/customFilterHandlers'; +import sortByName from 'Utilities/Array/sortByName'; +import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; +import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers'; import createHandleActions from './Creators/createHandleActions'; +import { filters, filterPredicates } from './artistActions'; // // Variables @@ -137,6 +141,18 @@ export const defaultState = { isSortable: true, isVisible: false }, + { + name: 'genres', + label: 'Genres', + isSortable: false, + isVisible: false + }, + { + name: 'ratings', + label: 'Rating', + isSortable: true, + isVisible: false + }, { name: 'tags', label: 'Tags', @@ -153,10 +169,12 @@ export const defaultState = { sortPredicates: { trackProgress: function(item) { + const { statistics = {} } = item; + const { trackCount = 0, trackFileCount - } = item.statistics; + } = statistics; const progress = trackCount ? trackFileCount / trackCount * 100 : 100; @@ -178,22 +196,136 @@ export const defaultState = { }, albumCount: function(item) { - return item.statistics.albumCount; + const { statistics = {} } = item; + + return statistics.albumCount; }, trackCount: function(item) { - return item.statistics.totalTrackCount; + const { statistics = {} } = item; + + return statistics.totalTrackCount; }, sizeOnDisk: function(item) { - return item.statistics.sizeOnDisk; + const { statistics = {} } = item; + + return statistics.sizeOnDisk; + }, + + ratings: function(item) { + const { ratings = {} } = item; + + return ratings.value; } }, selectedFilterKey: 'all', - // filters come from artistActions + + filters, + filterPredicates, + + filterBuilderProps: [ + { + name: 'monitored', + label: 'Monitored', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.BOOL + }, + { + name: 'status', + label: 'Status', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.ARTIST_STATUS + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.QUALITY_PROFILE + }, + { + name: 'languageProfileId', + label: 'Language Profile', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.LANGUAGE_PROFILE + }, + { + name: 'metadataProfileId', + label: 'Metadata Profile', + type: filterBuilderTypes.EXACT, + valueType: filterBuilderValueTypes.METADATA_PROFILE + }, + { + name: 'nextAlbum', + label: 'Next Album', + type: filterBuilderTypes.DATE, + valueType: filterBuilderValueTypes.DATE + }, + { + name: 'lastAlbum', + label: 'Last Album', + type: filterBuilderTypes.DATE, + valueType: filterBuilderValueTypes.DATE + }, + { + name: 'added', + label: 'Added', + type: filterBuilderTypes.DATE, + valueType: filterBuilderValueTypes.DATE + }, + { + name: 'albumCount', + label: 'Album Count', + type: filterBuilderTypes.NUMBER + }, + { + name: 'trackProgress', + label: 'Track Progress', + type: filterBuilderTypes.NUMBER + }, + { + name: 'path', + label: 'Path', + type: filterBuilderTypes.STRING + }, + { + name: 'sizeOnDisk', + label: 'Size on Disk', + type: filterBuilderTypes.NUMBER + }, + { + name: 'genres', + label: 'Genres', + type: filterBuilderTypes.ARRAY, + optionsSelector: function(items) { + const tagList = items.reduce((acc, artist) => { + artist.genres.forEach((genre) => { + acc.push({ + id: genre, + name: genre + }); + }); + + return acc; + }, []); + + return tagList.sort(sortByName); + } + }, + { + name: 'ratings', + label: 'Rating', + type: filterBuilderTypes.NUMBER + }, + { + name: 'tags', + label: 'Tags', + type: filterBuilderTypes.ARRAY, + valueType: filterBuilderValueTypes.TAG + } + ], customFilters: [] - // filterPredicates come from artistActions }; export const persistState = [ @@ -218,6 +350,8 @@ export const SET_ARTIST_TABLE_OPTION = 'artistIndex/setArtistTableOption'; export const SET_ARTIST_POSTER_OPTION = 'artistIndex/setArtistPosterOption'; export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption'; export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption'; +export const REMOVE_ARTIST_CUSTOM_FILTER = 'artistIndex/removeArtistCustomFilter'; +export const SAVE_ARTIST_CUSTOM_FILTER = 'artistIndex/saveArtistCustomFilter'; // // Action Creators @@ -229,7 +363,8 @@ export const setArtistTableOption = createAction(SET_ARTIST_TABLE_OPTION); export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION); export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION); export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_OPTION); - +export const removeArtistCustomFilter = createAction(REMOVE_ARTIST_CUSTOM_FILTER); +export const saveArtistCustomFilter = createAction(SAVE_ARTIST_CUSTOM_FILTER); // // Reducers @@ -278,6 +413,11 @@ export const reducers = createHandleActions({ ...payload } }; - } + }, + + ...createCustomFilterReducers(section, { + [customFilterHandlers.REMOVE]: REMOVE_ARTIST_CUSTOM_FILTER, + [customFilterHandlers.SAVE]: SAVE_ARTIST_CUSTOM_FILTER + }) }, defaultState, section); diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js index 956fc8675..f6dbcfe55 100644 --- a/frontend/src/Store/Actions/blacklistActions.js +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks'; import { sortDirections } from 'Helpers/Props'; import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; import createHandleActions from './Creators/createHandleActions'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; // @@ -59,8 +60,8 @@ export const defaultState = { isVisible: false }, { - name: 'details', - columnLabel: 'Details', + name: 'actions', + columnLabel: 'Actions', isVisible: true, isModifiable: false } @@ -85,6 +86,7 @@ export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage'; export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage'; export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort'; export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption'; +export const REMOVE_FROM_BLACKLIST = 'blacklist/removeFromBlacklist'; // // Action Creators @@ -97,6 +99,7 @@ export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE); export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE); export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT); export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION); +export const removeFromBlacklist = createThunk(REMOVE_FROM_BLACKLIST); // // Action Handlers @@ -114,7 +117,9 @@ export const actionHandlers = handleThunks({ [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLACKLIST_PAGE, [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLACKLIST_PAGE, [serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT - }) + }), + + [REMOVE_FROM_BLACKLIST]: createRemoveItemHandler(section, '/blacklist') }); // diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index 6bbf679e8..3bbd901b3 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -287,7 +287,7 @@ export const actionHandlers = handleThunks({ [SET_CALENDAR_VIEW]: function(getState, payload, dispatch) { const state = getState(); const view = payload.view; - const time = view === calendarViews.FORECAST ? + const time = view === calendarViews.FORECAST || calendarViews.AGENDA ? moment() : state.calendar.time; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js index 30f376876..b0cf0b3a4 100644 --- a/frontend/src/Store/Actions/interactiveImportActions.js +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import $ from 'jquery'; import moment from 'moment'; import { createAction } from 'redux-actions'; @@ -44,7 +43,7 @@ export const defaultState = { }, quality: function(item, direction) { - return item.quality.qualityWeight; + return item.quality ? item.quality.qualityWeight : 0; } }, @@ -144,7 +143,7 @@ export const reducers = createHandleActions({ const id = payload.id; const newState = Object.assign({}, state); const items = newState.items; - const index = _.findIndex(items, { id }); + const index = items.findIndex((item) => item.id === id); const item = Object.assign({}, items[index], payload); newState.items = [...items]; @@ -157,7 +156,7 @@ export const reducers = createHandleActions({ const folder = payload.folder; const recentFolder = { folder, lastUsed: moment().toISOString() }; const recentFolders = [...state.recentFolders]; - const index = _.findIndex(recentFolders, { folder }); + const index = recentFolders.findIndex((r) => r.folder === folder); if (index > -1) { recentFolders.splice(index, 1, recentFolder); @@ -170,7 +169,10 @@ export const reducers = createHandleActions({ [REMOVE_RECENT_FOLDER]: function(state, { payload }) { const folder = payload.folder; - const recentFolders = _.remove([...state.recentFolders], { folder }); + const recentFolders = [...state.recentFolders]; + const index = recentFolders.findIndex((r) => r.folder === folder); + + recentFolders.splice(index, 1); return Object.assign({}, state, { recentFolders }); }, diff --git a/frontend/src/Store/Actions/oAuthActions.js b/frontend/src/Store/Actions/oAuthActions.js index 39a177cd8..d0b3bb930 100644 --- a/frontend/src/Store/Actions/oAuthActions.js +++ b/frontend/src/Store/Actions/oAuthActions.js @@ -16,8 +16,8 @@ export const section = 'oAuth'; export const defaultState = { authorizing: false, - accessToken: null, - accessTokenSecret: null + result: null, + error: null }; // @@ -50,9 +50,11 @@ function showOAuthWindow(url) { const splitQuery = query.substring(1).split('&'); splitQuery.forEach((param) => { - const paramSplit = param.split('='); + if (param) { + const paramSplit = param.split('='); - queryParams[paramSplit[0]] = paramSplit[1]; + queryParams[paramSplit[0]] = paramSplit[1]; + } }); onComplete(); @@ -70,7 +72,7 @@ export const actionHandlers = handleThunks({ [START_OAUTH]: function(getState, payload, dispatch) { const actionPayload = { action: 'startOAuth', - queryParams: { callbackUrl: `${window.location.origin}/oauth.html` }, + queryParams: { callbackUrl: `${window.location.origin}${window.Lidarr.urlBase}/oauth.html` }, ...payload }; @@ -78,33 +80,36 @@ export const actionHandlers = handleThunks({ authorizing: true })); + let startResponse = {}; + const promise = requestAction(actionPayload) .then((response) => { + startResponse = response; return showOAuthWindow(response.oauthUrl); }) .then((queryParams) => { return requestAction({ action: 'getOAuthToken', - queryParams, + queryParams: { + ...startResponse, + ...queryParams + }, ...payload }); }) .then((response) => { - const { - accessToken, - accessTokenSecret - } = response; - dispatch(setOAuthValue({ authorizing: false, - accessToken, - accessTokenSecret + result: response, + error: null })); }); - promise.fail(() => { + promise.fail((xhr) => { dispatch(setOAuthValue({ - authorizing: false + authorizing: false, + result: null, + error: xhr })); }); } diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 54df00ae5..8102fe6d8 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -78,16 +78,19 @@ export const defaultState = { { name: 'protocol', label: 'Protocol', + isSortable: true, isVisible: false }, { name: 'indexer', label: 'Indexer', + isSortable: true, isVisible: false }, { name: 'downloadClient', label: 'Download Client', + isSortable: true, isVisible: false }, { @@ -269,7 +272,7 @@ export const actionHandlers = handleThunks({ promise.done((data) => { dispatch(batchActions([ - dispatch(fetchQueue()), + fetchQueue(), ...ids.map((id) => { return updateItem({ diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js index 29c61cf61..b9d217bd3 100644 --- a/frontend/src/Store/Actions/tagActions.js +++ b/frontend/src/Store/Actions/tagActions.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import { createThunk, handleThunks } from 'Store/thunks'; import createFetchHandler from './Creators/createFetchHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; import createHandleActions from './Creators/createHandleActions'; import { update } from './baseActions'; @@ -16,7 +17,14 @@ export const defaultState = { isFetching: false, isPopulated: false, error: null, - items: [] + items: [], + + details: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + } }; // @@ -24,12 +32,16 @@ export const defaultState = { export const FETCH_TAGS = 'tags/fetchTags'; export const ADD_TAG = 'tags/addTag'; +export const DELETE_TAG = 'tags/deleteTag'; +export const FETCH_TAG_DETAILS = 'tags/fetchTagDetails'; // // Action Creators export const fetchTags = createThunk(FETCH_TAGS); export const addTag = createThunk(ADD_TAG); +export const deleteTag = createThunk(DELETE_TAG); +export const fetchTagDetails = createThunk(FETCH_TAG_DETAILS); // // Action Handlers @@ -51,7 +63,11 @@ export const actionHandlers = handleThunks({ dispatch(update({ section, data: tags })); payload.onTagCreated(data); }); - } + }, + + [DELETE_TAG]: createRemoveItemHandler(section, '/tag'), + [FETCH_TAG_DETAILS]: createFetchHandler('tags.details', '/tag/detail') + }); // diff --git a/frontend/src/Store/Middleware/middlewares.js b/frontend/src/Store/Middleware/middlewares.js index 5cf93b2a6..5583c21ef 100644 --- a/frontend/src/Store/Middleware/middlewares.js +++ b/frontend/src/Store/Middleware/middlewares.js @@ -15,7 +15,10 @@ export default function(history) { middlewares.push(routerMiddleware(history)); middlewares.push(thunk); - return compose( + // eslint-disable-next-line no-underscore-dangle + const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + + return composeEnhancers( applyMiddleware(...middlewares), persistState ); diff --git a/frontend/src/Store/Middleware/sentryMiddleware.js b/frontend/src/Store/Middleware/sentryMiddleware.js index a6642cc2f..454e1fd24 100644 --- a/frontend/src/Store/Middleware/sentryMiddleware.js +++ b/frontend/src/Store/Middleware/sentryMiddleware.js @@ -32,17 +32,20 @@ export default function sentryMiddleware() { } const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' : - 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290'; - - Raven.config(dsn).install(); - - return createRavenMiddleware(Raven, { - environment: isProduction ? 'production' : 'development', - release, - tags: { - branch, - version - }, - dataCallback: cleanseData - }); + 'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427'; + + Raven.config( + dsn, + { + environment: isProduction ? 'production' : 'development', + release, + tags: { + branch, + version + }, + dataCallback: cleanseData + } + ).install(); + + return createRavenMiddleware(Raven); } diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js index de85d1801..c00adc159 100644 --- a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -1,37 +1,7 @@ import _ from 'lodash'; import { createSelector } from 'reselect'; import findSelectedFilters from 'Utilities/Filter/findSelectedFilters'; -import { filterTypes, sortDirections } from 'Helpers/Props'; - -const filterTypePredicates = { - [filterTypes.CONTAINS]: function(value, filterValue) { - return value.toLowerCase().contains(filterValue.toLowerCase()); - }, - - [filterTypes.EQUAL]: function(value, filterValue) { - return value === filterValue; - }, - - [filterTypes.GREATER_THAN]: function(value, filterValue) { - return value > filterValue; - }, - - [filterTypes.GREATER_THAN_OR_EQUAL]: function(value, filterValue) { - return value >= filterValue; - }, - - [filterTypes.LESS_THAN]: function(value, filterValue) { - return value < filterValue; - }, - - [filterTypes.LESS_THAN_OR_EQUAL]: function(value, filterValue) { - return value <= filterValue; - }, - - [filterTypes.NOT_EQUAL]: function(value, filterValue) { - return value !== filterValue; - } -}; +import { filterTypePredicates, filterTypes, sortDirections } from 'Helpers/Props'; function getSortClause(sortKey, sortDirection, sortPredicates) { if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) { @@ -124,10 +94,10 @@ function sort(items, state) { return _.orderBy(items, clauses, orders); } -function createClientSideCollectionSelector() { +function createClientSideCollectionSelector(section, uiSection) { return createSelector( - (state, { section }) => _.get(state, section), - (state, { uiSection }) => _.get(state, uiSection), + (state) => _.get(state, section), + (state) => _.get(state, uiSection), (sectionState, uiSectionState = {}) => { const state = Object.assign({}, sectionState, uiSectionState); @@ -137,7 +107,8 @@ function createClientSideCollectionSelector() { return { ...sectionState, ...uiSectionState, - items: sorted + items: sorted, + totalItems: state.items.length }; } ); diff --git a/frontend/src/Store/Selectors/createProviderSettingsSelector.js b/frontend/src/Store/Selectors/createProviderSettingsSelector.js index 0b8805803..bd530bc24 100644 --- a/frontend/src/Store/Selectors/createProviderSettingsSelector.js +++ b/frontend/src/Store/Selectors/createProviderSettingsSelector.js @@ -2,10 +2,10 @@ import _ from 'lodash'; import { createSelector } from 'reselect'; import selectSettings from 'Store/Selectors/selectSettings'; -function createProviderSettingsSelector() { +function createProviderSettingsSelector(sectionName) { return createSelector( (state, { id }) => id, - (state, { section }) => state.settings[section], + (state) => state.settings[sectionName], (id, section) => { if (!id) { const item = _.isArray(section.schema) ? section.selectedSchema : section.schema; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.js b/frontend/src/Store/Selectors/createSettingsSectionSelector.js index 14af65a01..a9f6cbff6 100644 --- a/frontend/src/Store/Selectors/createSettingsSectionSelector.js +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.js @@ -1,10 +1,9 @@ -import _ from 'lodash'; import { createSelector } from 'reselect'; import selectSettings from 'Store/Selectors/selectSettings'; -function createSettingsSectionSelector() { +function createSettingsSectionSelector(section) { return createSelector( - (state, { section }) => _.get(state, section), + (state) => state.settings[section], (sectionSettings) => { const { isFetching, diff --git a/frontend/src/Store/Selectors/createTagDetailsSelector.js b/frontend/src/Store/Selectors/createTagDetailsSelector.js new file mode 100644 index 000000000..dd178944c --- /dev/null +++ b/frontend/src/Store/Selectors/createTagDetailsSelector.js @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; + +function createTagDetailsSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.tags.details.items, + (id, tagDetails) => { + return tagDetails.find((t) => t.id === id); + } + ); +} + +export default createTagDetailsSelector; diff --git a/frontend/src/Store/connectSection.js b/frontend/src/Store/connectSection.js deleted file mode 100644 index 0158cc4de..000000000 --- a/frontend/src/Store/connectSection.js +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint max-params: 0 */ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import getDisplayName from 'Helpers/getDisplayName'; - -function connectSection(mapStateToProps, mapDispatchToProps, mergeProps, options = {}, sectionOptions = {}) { - return function wrap(WrappedComponent) { - const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(WrappedComponent); - - class Section extends Component { - - // - // Control - - getWrappedInstance = () => { - if (this._wrappedInstance) { - return this._wrappedInstance.getWrappedInstance(); - } - } - - // - // Listeners - - setWrappedInstanceRef = (ref) => { - this._wrappedInstance = ref; - } - - // - // Render - - render() { - if (options.withRef) { - return ( - - ); - } - - return ( - - ); - } - } - - Section.displayName = `Section(${getDisplayName(WrappedComponent)})`; - Section.WrappedComponent = WrappedComponent; - - return Section; - }; -} - -export default connectSection; diff --git a/frontend/src/System/Status/About/About.css b/frontend/src/System/Status/About/About.css new file mode 100644 index 000000000..fc20848b0 --- /dev/null +++ b/frontend/src/System/Status/About/About.css @@ -0,0 +1,5 @@ +.descriptionList { + composes: descriptionList from 'Components/DescriptionList/DescriptionList.css'; + + margin-bottom: 10px; +} diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js index 8800276f0..a177913c2 100644 --- a/frontend/src/System/Status/About/About.js +++ b/frontend/src/System/Status/About/About.js @@ -4,6 +4,7 @@ import titleCase from 'Utilities/String/titleCase'; import FieldSet from 'Components/FieldSet'; import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import styles from './About.css'; class About extends Component { @@ -22,10 +23,8 @@ class About extends Component { } = this.props; return ( -
- +
+ +
{ isFetching && diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js index a498d6c0c..2bcac7ce3 100644 --- a/frontend/src/System/Status/MoreInfo/MoreInfo.js +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -12,9 +12,7 @@ class MoreInfo extends Component { render() { return ( -
+
Home page diff --git a/frontend/src/System/Updates/Updates.css b/frontend/src/System/Updates/Updates.css index 307028426..3502f6d1f 100644 --- a/frontend/src/System/Updates/Updates.css +++ b/frontend/src/System/Updates/Updates.css @@ -1,3 +1,7 @@ +.updateAvailable { + display: flex; +} + .upToDate { display: flex; margin-bottom: 20px; @@ -14,6 +18,13 @@ line-height: 30px; } +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin-top: 5px; + margin-left: auto; +} + .update { margin-top: 20px; } diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js index 36a31a3db..d46b8e1a7 100644 --- a/frontend/src/System/Updates/Updates.js +++ b/frontend/src/System/Updates/Updates.js @@ -19,6 +19,7 @@ class Updates extends Component { render() { const { + isFetching, isPopulated, error, items, @@ -36,7 +37,7 @@ class Updates extends Component { { - !isPopulated && + !isPopulated && !error && } @@ -47,14 +48,24 @@ class Updates extends Component { { hasUpdateToInstall && +
Install Latest + + { + isFetching && + + } +
} { @@ -68,6 +79,14 @@ class Updates extends Component {
The latest version of Lidarr is already installed
+ + { + isFetching && + + }
} @@ -126,7 +145,7 @@ class Updates extends Component { { !!error && -
+
Failed to fetch updates
} @@ -138,6 +157,7 @@ class Updates extends Component { } Updates.propTypes = { + isFetching: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired, error: PropTypes.object, items: PropTypes.array.isRequired, diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js index 0ac6cf239..0d0aa491f 100644 --- a/frontend/src/System/Updates/UpdatesConnector.js +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -16,12 +16,14 @@ function createMapStateToProps() { createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), (updates, uiSettings, isInstallingUpdate) => { const { + isFetching, isPopulated, error, items } = updates; return { + isFetching, isPopulated, error, items, diff --git a/frontend/src/Utilities/Date/dateFilterPredicate.js b/frontend/src/Utilities/Date/dateFilterPredicate.js new file mode 100644 index 000000000..2c74f435a --- /dev/null +++ b/frontend/src/Utilities/Date/dateFilterPredicate.js @@ -0,0 +1,33 @@ +import moment from 'moment'; +import isAfter from 'Utilities/Date/isAfter'; +import isBefore from 'Utilities/Date/isBefore'; +import * as filterTypes from 'Helpers/Props/filterTypes'; + +export default function(itemValue, filterValue, type) { + if (!itemValue) { + return false; + } + + switch (type) { + case filterTypes.LESS_THAN: + return moment(itemValue).isBefore(filterValue); + + case filterTypes.GREATER_THAN: + return moment(itemValue).isAfter(filterValue); + + case filterTypes.IN_LAST: + return ( + isAfter(itemValue, { [filterValue.time]: filterValue.value * -1 }) && + isBefore(itemValue) + ); + + case filterTypes.IN_NEXT: + return ( + isAfter(itemValue) && + isBefore(itemValue, { [filterValue.time]: filterValue.value }) + ); + + default: + return false; + } +} diff --git a/frontend/src/Utilities/String/isString.js b/frontend/src/Utilities/String/isString.js new file mode 100644 index 000000000..1e7c3dff8 --- /dev/null +++ b/frontend/src/Utilities/String/isString.js @@ -0,0 +1,3 @@ +export default function isString(possibleString) { + return typeof possibleString === 'string' || possibleString instanceof String; +} diff --git a/frontend/src/Utilities/String/titleCase.js b/frontend/src/Utilities/String/titleCase.js index 531e4df68..5b76c10dd 100644 --- a/frontend/src/Utilities/String/titleCase.js +++ b/frontend/src/Utilities/String/titleCase.js @@ -3,7 +3,7 @@ function titleCase(input) { return ''; } - return input.replace(/\w\S*/g, (match) => { + return input.replace(/\b\w+/g, (match) => { return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase(); }); } diff --git a/frontend/src/login.html b/frontend/src/login.html index 7d71036f1..60505bc23 100644 --- a/frontend/src/login.html +++ b/frontend/src/login.html @@ -200,9 +200,9 @@
-
+ @@ -211,6 +211,12 @@