mirror of https://github.com/lidarr/Lidarr
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
This commit is contained in:
parent
afa78b1d20
commit
6581b3a2c5
|
@ -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);
|
||||
const handler = this[getHandlerName(name)];
|
||||
|
||||
if (handler) {
|
||||
handler(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'command') {
|
||||
this.handleCommand(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'album') {
|
||||
this.handleAlbum(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'track') {
|
||||
this.handleTrack(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…
Reference in New Issue