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
- build.sh
- test.sh
- package.json
- appveyor-package.sh

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

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

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

@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import BlacklistRow from './BlacklistRow';
function createMapStateToProps() {
@ -14,4 +15,12 @@ function createMapStateToProps() {
);
}
export default connect(createMapStateToProps)(BlacklistRow);
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeFromBlacklist({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -4,7 +4,7 @@ import { kinds, filterBuilderTypes } from 'Helpers/Props';
import TagInput, { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
const NAME = 'value';
export const NAME = 'value';
class FilterBuilderRowValue extends Component {
@ -91,7 +91,7 @@ class FilterBuilderRowValue extends Component {
}
FilterBuilderRowValue.propTypes = {
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired,
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number])).isRequired,
selectedFilterBuilderProp: PropTypes.object.isRequired,
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
onChange: PropTypes.func.isRequired

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

@ -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 {
position: relative;
flex: 1 1 auto;
}
.inputUnit {
position: absolute;
top: 0;
right: 20px;
margin-top: 7px;
width: 75px;
color: #c6c6c6;
text-align: right;
pointer-events: none;
user-select: none;
}
.inputUnitNumber {
composes: inputUnit;
right: 40px;
}
.pendingChangesContainer {
display: flex;
justify-content: flex-end;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,33 +1,50 @@
import * as filterTypes from './filterTypes';
export const ARRAY = 'array';
export const DATE = 'date';
export const EXACT = 'exact';
export const NUMBER = 'number';
export const STRING = 'string';
export const all = [
ARRAY,
DATE,
EXACT,
NUMBER,
STRING
];
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]: [
{ key: filterTypes.EQUAL, value: 'Is' },
{ key: filterTypes.NOT_EQUAL, value: 'Is Not' }
{ key: filterTypes.EQUAL, value: 'is' },
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
],
[NUMBER]: [
{ key: filterTypes.EQUAL, value: 'Equal' },
{ key: filterTypes.GREATER_THAN, value: 'Greater Than' },
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'Greater Than or Equal' },
{ key: filterTypes.LESS_THAN, value: 'Less Than' },
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'Less Than or Equal' },
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
{ key: filterTypes.EQUAL, value: 'equal' },
{ key: filterTypes.GREATER_THAN, value: 'greater than' },
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'greater than or equal' },
{ key: filterTypes.LESS_THAN, value: 'less than' },
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'less than or equal' },
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
],
[STRING]: [
{ key: filterTypes.CONTAINS, value: 'Contains' },
{ key: filterTypes.EQUAL, value: 'Equal' },
{ key: filterTypes.NOT_EQUAL, value: 'Not Equal' }
{ key: filterTypes.CONTAINS, value: 'contains' },
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
{ 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 INDEXER = 'indexer';
export const LANGUAGE_PROFILE = 'languageProfile';
export const METADATA_PROFILE = 'metadataProfile';
export const PROTOCOL = 'protocol';
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 GREATER_THAN = 'greaterThan';
export const GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual';
export const IN_LAST = 'inLast';
export const IN_NEXT = 'inNext';
export const LESS_THAN = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual';
export const all = [
@ -13,5 +16,6 @@ export const all = [
GREATER_THAN_OR_EQUAL,
LESS_THAN,
LESS_THAN_OR_EQUAL,
NOT_CONTAINS,
NOT_EQUAL
];

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

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

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

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

@ -1,7 +1,23 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds, sizes } from 'Helpers/Props';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import styles from './SelectAlbumRow.css';
function getTrackCountKind(monitored, trackFileCount, trackCount) {
if (trackFileCount === trackCount && trackCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class SelectAlbumRow extends Component {
@ -16,14 +32,87 @@ class SelectAlbumRow extends Component {
// Render
render() {
const {
title,
albumType,
releaseDate,
statistics,
monitored,
columns
} = this.props;
const {
trackCount,
trackFileCount,
totalTrackCount
} = statistics;
return (
<Link
className={styles.season}
component="div"
onPress={this.onPress}
>
{this.props.title} ({this.props.albumType})
</Link>
<TableRow>
{
columns.map((column) => {
const {
name,
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,
title: PropTypes.string.isRequired,
albumType: PropTypes.string.isRequired,
onAlbumSelect: PropTypes.func.isRequired
releaseDate: PropTypes.string.isRequired,
onAlbumSelect: PropTypes.func.isRequired,
statistics: PropTypes.object.isRequired,
monitored: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired
};
SelectAlbumRow.defaultProps = {
statistics: {
trackCount: 0,
trackFileCount: 0
}
};
export default SelectAlbumRow;

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

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

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

@ -1,8 +1,8 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import connectSection from 'Store/connectSection';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions';
@ -11,7 +11,7 @@ import InteractiveImportModalContent from './InteractiveImportModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector(),
createClientSideCollectionSelector('interactiveImport'),
(interactiveImport) => {
return interactiveImport;
}
@ -125,8 +125,19 @@ class InteractiveImportModalContentConnector extends Component {
return false;
}
if (!quality) {
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
return false;
}
if (!language) {
this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' });
return false;
}
files.push({
path: item.path,
folderName: item.folderName,
artistId: artist.id,
albumId: album.id,
trackIds: _.map(tracks, 'id'),
@ -190,10 +201,4 @@ InteractiveImportModalContentConnector.defaultProps = {
filterExistingFiles: true
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
undefined,
{ section: 'interactiveImport' }
)(InteractiveImportModalContentConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);

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

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

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

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

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

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

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

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

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

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

@ -49,7 +49,8 @@ function BackupSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="backupInterval"
helpText="Interval in days"
unit="days"
helpText="Interval to backup the Lidarr DB and settings"
onChange={onInputChange}
{...backupInterval}
/>
@ -64,7 +65,8 @@ function BackupSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
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}
{...backupRetention}
/>

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

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

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

@ -96,7 +96,7 @@ function EditIndexerModalContent(props) {
<FormInputGroup
type={inputTypes.CHECK}
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'}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
@ -110,7 +110,7 @@ function EditIndexerModalContent(props) {
<FormInputGroup
type={inputTypes.CHECK}
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'}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}

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

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

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

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

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

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

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

Loading…
Cancel
Save