New: UI Updates, Tag manager, More custom filters (#437)

* New: UI Updates, Tag manager, More custom filters

* fixup! Fix ScanFixture Unit Tests

* Fixed: Sentry Errors from UI don't have release, branch, environment

* Changed: Bump Mobile Detect for New Device Detection

* Fixed: Build on changes to package.json

* fixup! Add MetadataProfile filter option

* fixup! Tag Note, Blacklist, Manual Import

* fixup: Remove connectSection

* fixup: root folder comment
pull/447/head
Qstick 6 years ago committed by GitHub
parent afa78b1d20
commit 6581b3a2c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -56,4 +56,5 @@ only_commits:
- appveyor.yml - appveyor.yml
- build.sh - build.sh
- test.sh - test.sh
- package.json
- appveyor-package.sh - appveyor-package.sh

@ -5,6 +5,7 @@ const path = require('path');
const webpack = require('webpack'); const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler'); const errorHandler = require('./helpers/errorHandler');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ExtractTextPlugin = require('extract-text-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const uiFolder = 'UI'; const uiFolder = 'UI';
const root = path.join(__dirname, '..', 'src'); const root = path.join(__dirname, '..', 'src');
@ -27,19 +28,49 @@ const extractCSSPlugin = new ExtractTextPlugin({
ignoreOrder: true 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 = { const config = {
devtool: '#source-map', devtool: '#source-map',
stats: { stats: {
children: false children: false
}, },
watchOptions: { watchOptions: {
ignored: /node_modules/ ignored: /node_modules/
}, },
entry: { entry: {
preload: 'preload.js', preload: 'preload.js',
vendor: 'vendor.js', vendor: 'vendor.js',
index: 'index.js' index: 'index.js'
}, },
resolve: { resolve: {
modules: [ modules: [
root, root,
@ -50,35 +81,21 @@ const config = {
jquery: 'jquery/src/jquery' jquery: 'jquery/src/jquery'
} }
}, },
output: { output: {
filename: path.join('_output', uiFolder, '[name].js'), filename: path.join('_output', uiFolder, '[name].js'),
sourceMapFilename: '[file].map' sourceMapFilename: '[file].map'
}, },
plugins: [
extractCSSPlugin, plugins,
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env': {
NODE_ENV: isProduction ? JSON.stringify('production') : JSON.stringify('development')
}
})
],
resolveLoader: { resolveLoader: {
modules: [ modules: [
'node_modules', 'node_modules',
'frontend/gulp/webpack/' 'frontend/gulp/webpack/'
] ]
}, },
// TODO: Do we need this loader?
// eslint: {
// formatter: function(results) {
// return JSON.stringify(results);
// }
// },
module: { module: {
rules: [ rules: [
{ {

@ -11,8 +11,8 @@
width: 80px; width: 80px;
} }
.details { .actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css'; composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 30px; width: 70px;
} }

@ -1,6 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
@ -48,7 +48,8 @@ class BlacklistRow extends Component {
protocol, protocol,
indexer, indexer,
message, message,
columns columns,
onRemovePress
} = this.props; } = this.props;
return ( return (
@ -129,16 +130,21 @@ class BlacklistRow extends Component {
); );
} }
if (name === 'details') { if (name === 'actions') {
return ( return (
<TableRowCell <TableRowCell
key={name} key={name}
className={styles.details} className={styles.actions}
> >
<IconButton <IconButton
name={icons.INFO} name={icons.INFO}
onPress={this.onDetailsPress} onPress={this.onDetailsPress}
/> />
<IconButton
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
/>
</TableRowCell> </TableRowCell>
); );
} }
@ -171,7 +177,8 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
columns: PropTypes.arrayOf(PropTypes.object).isRequired columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onRemovePress: PropTypes.func.isRequired
}; };
export default BlacklistRow; export default BlacklistRow;

@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import BlacklistRow from './BlacklistRow'; import BlacklistRow from './BlacklistRow';
function createMapStateToProps() { 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);

@ -106,9 +106,7 @@ class ImportArtistSelectFolder extends Component {
{ {
items.length > 0 ? items.length > 0 ?
<div className={styles.recentFolders}> <div className={styles.recentFolders}>
<FieldSet <FieldSet legend="Recent Folders">
legend="Recent Folders"
>
<Table <Table
columns={rootFolderColumns} columns={rootFolderColumns}
> >

@ -1,3 +1,9 @@
.descriptionList {
composes: descriptionList from 'Components/DescriptionList/DescriptionList.css';
margin-right: 10px;
}
.title { .title {
composes: title from 'Components/DescriptionList/DescriptionListItemTitle.css'; composes: title from 'Components/DescriptionList/DescriptionListItemTitle.css';

@ -14,7 +14,7 @@ function SceneInfo(props) {
} = props; } = props;
return ( return (
<DescriptionList> <DescriptionList className={styles.descriptionList}>
{ {
sceneSeasonNumber !== undefined && sceneSeasonNumber !== undefined &&
<DescriptionListItem <DescriptionListItem

@ -117,9 +117,9 @@ class InteractiveAlbumSearchModalContent extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={customFilters}
onFilterSelect={onFilterSelect}
buttonComponent={PageMenuButton} buttonComponent={PageMenuButton}
filterModalConnectorComponent={InteractiveSearchFilterModalConnector} filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
onFilterSelect={onFilterSelect}
/> />
</div> </div>

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import * as releaseActions from 'Store/Actions/releaseActions'; import * as releaseActions from 'Store/Actions/releaseActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
@ -10,7 +10,7 @@ import InteractiveAlbumSearchModalContent from './InteractiveAlbumSearchModalCon
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.releases.items.length, (state) => state.releases.items.length,
createClientSideCollectionSelector(), createClientSideCollectionSelector('releases'),
createUISettingsSelector(), createUISettingsSelector(),
(totalReleasesCount, releases, uiSettings) => { (totalReleasesCount, releases, uiSettings) => {
return { return {
@ -95,10 +95,4 @@ InteractiveAlbumSearchModalContentConnector.propTypes = {
dispatchCancelFetchReleases: PropTypes.func.isRequired dispatchCancelFetchReleases: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveAlbumSearchModalContentConnector);
createMapStateToProps,
createMapDispatchToProps,
undefined,
undefined,
{ section: 'releases' }
)(InteractiveAlbumSearchModalContentConnector);

@ -18,8 +18,8 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemoveCustomFilterPress(index) { onRemoveCustomFilterPress(payload) {
dispatch(releaseActions.removeReleasesCustomFilter({ index })); dispatch(releaseActions.removeReleasesCustomFilter(payload));
}, },
onSaveCustomFilterPress(payload) { onSaveCustomFilterPress(payload) {

@ -101,6 +101,7 @@ class AlbumStudio extends Component {
isFetching, isFetching,
isPopulated, isPopulated,
error, error,
totalItems,
items, items,
selectedFilterKey, selectedFilterKey,
filters, filters,
@ -178,7 +179,7 @@ class AlbumStudio extends Component {
{ {
!error && isPopulated && !items.length && !error && isPopulated && !items.length &&
<NoArtist /> <NoArtist totalItems={totalItems} />
} }
</PageContentBodyConnector> </PageContentBodyConnector>
@ -197,6 +198,7 @@ AlbumStudio.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),

@ -26,7 +26,7 @@ class AlbumStudioAlbum extends Component {
id, id,
title, title,
monitored, monitored,
statistics, statistics = {},
isSaving isSaving
} = this.props; } = this.props;

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { setAlbumStudioSort, setAlbumStudioFilter, saveAlbumStudio } from 'Store/Actions/albumStudioActions'; import { setAlbumStudioSort, setAlbumStudioFilter, saveAlbumStudio } from 'Store/Actions/albumStudioActions';
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions'; import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
@ -9,7 +9,7 @@ import AlbumStudio from './AlbumStudio';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createClientSideCollectionSelector(), createClientSideCollectionSelector('artist', 'albumStudio'),
(artist) => { (artist) => {
return { return {
...artist ...artist
@ -88,10 +88,4 @@ AlbumStudioConnector.propTypes = {
saveAlbumStudio: PropTypes.func.isRequired saveAlbumStudio: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'artist', uiSection: 'albumStudio' }
)(AlbumStudioConnector);

@ -26,6 +26,7 @@ import ImportListSettings from 'Settings/ImportLists/ImportListSettings';
import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings'; import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import MetadataSettings from 'Settings/Metadata/MetadataSettings'; import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import TagSettings from 'Settings/Tags/TagSettings';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import UISettingsConnector from 'Settings/UI/UISettingsConnector'; import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import Status from 'System/Status/Status'; import Status from 'System/Status/Status';
@ -191,6 +192,11 @@ function AppRoutes(props) {
component={MetadataSettings} component={MetadataSettings}
/> />
<Route
path="/settings/tags"
component={TagSettings}
/>
<Route <Route
path="/settings/general" path="/settings/general"
component={GeneralSettingsConnector} component={GeneralSettingsConnector}

@ -29,9 +29,8 @@
font-size: 18px; font-size: 18px;
} }
.episodeCountContainer { .episodeCountTooltip {
margin-left: 10px; display: flex;
vertical-align: text-bottom;
} }
.expandButton { .expandButton {

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { findCommand } from 'Utilities/Command'; import { findCommand } from 'Utilities/Command';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector';
@ -18,7 +17,7 @@ import ArtistDetailsSeason from './ArtistDetailsSeason';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { label }) => label, (state, { label }) => label,
createClientSideCollectionSelector(), createClientSideCollectionSelector('albums'),
createArtistSelector(), createArtistSelector(),
createCommandsSelector(), createCommandsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
@ -96,10 +95,4 @@ ArtistDetailsSeasonConnector.propTypes = {
executeCommand: PropTypes.func.isRequired executeCommand: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'albums' }
)(ArtistDetailsSeasonConnector);

@ -13,9 +13,10 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import NoArtist from 'Artist/NoArtist'; import NoArtist from 'Artist/NoArtist';
import OrganizeArtistModal from './Organize/OrganizeArtistModal';
import ArtistEditorRowConnector from './ArtistEditorRowConnector'; import ArtistEditorRowConnector from './ArtistEditorRowConnector';
import ArtistEditorFooter from './ArtistEditorFooter'; import ArtistEditorFooter from './ArtistEditorFooter';
import OrganizeArtistModal from './Organize/OrganizeArtistModal'; import ArtistEditorFilterModalConnector from './ArtistEditorFilterModalConnector';
function getColumns(showLanguageProfile, showMetadataProfile) { function getColumns(showLanguageProfile, showMetadataProfile) {
return [ return [
@ -148,6 +149,7 @@ class ArtistEditor extends Component {
isFetching, isFetching,
isPopulated, isPopulated,
error, error,
totalItems,
items, items,
selectedFilterKey, selectedFilterKey,
filters, filters,
@ -184,6 +186,7 @@ class ArtistEditor extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={customFilters}
filterModalConnectorComponent={ArtistEditorFilterModalConnector}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
</PageToolbarSection> </PageToolbarSection>
@ -234,7 +237,7 @@ class ArtistEditor extends Component {
{ {
!error && isPopulated && !items.length && !error && isPopulated && !items.length &&
<NoArtist /> <NoArtist totalItems={totalItems} />
} }
</PageContentBodyConnector> </PageContentBodyConnector>
@ -266,6 +269,7 @@ ArtistEditor.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),

@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandSelector from 'Store/Selectors/createCommandSelector'; import createCommandSelector from 'Store/Selectors/createCommandSelector';
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions'; import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
@ -14,7 +14,7 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.languageProfiles, (state) => state.settings.languageProfiles,
(state) => state.settings.metadataProfiles, (state) => state.settings.metadataProfiles,
createClientSideCollectionSelector(), createClientSideCollectionSelector('artist', 'artistEditor'),
createCommandSelector(commandNames.RENAME_ARTIST), createCommandSelector(commandNames.RENAME_ARTIST),
(languageProfiles, metadataProfiles, artist, isOrganizingArtist) => { (languageProfiles, metadataProfiles, artist, isOrganizingArtist) => {
return { return {
@ -89,10 +89,4 @@ ArtistEditorConnector.propTypes = {
dispatchExecuteCommand: PropTypes.func.isRequired dispatchExecuteCommand: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(ArtistEditorConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'artist', uiSection: 'artistEditor' }
)(ArtistEditorConnector);

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

@ -185,6 +185,7 @@ class ArtistIndex extends Component {
isFetching, isFetching,
isPopulated, isPopulated,
error, error,
totalItems,
items, items,
selectedFilterKey, selectedFilterKey,
filters, filters,
@ -341,7 +342,7 @@ class ArtistIndex extends Component {
{ {
!error && isPopulated && !items.length && !error && isPopulated && !items.length &&
<NoArtist /> <NoArtist totalItems={totalItems} />
} }
</PageContentBodyConnector> </PageContentBodyConnector>
@ -379,6 +380,7 @@ ArtistIndex.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import createCommandSelector from 'Store/Selectors/createCommandSelector'; import createCommandSelector from 'Store/Selectors/createCommandSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -45,18 +46,21 @@ function getScrollTop(view, scrollTop, isSmallScreen) {
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.artist, createClientSideCollectionSelector('artist', 'artistIndex'),
(state) => state.artistIndex,
createCommandSelector(commandNames.REFRESH_ARTIST), createCommandSelector(commandNames.REFRESH_ARTIST),
createCommandSelector(commandNames.RSS_SYNC), createCommandSelector(commandNames.RSS_SYNC),
createDimensionsSelector(), createDimensionsSelector(),
(artist, artistIndex, isRefreshingArtist, isRssSyncExecuting, dimensionsState) => { (
artist,
isRefreshingArtist,
isRssSyncExecuting,
dimensionsState
) => {
return { return {
...artist,
isRefreshingArtist, isRefreshingArtist,
isRssSyncExecuting, isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen, isSmallScreen: dimensionsState.isSmallScreen
...artist,
...artistIndex
}; };
} }
); );

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

@ -15,8 +15,16 @@ function ArtistIndexFooter({ artist }) {
let totalFileSize = 0; let totalFileSize = 0;
artist.forEach((s) => { artist.forEach((s) => {
tracks += s.statistics.trackCount || 0; const { statistics = {} } = s;
trackFiles += s.statistics.trackFileCount || 0;
const {
trackCount = 0,
trackFileCount = 0,
sizeOnDisk = 0
} = statistics;
tracks += trackCount;
trackFiles += trackFileCount;
if (s.status === 'ended') { if (s.status === 'ended') {
ended++; ended++;
@ -28,7 +36,7 @@ function ArtistIndexFooter({ artist }) {
monitored++; monitored++;
} }
totalFileSize += s.statistics.sizeOnDisk || 0; totalFileSize += sizeOnDisk;
}); });
return ( return (

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -8,7 +8,7 @@ import ArtistIndexBanners from './ArtistIndexBanners';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.artistIndex.bannerOptions, (state) => state.artistIndex.bannerOptions,
createClientSideCollectionSelector(), createClientSideCollectionSelector('artist', 'artistIndex'),
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(bannerOptions, artist, uiSettings, dimensions) => { (bannerOptions, artist, uiSettings, dimensions) => {
@ -24,10 +24,4 @@ function createMapStateToProps() {
); );
} }
export default connectSection( export default connect(createMapStateToProps)(ArtistIndexBanners);
createMapStateToProps,
undefined,
undefined,
undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexBanners);

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import FilterMenu from 'Components/Menu/FilterMenu'; import FilterMenu from 'Components/Menu/FilterMenu';
import ArtistIndexFilterModalConnector from 'Artist/Index/ArtistIndexFilterModalConnector';
function ArtistIndexFilterMenu(props) { function ArtistIndexFilterMenu(props) {
const { const {
@ -19,6 +20,7 @@ function ArtistIndexFilterMenu(props) {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={customFilters}
filterModalConnectorComponent={ArtistIndexFilterModalConnector}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
); );

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -8,7 +8,7 @@ import ArtistIndexOverviews from './ArtistIndexOverviews';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.artistIndex.overviewOptions, (state) => state.artistIndex.overviewOptions,
createClientSideCollectionSelector(), createClientSideCollectionSelector('artist', 'artistIndex'),
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(overviewOptions, artist, uiSettings, dimensions) => { (overviewOptions, artist, uiSettings, dimensions) => {
@ -24,10 +24,4 @@ function createMapStateToProps() {
); );
} }
export default connectSection( export default connect(createMapStateToProps)(ArtistIndexOverviews);
createMapStateToProps,
undefined,
undefined,
undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexOverviews);

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@ -8,7 +8,7 @@ import ArtistIndexPosters from './ArtistIndexPosters';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.artistIndex.posterOptions, (state) => state.artistIndex.posterOptions,
createClientSideCollectionSelector(), createClientSideCollectionSelector('artist', 'artistIndex'),
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(posterOptions, artist, uiSettings, dimensions) => { (posterOptions, artist, uiSettings, dimensions) => {
@ -24,10 +24,4 @@ function createMapStateToProps() {
); );
} }
export default connectSection( export default connect(createMapStateToProps)(ArtistIndexPosters);
createMapStateToProps,
undefined,
undefined,
undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexPosters);

@ -20,7 +20,8 @@
.nextAlbum, .nextAlbum,
.lastAlbum, .lastAlbum,
.added { .added,
.genres {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 180px; flex: 0 0 180px;
@ -58,6 +59,12 @@
flex: 0 0 120px; flex: 0 0 120px;
} }
.ratings {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 80px;
}
.tags { .tags {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';

@ -20,7 +20,8 @@
.nextAlbum, .nextAlbum,
.lastAlbum, .lastAlbum,
.added { .added,
.genres {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 0 180px; flex: 0 0 180px;
@ -60,6 +61,12 @@
flex: 0 0 120px; flex: 0 0 120px;
} }
.ratings {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 80px;
}
.tags { .tags {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind'; import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import HeartRating from 'Components/HeartRating';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
@ -75,6 +76,8 @@ class ArtistIndexRow extends Component {
lastAlbum, lastAlbum,
added, added,
statistics, statistics,
genres,
ratings,
path, path,
tags, tags,
columns, columns,
@ -303,6 +306,34 @@ class ArtistIndexRow extends Component {
); );
} }
if (name === 'genres') {
const joinedGenres = genres.join(', ');
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<span title={joinedGenres}>
{joinedGenres}
</span>
</VirtualTableRowCell>
);
}
if (name === 'ratings') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
<HeartRating
rating={ratings.value}
/>
</VirtualTableRowCell>
);
}
if (name === 'tags') { if (name === 'tags') {
return ( return (
<VirtualTableRowCell <VirtualTableRowCell
@ -376,6 +407,8 @@ ArtistIndexRow.propTypes = {
statistics: PropTypes.object.isRequired, statistics: PropTypes.object.isRequired,
latestAlbum: PropTypes.object, latestAlbum: PropTypes.object,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
ratings: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isRefreshingArtist: PropTypes.bool.isRequired, isRefreshingArtist: PropTypes.bool.isRequired,

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { setArtistSort } from 'Store/Actions/artistIndexActions'; import { setArtistSort } from 'Store/Actions/artistIndexActions';
import ArtistIndexTable from './ArtistIndexTable'; import ArtistIndexTable from './ArtistIndexTable';
@ -7,7 +7,7 @@ import ArtistIndexTable from './ArtistIndexTable';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.app.dimensions, (state) => state.app.dimensions,
createClientSideCollectionSelector(), createClientSideCollectionSelector('artist', 'artistIndex'),
(dimensions, artist) => { (dimensions, artist) => {
return { return {
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
@ -25,10 +25,4 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
export default connectSection( export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexTable);
createMapStateToProps,
createMapDispatchToProps,
undefined,
undefined,
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexTable);

@ -1,9 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import styles from './NoArtist.css'; import styles from './NoArtist.css';
function NoArtist() { function NoArtist(props) {
const { totalItems } = props;
if (totalItems > 0) {
return (
<div>
<div className={styles.message}>
All artists are hidden due to the applied filter.
</div>
</div>
);
}
return ( return (
<div> <div>
<div className={styles.message}> <div className={styles.message}>
@ -31,4 +44,8 @@ function NoArtist() {
); );
} }
NoArtist.propTypes = {
totalItems: PropTypes.number.isRequired
};
export default NoArtist; export default NoArtist;

@ -2,14 +2,15 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import CalendarDay from './CalendarDay'; import CalendarDay from './CalendarDay';
function createCalendarEventsConnector() { function createCalendarEventsConnector() {
return createSelector( return createSelector(
(state, { date }) => date, (state, { date }) => date,
(state) => state.calendar, createClientSideCollectionSelector('calendar'),
(date, calendar) => { (date, calendar) => {
const filtered = _.filter(calendar.items, (item) => { const filtered = _.filter(calendar.items, (item) => {
return moment(date).isSame(moment(item.releaseDate), 'day'); return moment(date).isSame(moment(item.releaseDate), 'day');
@ -52,10 +53,4 @@ CalendarDayConnector.propTypes = {
date: PropTypes.string.isRequired date: PropTypes.string.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps)(CalendarDayConnector);
createMapStateToProps,
undefined,
undefined,
undefined,
{ section: 'calendar' }
)(CalendarDayConnector);

@ -1,4 +1,4 @@
.descriptionList { .descriptionList {
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 0;
} }

@ -9,11 +9,12 @@ class DescriptionList extends Component {
render() { render() {
const { const {
className,
children children
} = this.props; } = this.props;
return ( return (
<dl className={styles.descriptionList}> <dl className={className}>
{children} {children}
</dl> </dl>
); );
@ -21,7 +22,12 @@ class DescriptionList extends Component {
} }
DescriptionList.propTypes = { DescriptionList.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.node children: PropTypes.node
}; };
DescriptionList.defaultProps = {
className: styles.descriptionList
};
export default DescriptionList; export default DescriptionList;

@ -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 (
<FilterBuilderRowValue
tagList={protocols}
{...props}
/>
);
}
export default ArtistStatusFilterBuilderRowValue;

@ -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 (
<FilterBuilderRowValue
tagList={protocols}
{...props}
/>
);
}
export default BoolFilterBuilderRowValue;

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

@ -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 (
<div className={styles.container}>
<NumberInput
className={styles.numberInput}
name={NAME}
value={filterValue.value}
onChange={this.onValueChange}
/>
<SelectInput
className={styles.selectInput}
name={NAME}
value={filterValue.time}
values={timeOptions}
onChange={this.onTimeChange}
/>
</div>
);
}
return (
<TextInput
name={NAME}
value={filterValue}
placeholder="yyyy-mm-dd"
onChange={this.onValueChange}
/>
);
}
}
DateFilterBuilderRowValue.propTypes = {
filterType: PropTypes.string,
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
onChange: PropTypes.func.isRequired
};
export default DateFilterBuilderRowValue;

@ -3,10 +3,17 @@ import React, { Component } from 'react';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import LanguageProfileFilterBuilderRowValueConnector from './LanguageProfileFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css'; import styles from './FilterBuilderRow.css';
function getselectedFilterBuilderProp(filterBuilderProps, name) { function getselectedFilterBuilderProp(filterBuilderProps, name) {
@ -29,6 +36,14 @@ function getDefaultFilterType(selectedFilterBuilderProp) {
return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key; return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key;
} }
function getDefaultFilterValue(selectedFilterBuilderProp) {
if (selectedFilterBuilderProp.type === filterBuilderTypes.DATE) {
return '';
}
return [];
}
function getRowValueConnector(selectedFilterBuilderProp) { function getRowValueConnector(selectedFilterBuilderProp) {
if (!selectedFilterBuilderProp) { if (!selectedFilterBuilderProp) {
return FilterBuilderRowValueConnector; return FilterBuilderRowValueConnector;
@ -37,15 +52,36 @@ function getRowValueConnector(selectedFilterBuilderProp) {
const valueType = selectedFilterBuilderProp.valueType; const valueType = selectedFilterBuilderProp.valueType;
switch (valueType) { switch (valueType) {
case filterBuilderValueTypes.BOOL:
return BoolFilterBuilderRowValue;
case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue;
case filterBuilderValueTypes.INDEXER: case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector; return IndexerFilterBuilderRowValueConnector;
case filterBuilderValueTypes.LANGUAGE_PROFILE:
return LanguageProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.METADATA_PROFILE:
return MetadataProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.PROTOCOL: case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue; return ProtocolFilterBuilderRowValue;
case filterBuilderValueTypes.QUALITY: case filterBuilderValueTypes.QUALITY:
return QualityFilterBuilderRowValueConnector; return QualityFilterBuilderRowValueConnector;
case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.ARTIST_STATUS:
return ArtistStatusFilterBuilderRowValue;
case filterBuilderValueTypes.TAG:
return TagFilterBuilderRowValueConnector;
default: default:
return FilterBuilderRowValueConnector; return FilterBuilderRowValueConnector;
} }
@ -59,9 +95,15 @@ class FilterBuilderRow extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { const {
selectedFilterBuilderProp: null filterKey,
}; filterBuilderProps
} = props;
if (filterKey) {
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
}
} }
componentDidMount() { componentDidMount() {
@ -74,7 +116,7 @@ class FilterBuilderRow extends Component {
if (filterKey) { if (filterKey) {
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey); const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
this.setState({ selectedFilterBuilderProp }); this.selectedFilterBuilderProp = selectedFilterBuilderProp;
return; return;
} }
@ -83,13 +125,12 @@ class FilterBuilderRow extends Component {
const filter = { const filter = {
key: selectedFilterBuilderProp.name, key: selectedFilterBuilderProp.name,
value: [], value: getDefaultFilterValue(selectedFilterBuilderProp),
type: getDefaultFilterType(selectedFilterBuilderProp) type: getDefaultFilterType(selectedFilterBuilderProp)
}; };
this.setState({ selectedFilterBuilderProp }, () => { this.selectedFilterBuilderProp = selectedFilterBuilderProp;
onFilterChange(index, filter); onFilterChange(index, filter);
});
} }
// //
@ -107,13 +148,12 @@ class FilterBuilderRow extends Component {
const filter = { const filter = {
key, key,
value: [], value: getDefaultFilterValue(selectedFilterBuilderProp),
type type
}; };
this.setState({ selectedFilterBuilderProp }, () => { this.selectedFilterBuilderProp = selectedFilterBuilderProp;
onFilterChange(index, filter); onFilterChange(index, filter);
});
} }
onFilterChange = ({ name, value }) => { onFilterChange = ({ name, value }) => {
@ -163,12 +203,11 @@ class FilterBuilderRow extends Component {
filterType, filterType,
filterValue, filterValue,
filterCount, filterCount,
filterBuilderProps filterBuilderProps,
sectionItems
} = this.props; } = this.props;
const { const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
selectedFilterBuilderProp
} = this.state;
const keyOptions = filterBuilderProps.map((availablePropFilter) => { const keyOptions = filterBuilderProps.map((availablePropFilter) => {
return { return {
@ -209,8 +248,10 @@ class FilterBuilderRow extends Component {
{ {
filterValue != null && !!selectedFilterBuilderProp && filterValue != null && !!selectedFilterBuilderProp &&
<ValueComponent <ValueComponent
filterType={filterType}
filterValue={filterValue} filterValue={filterValue}
selectedFilterBuilderProp={selectedFilterBuilderProp} selectedFilterBuilderProp={selectedFilterBuilderProp}
sectionItems={sectionItems}
onChange={this.onFilterChange} onChange={this.onFilterChange}
/> />
} }
@ -236,10 +277,11 @@ class FilterBuilderRow extends Component {
FilterBuilderRow.propTypes = { FilterBuilderRow.propTypes = {
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
filterKey: PropTypes.string, 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, filterType: PropTypes.string,
filterCount: PropTypes.number.isRequired, filterCount: PropTypes.number.isRequired,
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired, filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
onFilterChange: PropTypes.func.isRequired, onFilterChange: PropTypes.func.isRequired,
onAddPress: PropTypes.func.isRequired, onAddPress: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired onRemovePress: PropTypes.func.isRequired

@ -4,7 +4,7 @@ import { kinds, filterBuilderTypes } from 'Helpers/Props';
import TagInput, { tagShape } from 'Components/Form/TagInput'; import TagInput, { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag'; import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
const NAME = 'value'; export const NAME = 'value';
class FilterBuilderRowValue extends Component { class FilterBuilderRowValue extends Component {
@ -91,7 +91,7 @@ class FilterBuilderRowValue extends Component {
} }
FilterBuilderRowValue.propTypes = { 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, selectedFilterBuilderProp: PropTypes.object.isRequired,
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired, tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired

@ -1,12 +1,13 @@
import _ from 'lodash'; import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import { filterBuilderTypes } from 'Helpers/Props'; import { filterBuilderTypes } from 'Helpers/Props';
import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValue from './FilterBuilderRowValue';
function createTagListSelector() { function createTagListSelector() {
return createSelector( return createSelector(
(state, { sectionItems }) => _.get(state, sectionItems), (state, { sectionItems }) => sectionItems,
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp, (state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
(sectionItems, selectedFilterBuilderProp) => { (sectionItems, selectedFilterBuilderProp) => {
if ( if (
@ -19,16 +20,20 @@ function createTagListSelector() {
let items = []; let items = [];
if (selectedFilterBuilderProp.optionsSelector) { if (selectedFilterBuilderProp.optionsSelector) {
items = sectionItems.map(selectedFilterBuilderProp.optionsSelector); items = selectedFilterBuilderProp.optionsSelector(sectionItems);
} else { } else {
items = sectionItems.map((item) => { items = sectionItems.reduce((acc, item) => {
const name = item[selectedFilterBuilderProp.name]; const name = item[selectedFilterBuilderProp.name];
return { if (name) {
id: name, acc.push({
name id: name,
}; name
}); });
}
return acc;
}, []).sort(sortByName);
} }
return _.uniqBy(items, 'id'); return _.uniqBy(items, 'id');

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

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

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

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

@ -9,9 +9,28 @@
} }
.inputContainer { .inputContainer {
position: relative;
flex: 1 1 auto; 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 { .pendingChangesContainer {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

@ -83,6 +83,7 @@ function FormInputGroup(props) {
containerClassName, containerClassName,
inputClassName, inputClassName,
type, type,
unit,
buttons, buttons,
helpText, helpText,
helpTexts, helpTexts,
@ -115,6 +116,19 @@ function FormInputGroup(props) {
hasButton={hasButton} hasButton={hasButton}
{...otherProps} {...otherProps}
/> />
{
unit &&
<div
className={
type === inputTypes.NUMBER ?
styles.inputUnitNumber :
styles.inputUnit
}
>
{unit}
</div>
}
</div> </div>
{ {
@ -219,6 +233,7 @@ FormInputGroup.propTypes = {
containerClassName: PropTypes.string.isRequired, containerClassName: PropTypes.string.isRequired,
inputClassName: PropTypes.string, inputClassName: PropTypes.string,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
unit: PropTypes.string,
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]), buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
helpText: PropTypes.string, helpText: PropTypes.string,
helpTexts: PropTypes.arrayOf(PropTypes.string), helpTexts: PropTypes.arrayOf(PropTypes.string),

@ -48,12 +48,14 @@ class NumberInput extends Component {
render() { render() {
const { const {
value,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<TextInput <TextInput
type="number" type="number"
value={value == null ? '' : value}
{...otherProps} {...otherProps}
onChange={this.onChange} onChange={this.onChange}
onBlur={this.onBlur} onBlur={this.onBlur}

@ -1,30 +1,39 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
function OAuthInput(props) { function OAuthInput(props) {
const { const {
label,
authorizing, authorizing,
error,
onPress onPress
} = props; } = props;
return ( return (
<div> <div>
<SpinnerButton <SpinnerErrorButton
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
isSpinning={authorizing} isSpinning={authorizing}
error={error}
onPress={onPress} onPress={onPress}
> >
Start OAuth {label}
</SpinnerButton> </SpinnerErrorButton>
</div> </div>
); );
} }
OAuthInput.propTypes = { OAuthInput.propTypes = {
label: PropTypes.string.isRequired,
authorizing: PropTypes.bool.isRequired, authorizing: PropTypes.bool.isRequired,
error: PropTypes.object,
onPress: PropTypes.func.isRequired onPress: PropTypes.func.isRequired
}; };
OAuthInput.defaultProps = {
label: 'Start OAuth'
};
export default OAuthInput; export default OAuthInput;

@ -26,18 +26,17 @@ class OAuthInputConnector extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
accessToken, result,
accessTokenSecret,
onChange onChange
} = this.props; } = this.props;
if (accessToken && if (!result || result === prevProps.result) {
accessToken !== prevProps.accessToken && return;
accessTokenSecret &&
accessTokenSecret !== prevProps.accessTokenSecret) {
onChange({ name: 'AccessToken', value: accessToken });
onChange({ name: 'AccessTokenSecret', value: accessTokenSecret });
} }
Object.keys(result).forEach((key) => {
onChange({ name: key, value: result[key] });
});
} }
componentWillUnmount = () => { componentWillUnmount = () => {
@ -70,8 +69,7 @@ class OAuthInputConnector extends Component {
} }
OAuthInputConnector.propTypes = { OAuthInputConnector.propTypes = {
accessToken: PropTypes.string, result: PropTypes.object,
accessTokenSecret: PropTypes.string,
provider: PropTypes.string.isRequired, provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired, providerData: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,

@ -7,19 +7,6 @@ import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
function getType(type) { function getType(type) {
// Textbox,
// Password,
// Checkbox,
// Select,
// Path,
// FilePath,
// Hidden,
// Tag,
// Action,
// Url,
// Captcha
// OAuth
switch (type) { switch (type) {
case 'captcha': case 'captcha':
return inputTypes.CAPTCHA; return inputTypes.CAPTCHA;
@ -27,6 +14,8 @@ function getType(type) {
return inputTypes.CHECK; return inputTypes.CHECK;
case 'password': case 'password':
return inputTypes.PASSWORD; return inputTypes.PASSWORD;
case 'number':
return inputTypes.NUMBER;
case 'path': case 'path':
return inputTypes.PATH; return inputTypes.PATH;
case 'select': case 'select':
@ -83,6 +72,7 @@ function ProviderFieldFormGroup(props) {
<FormInputGroup <FormInputGroup
type={getType(type)} type={getType(type)}
name={name} name={name}
label={label}
helpText={helpText} helpText={helpText}
helpLink={helpLink} helpLink={helpLink}
value={value} value={value}

@ -32,7 +32,7 @@ function RootFolderSelectInputSelectedValue(props) {
} }
RootFolderSelectInputSelectedValue.propTypes = { RootFolderSelectInputSelectedValue.propTypes = {
value: PropTypes.string.isRequired, value: PropTypes.string,
freeSpace: PropTypes.number, freeSpace: PropTypes.number,
includeFreeSpace: PropTypes.bool.isRequired includeFreeSpace: PropTypes.bool.isRequired
}; };

@ -32,6 +32,7 @@
min-width: 20%; min-width: 20%;
max-width: 100%; max-width: 100%;
width: 0%; width: 0%;
height: 21px;
border: none; border: none;
} }

@ -58,20 +58,20 @@ class TagInput extends Component {
return name; return name;
} }
// addTag = _.debounce((tag) => {
// Listeners
onInputContainerPress = () => {
this._autosuggestRef.input.focus();
}
onTagAdd(tag) {
this.props.onTagAdd(tag); this.props.onTagAdd(tag);
this.setState({ this.setState({
value: '', value: '',
suggestions: [] suggestions: []
}); });
}, 250, { leading: true, trailing: false })
//
// Listeners
onInputContainerPress = () => {
this._autosuggestRef.input.focus();
} }
onInputChange = (event, { newValue, method }) => { onInputChange = (event, { newValue, method }) => {
@ -116,10 +116,9 @@ class TagInput extends Component {
const tag = getTag(value, selectedIndex, suggestions, allowNew); const tag = getTag(value, selectedIndex, suggestions, allowNew);
if (tag) { 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); const tag = getTag(value, selectedIndex, suggestions, allowNew);
if (tag) { if (tag) {
this.onTagAdd(tag); this.addTag(tag);
} }
} }
@ -174,7 +173,7 @@ class TagInput extends Component {
} }
onSuggestionSelected = (event, { suggestion }) => { onSuggestionSelected = (event, { suggestion }) => {
this.onTagAdd(suggestion); this.addTag(suggestion);
} }
// //
@ -262,7 +261,7 @@ class TagInput extends Component {
} }
export const tagShape = { 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 name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
}; };

@ -6,7 +6,6 @@
color: $white; color: $white;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
font-weight: bold;
line-height: 1; line-height: 1;
cursor: default; cursor: default;
} }
@ -92,6 +91,7 @@
.large { .large {
padding: 3px 7px; padding: 3px 7px;
font-weight: bold;
font-size: 14px; font-size: 14px;
} }

@ -48,6 +48,10 @@ class Menu extends Component {
this.setMaxHeight(); this.setMaxHeight();
} }
componentWillUnmount() {
this._removeListener();
}
// //
// Control // Control

@ -54,7 +54,7 @@
.extraLarge { .extraLarge {
composes: modal; composes: modal;
width: 1440px; width: 1280px;
} }
@media only screen and (max-width: $breakpointExtraLarge) { @media only screen and (max-width: $breakpointExtraLarge) {

@ -123,6 +123,10 @@ const links = [
title: 'Metadata', title: 'Metadata',
to: '/settings/metadata' to: '/settings/metadata'
}, },
{
title: 'Tags',
to: '/settings/tags'
},
{ {
title: 'General', title: 'General',
to: '/settings/general' to: '/settings/general'

@ -5,6 +5,7 @@ import { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { repopulatePage } from 'Utilities/pagePopulator'; import { repopulatePage } from 'Utilities/pagePopulator';
import titleCase from 'Utilities/String/titleCase';
import { updateCommand, finishCommand } from 'Store/Actions/commandActions'; import { updateCommand, finishCommand } from 'Store/Actions/commandActions';
import { setAppValue, setVersion } from 'Store/Actions/appActions'; import { setAppValue, setVersion } from 'Store/Actions/appActions';
import { update, updateItem, removeItem } from 'Store/Actions/baseActions'; import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
@ -34,6 +35,13 @@ function isAppDisconnected(disconnectedTime) {
return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180; return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180;
} }
function getHandlerName(name) {
name = titleCase(name);
name = name.replace('/', '');
return `handle${name}`;
}
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.app.isReconnecting, (state) => state.app.isReconnecting,
@ -91,6 +99,10 @@ class SignalRConnector extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.retryTimeoutId) {
this.retryTimeoutId = clearTimeout(this.retryTimeoutId);
}
this.signalRconnection.stop(); this.signalRconnection.stop();
this.signalRconnection = null; this.signalRconnection = null;
} }
@ -106,6 +118,11 @@ class SignalRConnector extends Component {
} }
this.retryTimeoutId = setTimeout(() => { this.retryTimeoutId = setTimeout(() => {
if (!this.signalRconnection) {
console.error('signalR: Connection was disposed');
return;
}
this.signalRconnection.start(this.signalRconnectionOptions); this.signalRconnection.start(this.signalRconnectionOptions);
this.retryInterval = Math.min(this.retryInterval + 1, 10); this.retryInterval = Math.min(this.retryInterval + 1, 10);
}, this.retryInterval * 1000); }, this.retryInterval * 1000);
@ -117,70 +134,14 @@ class SignalRConnector extends Component {
body body
} = message; } = message;
if (name === 'calendar') { const handler = this[getHandlerName(name)];
this.handleCalendar(body);
return;
}
if (name === 'command') {
this.handleCommand(body);
return;
}
if (name === 'album') {
this.handleAlbum(body);
return;
}
if (name === 'track') { if (handler) {
this.handleTrack(body); handler(body);
return; return;
} }
if (name === 'trackfile') { console.error(`signalR: Unable to find handler for ${name}`);
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;
}
} }
handleCalendar = (body) => { handleCalendar = (body) => {
@ -237,7 +198,7 @@ class SignalRConnector extends Component {
} }
} }
handleHealth = (body) => { handleHealth = () => {
this.props.fetchHealth(); this.props.fetchHealth();
} }
@ -252,13 +213,13 @@ class SignalRConnector extends Component {
} }
} }
handleQueue = (body) => { handleQueue = () => {
if (this.props.isQueuePopulated) { if (this.props.isQueuePopulated) {
this.props.fetchQueue(); this.props.fetchQueue();
} }
} }
handleQueueDetails = (body) => { handleQueueDetails = () => {
this.props.fetchQueueDetails(); this.props.fetchQueueDetails();
} }
@ -292,12 +253,16 @@ class SignalRConnector extends Component {
} }
} }
handleSystemTask = () => {
// No-op for now, we may want this later
}
// //
// Listeners // Listeners
onStateChanged = (change) => { onStateChanged = (change) => {
const state = getState(change.newState); const state = getState(change.newState);
console.log(`SignalR: ${state}`); console.log(`signalR: ${state}`);
if (state === 'connected') { if (state === 'connected') {
// Clear disconnected time // Clear disconnected time
@ -326,7 +291,7 @@ class SignalRConnector extends Component {
} }
onReceived = (message) => { onReceived = (message) => {
console.debug('SignalR: received', message.name, message.body); console.debug('signalR: received', message.name, message.body);
this.handleMessage(message); this.handleMessage(message);
} }

@ -6,6 +6,7 @@ function TableRow(props) {
const { const {
className, className,
children, children,
overlayContent,
...otherProps ...otherProps
} = props; } = props;
@ -21,7 +22,8 @@ function TableRow(props) {
TableRow.propTypes = { TableRow.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
children: PropTypes.node children: PropTypes.node,
overlayContent: PropTypes.bool
}; };
TableRow.defaultProps = { TableRow.defaultProps = {

@ -100,5 +100,5 @@
} }
.body { .body {
padding: 20px; padding: 10px;
} }

@ -89,6 +89,7 @@ class Popover extends Component {
render() { render() {
const { const {
className,
anchor, anchor,
title, title,
body, body,
@ -103,6 +104,7 @@ class Popover extends Component {
{...tetherOptions[position]} {...tetherOptions[position]}
> >
<span <span
className={className}
// onClick={this.onClick} // onClick={this.onClick}
onMouseEnter={this.onMouseEnter} onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave} onMouseLeave={this.onMouseLeave}
@ -141,6 +143,7 @@ class Popover extends Component {
} }
Popover.propTypes = { Popover.propTypes = {
className: PropTypes.string,
anchor: PropTypes.node.isRequired, anchor: PropTypes.node.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,

@ -1,33 +1,50 @@
import * as filterTypes from './filterTypes'; import * as filterTypes from './filterTypes';
export const ARRAY = 'array';
export const DATE = 'date';
export const EXACT = 'exact'; export const EXACT = 'exact';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const STRING = 'string'; export const STRING = 'string';
export const all = [ export const all = [
ARRAY,
DATE,
EXACT, EXACT,
NUMBER, NUMBER,
STRING STRING
]; ];
export const possibleFilterTypes = { export const possibleFilterTypes = {
[ARRAY]: [
{ key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
],
[DATE]: [
{ key: filterTypes.LESS_THAN, value: 'is before' },
{ key: filterTypes.GREATER_THAN, value: 'is after' },
{ key: filterTypes.IN_LAST, value: 'in the last' },
{ key: filterTypes.IN_NEXT, value: 'in the next' }
],
[EXACT]: [ [EXACT]: [
{ key: filterTypes.EQUAL, value: 'Is' }, { key: filterTypes.EQUAL, value: 'is' },
{ key: filterTypes.NOT_EQUAL, value: 'Is Not' } { key: filterTypes.NOT_EQUAL, value: 'is not' }
], ],
[NUMBER]: [ [NUMBER]: [
{ key: filterTypes.EQUAL, value: 'Equal' }, { key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.GREATER_THAN, value: 'Greater Than' }, { key: filterTypes.GREATER_THAN, value: 'greater than' },
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'Greater Than or Equal' }, { key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'greater than or equal' },
{ key: filterTypes.LESS_THAN, value: 'Less Than' }, { key: filterTypes.LESS_THAN, value: 'less than' },
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'Less Than or Equal' }, { key: filterTypes.LESS_THAN_OR_EQUAL, value: 'less than or equal' },
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' } { key: filterTypes.NOT_EQUAL, value: 'not equal' }
], ],
[STRING]: [ [STRING]: [
{ key: filterTypes.CONTAINS, value: 'Contains' }, { key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.EQUAL, value: 'Equal' }, { key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' } { key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
] ]
}; };

@ -1,4 +1,11 @@
export const BOOL = 'bool';
export const DATE = 'date';
export const DEFAULT = 'default'; export const DEFAULT = 'default';
export const INDEXER = 'indexer'; export const INDEXER = 'indexer';
export const LANGUAGE_PROFILE = 'languageProfile';
export const METADATA_PROFILE = 'metadataProfile';
export const PROTOCOL = 'protocol'; export const PROTOCOL = 'protocol';
export const QUALITY = 'quality'; export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const ARTIST_STATUS = 'artistStatus';
export const TAG = 'tag';

@ -0,0 +1,45 @@
import * as filterTypes from './filterTypes';
const filterTypePredicates = {
[filterTypes.CONTAINS]: function(itemValue, filterValue) {
if (Array.isArray(itemValue)) {
return itemValue.some((v) => 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;

@ -2,8 +2,11 @@ export const CONTAINS = 'contains';
export const EQUAL = 'equal'; export const EQUAL = 'equal';
export const GREATER_THAN = 'greaterThan'; export const GREATER_THAN = 'greaterThan';
export const GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual'; 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 = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual'; export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual'; export const NOT_EQUAL = 'notEqual';
export const all = [ export const all = [
@ -13,5 +16,6 @@ export const all = [
GREATER_THAN_OR_EQUAL, GREATER_THAN_OR_EQUAL,
LESS_THAN, LESS_THAN,
LESS_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL,
NOT_CONTAINS,
NOT_EQUAL NOT_EQUAL
]; ];

@ -2,6 +2,7 @@ import * as align from './align';
import * as inputTypes from './inputTypes'; import * as inputTypes from './inputTypes';
import * as filterBuilderTypes from './filterBuilderTypes'; import * as filterBuilderTypes from './filterBuilderTypes';
import * as filterBuilderValueTypes from './filterBuilderValueTypes'; import * as filterBuilderValueTypes from './filterBuilderValueTypes';
import filterTypePredicates from './filterTypePredicates';
import * as filterTypes from './filterTypes'; import * as filterTypes from './filterTypes';
import * as icons from './icons'; import * as icons from './icons';
import * as kinds from './kinds'; import * as kinds from './kinds';
@ -16,6 +17,7 @@ export {
inputTypes, inputTypes,
filterBuilderTypes, filterBuilderTypes,
filterBuilderValueTypes, filterBuilderValueTypes,
filterTypePredicates,
filterTypes, filterTypes,
icons, icons,
kinds, kinds,

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

@ -1,15 +1,62 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import { scrollDirections } from 'Helpers/Props';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter'; 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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import SelectAlbumRow from './SelectAlbumRow'; 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 { class SelectAlbumModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
filter: ''
};
}
//
// Listeners
onFilterChange = ({ value }) => {
this.setState({ filter: value.toLowerCase() });
}
// //
// Render // Render
@ -18,33 +65,60 @@ class SelectAlbumModalContent extends Component {
items, items,
onAlbumSelect, onAlbumSelect,
onModalClose, onModalClose,
isFetching isFetching,
...otherProps
} = this.props; } = this.props;
const filter = this.state.filter;
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Manual Import - Select Album Manual Import - Select Album
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
{ {
isFetching && isFetching &&
<LoadingIndicator /> <LoadingIndicator />
} }
{ <TextInput
items.map((item) => { className={styles.filterInput}
return ( placeholder="Filter album"
<SelectAlbumRow name="filter"
key={item.id} value={filter}
id={item.id} autoFocus={true}
title={item.title} onChange={this.onFilterChange}
albumType={item.albumType} />
onAlbumSelect={onAlbumSelect}
/> <Scroller className={styles.scroller}>
); {
}) <Table
} columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return item.title.toLowerCase().includes(filter) ?
(
<SelectAlbumRow
key={item.id}
columns={columns}
onAlbumSelect={onAlbumSelect}
{...item}
/>
) :
null;
})
}
</TableBody>
</Table>
}
</Scroller>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

@ -1,8 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { import {
updateInteractiveImportItem, updateInteractiveImportItem,
fetchInteractiveImportAlbums, fetchInteractiveImportAlbums,
@ -14,7 +14,7 @@ import SelectAlbumModalContent from './SelectAlbumModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createClientSideCollectionSelector(), createClientSideCollectionSelector('interactiveImport.albums'),
(albums) => { (albums) => {
return albums; return albums;
} }
@ -92,10 +92,4 @@ SelectAlbumModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(SelectAlbumModalContentConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'interactiveImport.albums' }
)(SelectAlbumModalContentConnector);

@ -1,4 +0,0 @@
.season {
padding: 8px;
border-bottom: 1px solid $borderColor;
}

@ -1,7 +1,23 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; 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 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 { class SelectAlbumRow extends Component {
@ -16,14 +32,87 @@ class SelectAlbumRow extends Component {
// Render // Render
render() { render() {
const {
title,
albumType,
releaseDate,
statistics,
monitored,
columns
} = this.props;
const {
trackCount,
trackFileCount,
totalTrackCount
} = statistics;
return ( return (
<Link
className={styles.season} <TableRow>
component="div" {
onPress={this.onPress} columns.map((column) => {
> const {
{this.props.title} ({this.props.albumType}) name,
</Link> isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'title') {
return (
<TableRowCell key={name}>
<Link
onPress={this.onPress}
>
{title}
</Link>
</TableRowCell>
);
}
if (name === 'albumType') {
return (
<TableRowCell key={name}>
{albumType}
</TableRowCell>
);
}
if (name === 'releaseDate') {
return (
<RelativeDateCellConnector
key={name}
date={releaseDate}
/>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
>
<Label
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
kind={getTrackCountKind(monitored, trackFileCount, trackCount)}
size={sizes.MEDIUM}
>
{
<span>{trackFileCount} / {trackCount}</span>
}
</Label>
</TableRowCell>
);
}
return null;
})
}
</TableRow>
); );
} }
} }
@ -32,7 +121,18 @@ SelectAlbumRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
albumType: 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; export default SelectAlbumRow;

@ -21,6 +21,10 @@ const recentFoldersColumns = [
{ {
name: 'lastUsed', name: 'lastUsed',
label: 'Last Used' label: 'Last Used'
},
{
name: 'actions',
label: ''
} }
]; ];
@ -62,6 +66,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
render() { render() {
const { const {
recentFolders, recentFolders,
onRemoveRecentFolderPress,
onModalClose onModalClose
} = this.props; } = this.props;
@ -95,6 +100,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
folder={recentFolder.folder} folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed} lastUsed={recentFolder.lastUsed}
onPress={this.onRecentPathPress} onPress={this.onRecentPathPress}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/> />
); );
}) })
@ -155,6 +161,7 @@ InteractiveImportSelectFolderModalContent.propTypes = {
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired, recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onQuickImportPress: PropTypes.func.isRequired, onQuickImportPress: PropTypes.func.isRequired,
onInteractiveImportPress: PropTypes.func.isRequired, onInteractiveImportPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { addRecentFolder } from 'Store/Actions/interactiveImportActions'; import { addRecentFolder, removeRecentFolder } from 'Store/Actions/interactiveImportActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent'; import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent';
@ -20,6 +20,7 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
addRecentFolder, addRecentFolder,
removeRecentFolder,
executeCommand executeCommand
}; };
@ -44,6 +45,10 @@ class InteractiveImportSelectFolderModalContentConnector extends Component {
this.props.onFolderSelect(folder); this.props.onFolderSelect(folder);
} }
onRemoveRecentFolderPress = (folder) => {
this.props.removeRecentFolder({ folder });
}
// //
// Render // Render
@ -57,6 +62,7 @@ class InteractiveImportSelectFolderModalContentConnector extends Component {
{...this.props} {...this.props}
onQuickImportPress={this.onQuickImportPress} onQuickImportPress={this.onQuickImportPress}
onInteractiveImportPress={this.onInteractiveImportPress} onInteractiveImportPress={this.onInteractiveImportPress}
onRemoveRecentFolderPress={this.onRemoveRecentFolderPress}
/> />
); );
} }
@ -67,6 +73,7 @@ InteractiveImportSelectFolderModalContentConnector.propTypes = {
onFolderSelect: PropTypes.func.isRequired, onFolderSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
addRecentFolder: PropTypes.func.isRequired, addRecentFolder: PropTypes.func.isRequired,
removeRecentFolder: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired executeCommand: PropTypes.func.isRequired
}; };

@ -0,0 +1,5 @@
.actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 40px;
}

@ -1,8 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import TableRowButton from 'Components/Table/TableRowButton'; import TableRowButton from 'Components/Table/TableRowButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import styles from './RecentFolderRow.css';
class RecentFolderRow extends Component { class RecentFolderRow extends Component {
@ -13,6 +16,17 @@ class RecentFolderRow extends Component {
this.props.onPress(this.props.folder); this.props.onPress(this.props.folder);
} }
onRemovePress = (event) => {
event.stopPropagation();
const {
folder,
onRemoveRecentFolderPress
} = this.props;
onRemoveRecentFolderPress(folder);
}
// //
// Render // Render
@ -27,6 +41,14 @@ class RecentFolderRow extends Component {
<TableRowCell>{folder}</TableRowCell> <TableRowCell>{folder}</TableRowCell>
<RelativeDateCellConnector date={lastUsed} /> <RelativeDateCellConnector date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
name={icons.REMOVE}
title="Remove"
onPress={this.onRemovePress}
/>
</TableRowCell>
</TableRowButton> </TableRowButton>
); );
} }
@ -35,7 +57,8 @@ class RecentFolderRow extends Component {
RecentFolderRow.propTypes = { RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired, lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired onPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired
}; };
export default RecentFolderRow; export default RecentFolderRow;

@ -1,8 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions'; import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
@ -11,7 +11,7 @@ import InteractiveImportModalContent from './InteractiveImportModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createClientSideCollectionSelector(), createClientSideCollectionSelector('interactiveImport'),
(interactiveImport) => { (interactiveImport) => {
return interactiveImport; return interactiveImport;
} }
@ -125,8 +125,19 @@ class InteractiveImportModalContentConnector extends Component {
return false; 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({ files.push({
path: item.path, path: item.path,
folderName: item.folderName,
artistId: artist.id, artistId: artist.id,
albumId: album.id, albumId: album.id,
trackIds: _.map(tracks, 'id'), trackIds: _.map(tracks, 'id'),
@ -190,10 +201,4 @@ InteractiveImportModalContentConnector.defaultProps = {
filterExistingFiles: true filterExistingFiles: true
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'interactiveImport' }
)(InteractiveImportModalContentConnector);

@ -191,6 +191,8 @@ class InteractiveImportRow extends Component {
const showArtistPlaceholder = isSelected && !artist; const showArtistPlaceholder = isSelected && !artist;
const showAlbumNumberPlaceholder = isSelected && !!artist && !album; const showAlbumNumberPlaceholder = isSelected && !!artist && !album;
const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length; const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !language;
return ( return (
<TableRow> <TableRow>
@ -237,20 +239,36 @@ class InteractiveImportRow extends Component {
className={styles.quality} className={styles.quality}
onPress={this.onSelectQualityPress} onPress={this.onSelectQualityPress}
> >
<EpisodeQuality {
className={styles.label} showQualityPlaceholder &&
quality={quality} <InteractiveImportRowCellPlaceholder />
/> }
{
!showQualityPlaceholder && !!quality &&
<EpisodeQuality
className={styles.label}
quality={quality}
/>
}
</TableRowCellButton> </TableRowCellButton>
<TableRowCellButton <TableRowCellButton
className={styles.language} className={styles.language}
onPress={this.onSelectLanguagePress} onPress={this.onSelectLanguagePress}
> >
<EpisodeLanguage {
className={styles.label} showLanguagePlaceholder &&
language={language} <InteractiveImportRowCellPlaceholder />
/> }
{
!showLanguagePlaceholder && !!language &&
<EpisodeLanguage
className={styles.label}
language={language}
/>
}
</TableRowCellButton> </TableRowCellButton>
<TableRowCell> <TableRowCell>
@ -310,16 +328,16 @@ class InteractiveImportRow extends Component {
<SelectQualityModal <SelectQualityModal
isOpen={isSelectQualityModalOpen} isOpen={isSelectQualityModalOpen}
id={id} id={id}
qualityId={quality.quality.id} qualityId={quality ? quality.quality.id : 0}
proper={quality.revision.version > 1} proper={quality ? quality.revision.version > 1 : false}
real={quality.revision.real > 0} real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose} onModalClose={this.onSelectQualityModalClose}
/> />
<SelectLanguageModal <SelectLanguageModal
isOpen={isSelectLanguageModalOpen} isOpen={isSelectLanguageModalOpen}
id={id} id={id}
languageId={language.id} languageId={language ? language.id : 0}
onModalClose={this.onSelectLanguageModalClose} onModalClose={this.onSelectLanguageModalClose}
/> />
</TableRow> </TableRow>

@ -22,7 +22,7 @@ function createMapStateToProps() {
isFetching, isFetching,
isPopulated, isPopulated,
error, error,
items: schema.languages || [] items: schema.languages ? [...schema.languages].reverse() : []
}; };
} }
); );

@ -1,8 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { fetchTracks, setTracksSort, clearTracks } from 'Store/Actions/trackActions'; import { fetchTracks, setTracksSort, clearTracks } from 'Store/Actions/trackActions';
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
@ -10,7 +10,7 @@ import SelectTrackModalContent from './SelectTrackModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createClientSideCollectionSelector(), createClientSideCollectionSelector('tracks'),
(tracks) => { (tracks) => {
return tracks; return tracks;
} }
@ -94,10 +94,4 @@ SelectTrackModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(SelectTrackModalContentConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'tracks' }
)(SelectTrackModalContentConnector);

@ -59,9 +59,7 @@ class DownloadClients extends Component {
} = this.state; } = this.state;
return ( return (
<FieldSet <FieldSet legend="Download Clients">
legend="Download Clients"
>
<PageSectionContent <PageSectionContent
errorMessage="Unable to load download clients" errorMessage="Unable to load download clients"
{...otherProps} {...otherProps}

@ -1,11 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import EditDownloadClientModalContentConnector from './EditDownloadClientModalContentConnector'; import EditDownloadClientModalContentConnector from './EditDownloadClientModalContentConnector';
function EditDownloadClientModal({ isOpen, onModalClose, ...otherProps }) { function EditDownloadClientModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
size={sizes.MEDIUM}
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >

@ -1,15 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, testDownloadClient } from 'Store/Actions/settingsActions'; import { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, testDownloadClient } from 'Store/Actions/settingsActions';
import connectSection from 'Store/connectSection';
import EditDownloadClientModalContent from './EditDownloadClientModalContent'; import EditDownloadClientModalContent from './EditDownloadClientModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createProviderSettingsSelector(), createProviderSettingsSelector('downloadClients'),
(advancedSettings, downloadClient) => { (advancedSettings, downloadClient) => {
return { return {
advancedSettings, advancedSettings,
@ -85,10 +85,4 @@ EditDownloadClientModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(EditDownloadClientModalContentConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'downloadClients' }
)(EditDownloadClientModalContentConnector);

@ -33,9 +33,7 @@ function DownloadClientOptions(props) {
{ {
hasSettings && !isFetching && !error && hasSettings && !isFetching && !error &&
<div> <div>
<FieldSet <FieldSet legend="Completed Download Handling">
legend="Completed Download Handling"
>
<Form> <Form>
<FormGroup size={sizes.MEDIUM}> <FormGroup size={sizes.MEDIUM}>
<FormLabel>Enable</FormLabel> <FormLabel>Enable</FormLabel>

@ -1,16 +1,18 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { fetchDownloadClientOptions, setDownloadClientOptionsValue, saveDownloadClientOptions } from 'Store/Actions/settingsActions'; import { fetchDownloadClientOptions, setDownloadClientOptionsValue, saveDownloadClientOptions } from 'Store/Actions/settingsActions';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import connectSection from 'Store/connectSection';
import DownloadClientOptions from './DownloadClientOptions'; import DownloadClientOptions from './DownloadClientOptions';
const SECTION = 'downloadClientOptions';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createSettingsSectionSelector(), createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => { (advancedSettings, sectionSettings) => {
return { return {
advancedSettings, advancedSettings,
@ -62,7 +64,7 @@ class DownloadClientOptionsConnector extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: this.props.section }); this.props.dispatchClearPendingChanges({ section: SECTION });
} }
// //
@ -86,7 +88,6 @@ class DownloadClientOptionsConnector extends Component {
} }
DownloadClientOptionsConnector.propTypes = { DownloadClientOptionsConnector.propTypes = {
section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchDownloadClientOptions: PropTypes.func.isRequired, dispatchFetchDownloadClientOptions: PropTypes.func.isRequired,
@ -97,10 +98,4 @@ DownloadClientOptionsConnector.propTypes = {
onChildStateChange: PropTypes.func.isRequired onChildStateChange: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientOptionsConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'settings.downloadClientOptions' }
)(DownloadClientOptionsConnector);

@ -1,11 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import EditRemotePathMappingModalContentConnector from './EditRemotePathMappingModalContentConnector'; import EditRemotePathMappingModalContentConnector from './EditRemotePathMappingModalContentConnector';
function EditRemotePathMappingModal({ isOpen, onModalClose, ...otherProps }) { function EditRemotePathMappingModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
size={sizes.MEDIUM}
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >

@ -44,9 +44,7 @@ class RemotePathMappings extends Component {
} = this.props; } = this.props;
return ( return (
<FieldSet <FieldSet legend="Remote Path Mappings">
legend="Remote Path Mappings"
>
<PageSectionContent <PageSectionContent
errorMessage="Unable to load Remote Path Mappings" errorMessage="Unable to load Remote Path Mappings"
{...otherProps} {...otherProps}

@ -49,7 +49,8 @@ function BackupSettings(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="backupInterval" name="backupInterval"
helpText="Interval in days" unit="days"
helpText="Interval to backup the Lidarr DB and settings"
onChange={onInputChange} onChange={onInputChange}
{...backupInterval} {...backupInterval}
/> />
@ -64,7 +65,8 @@ function BackupSettings(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="backupRetention" name="backupRetention"
helpText="Retention in days. Automatic backups older the retention will be cleaned up automatically" unit="days"
helpText="Automatic backups older than the retention will be cleaned up automatically"
onChange={onInputChange} onChange={onInputChange}
{...backupRetention} {...backupRetention}
/> />

@ -1,6 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@ -9,14 +10,15 @@ import { setGeneralSettingsValue, saveGeneralSettings, fetchGeneralSettings } fr
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { restart } from 'Store/Actions/systemActions'; import { restart } from 'Store/Actions/systemActions';
import connectSection from 'Store/connectSection';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import GeneralSettings from './GeneralSettings'; import GeneralSettings from './GeneralSettings';
const SECTION = 'general';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createSettingsSectionSelector(), createSettingsSectionSelector(SECTION),
createCommandsSelector(), createCommandsSelector(),
createSystemStatusSelector(), createSystemStatusSelector(),
(advancedSettings, sectionSettings, commands, systemStatus) => { (advancedSettings, sectionSettings, commands, systemStatus) => {
@ -59,7 +61,7 @@ class GeneralSettingsConnector extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section }); this.props.clearPendingChanges({ section: SECTION });
} }
// //
@ -98,7 +100,6 @@ class GeneralSettingsConnector extends Component {
} }
GeneralSettingsConnector.propTypes = { GeneralSettingsConnector.propTypes = {
section: PropTypes.string.isRequired,
isResettingApiKey: PropTypes.bool.isRequired, isResettingApiKey: PropTypes.bool.isRequired,
setGeneralSettingsValue: PropTypes.func.isRequired, setGeneralSettingsValue: PropTypes.func.isRequired,
saveGeneralSettings: PropTypes.func.isRequired, saveGeneralSettings: PropTypes.func.isRequired,
@ -108,10 +109,4 @@ GeneralSettingsConnector.propTypes = {
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(GeneralSettingsConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'settings.general' }
)(GeneralSettingsConnector);

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import { setImportListValue, setImportListFieldValue, saveImportList, testImportList } from 'Store/Actions/settingsActions'; import { setImportListValue, setImportListFieldValue, saveImportList, testImportList } from 'Store/Actions/settingsActions';
import connectSection from 'Store/connectSection';
import EditImportListModalContent from './EditImportListModalContent'; import EditImportListModalContent from './EditImportListModalContent';
function createMapStateToProps() { function createMapStateToProps() {
@ -11,7 +11,7 @@ function createMapStateToProps() {
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
(state) => state.settings.languageProfiles, (state) => state.settings.languageProfiles,
(state) => state.settings.metadataProfiles, (state) => state.settings.metadataProfiles,
createProviderSettingsSelector(), createProviderSettingsSelector('importLists'),
(advancedSettings, languageProfiles, metadataProfiles, importList) => { (advancedSettings, languageProfiles, metadataProfiles, importList) => {
return { return {
advancedSettings, advancedSettings,
@ -89,10 +89,4 @@ EditImportListModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'importLists' }
)(EditImportListModalContentConnector);

@ -1,11 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import EditIndexerModalContentConnector from './EditIndexerModalContentConnector'; import EditIndexerModalContentConnector from './EditIndexerModalContentConnector';
function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) { function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
size={sizes.MEDIUM}
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >

@ -96,7 +96,7 @@ function EditIndexerModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="enableAutomaticSearch" name="enableAutomaticSearch"
helpText={supportsSearch.value && 'Will be used when automatic searches are performed via the UI or by Lidarr'} helpText={supportsSearch.value ? 'Will be used when automatic searches are performed via the UI or by Lidarr' : undefined}
helpTextWarning={supportsSearch.value ? undefined : 'Search is not supported with this indexer'} helpTextWarning={supportsSearch.value ? undefined : 'Search is not supported with this indexer'}
isDisabled={!supportsSearch.value} isDisabled={!supportsSearch.value}
{...enableAutomaticSearch} {...enableAutomaticSearch}
@ -110,7 +110,7 @@ function EditIndexerModalContent(props) {
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="enableInteractiveSearch" name="enableInteractiveSearch"
helpText={supportsSearch.value && 'Will be used when interactive search is used'} helpText={supportsSearch.value ? 'Will be used when interactive search is used' : undefined}
helpTextWarning={supportsSearch.value ? undefined : 'Search is not supported with this indexer'} helpTextWarning={supportsSearch.value ? undefined : 'Search is not supported with this indexer'}
isDisabled={!supportsSearch.value} isDisabled={!supportsSearch.value}
{...enableInteractiveSearch} {...enableInteractiveSearch}

@ -1,15 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import { setIndexerValue, setIndexerFieldValue, saveIndexer, testIndexer } from 'Store/Actions/settingsActions'; import { setIndexerValue, setIndexerFieldValue, saveIndexer, testIndexer } from 'Store/Actions/settingsActions';
import connectSection from 'Store/connectSection';
import EditIndexerModalContent from './EditIndexerModalContent'; import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createProviderSettingsSelector(), createProviderSettingsSelector('indexers'),
(advancedSettings, indexer) => { (advancedSettings, indexer) => {
return { return {
advancedSettings, advancedSettings,
@ -85,10 +85,4 @@ EditIndexerModalContentConnector.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'indexers' }
)(EditIndexerModalContentConnector);

@ -59,9 +59,7 @@ class Indexers extends Component {
} = this.state; } = this.state;
return ( return (
<FieldSet <FieldSet legend="Indexers">
legend="Indexers"
>
<PageSectionContent <PageSectionContent
errorMessage="Unable to load Indexers" errorMessage="Unable to load Indexers"
{...otherProps} {...otherProps}

@ -19,9 +19,7 @@ function IndexerOptions(props) {
} = props; } = props;
return ( return (
<FieldSet <FieldSet legend="Options">
legend="Options"
>
{ {
isFetching && isFetching &&
<LoadingIndicator /> <LoadingIndicator />
@ -42,6 +40,7 @@ function IndexerOptions(props) {
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="minimumAge" name="minimumAge"
min={0} 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." 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} onChange={onInputChange}
{...settings.minimumAge} {...settings.minimumAge}
@ -55,6 +54,7 @@ function IndexerOptions(props) {
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="maximumSize" name="maximumSize"
min={0} min={0}
unit="MB"
helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited." helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited."
onChange={onInputChange} onChange={onInputChange}
{...settings.maximumSize} {...settings.maximumSize}
@ -68,6 +68,7 @@ function IndexerOptions(props) {
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="retention" name="retention"
min={0} min={0}
unit="days"
helpText="Usenet only: Set to zero to set for unlimited retention" helpText="Usenet only: Set to zero to set for unlimited retention"
onChange={onInputChange} onChange={onInputChange}
{...settings.retention} {...settings.retention}
@ -84,6 +85,7 @@ function IndexerOptions(props) {
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="rssSyncInterval" name="rssSyncInterval"
min={0} min={0}
unit="minutes"
helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)" 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" helpTextWarning="This will apply to all indexers, please follow the rules set forth by them"
helpLink="https://github.com/Lidarr/Lidarr/wiki/RSS-Sync" helpLink="https://github.com/Lidarr/Lidarr/wiki/RSS-Sync"

@ -1,16 +1,18 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { fetchIndexerOptions, setIndexerOptionsValue, saveIndexerOptions } from 'Store/Actions/settingsActions'; import { fetchIndexerOptions, setIndexerOptionsValue, saveIndexerOptions } from 'Store/Actions/settingsActions';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import connectSection from 'Store/connectSection';
import IndexerOptions from './IndexerOptions'; import IndexerOptions from './IndexerOptions';
const SECTION = 'indexerOptions';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.settings.advancedSettings, (state) => state.settings.advancedSettings,
createSettingsSectionSelector(), createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => { (advancedSettings, sectionSettings) => {
return { return {
advancedSettings, advancedSettings,
@ -62,7 +64,7 @@ class IndexerOptionsConnector extends Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: this.props.section }); this.props.dispatchClearPendingChanges({ section: SECTION });
} }
// //
@ -86,7 +88,6 @@ class IndexerOptionsConnector extends Component {
} }
IndexerOptionsConnector.propTypes = { IndexerOptionsConnector.propTypes = {
section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired, hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchIndexerOptions: PropTypes.func.isRequired, dispatchFetchIndexerOptions: PropTypes.func.isRequired,
@ -97,10 +98,4 @@ IndexerOptionsConnector.propTypes = {
onChildStateChange: PropTypes.func.isRequired onChildStateChange: PropTypes.func.isRequired
}; };
export default connectSection( export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector);
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'settings.indexerOptions' }
)(IndexerOptionsConnector);

@ -1,11 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector'; import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector';
function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) { function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
size={sizes.MEDIUM}
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >

@ -45,9 +45,7 @@ class Restrictions extends Component {
} = this.props; } = this.props;
return ( return (
<FieldSet <FieldSet legend="Restrictions">
legend="Restrictions"
>
<PageSectionContent <PageSectionContent
errorMessage="Unable to load Restrictions" errorMessage="Unable to load Restrictions"
{...otherProps} {...otherProps}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save