parent
1cc434a498
commit
be4e748977
@ -1,166 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
|
||||||
import ImportArtistTableConnector from './ImportArtistTableConnector';
|
|
||||||
import ImportArtistFooterConnector from './ImportArtistFooterConnector';
|
|
||||||
|
|
||||||
class ImportArtist extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {},
|
|
||||||
scroller: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
setScrollerRef = (ref) => {
|
|
||||||
this.setState({ scroller: ref });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
// Only select non-dupes
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemoveSelectedStateItem = (id) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
const selectedState = Object.assign({}, state.selectedState);
|
|
||||||
delete selectedState[id];
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
selectedState
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.onInputChange(this.getSelectedIds(), name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onImportPress = () => {
|
|
||||||
this.props.onImportPress(this.getSelectedIds());
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
rootFolderId,
|
|
||||||
path,
|
|
||||||
rootFoldersFetching,
|
|
||||||
rootFoldersPopulated,
|
|
||||||
rootFoldersError,
|
|
||||||
unmappedFolders,
|
|
||||||
showMetadataProfile
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
selectedState,
|
|
||||||
scroller
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title="Import Artist">
|
|
||||||
<PageContentBodyConnector
|
|
||||||
registerScroller={this.setScrollerRef}
|
|
||||||
onScroll={this.onScroll}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
rootFoldersFetching && !rootFoldersPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!rootFoldersFetching && !!rootFoldersError &&
|
|
||||||
<div>Unable to load root folders</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!rootFoldersError && rootFoldersPopulated && !unmappedFolders.length &&
|
|
||||||
<div>
|
|
||||||
All artist in {path} have been imported
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
|
||||||
<ImportArtistTableConnector
|
|
||||||
rootFolderId={rootFolderId}
|
|
||||||
unmappedFolders={unmappedFolders}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
selectedState={selectedState}
|
|
||||||
scroller={scroller}
|
|
||||||
showMetadataProfile={showMetadataProfile}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</PageContentBodyConnector>
|
|
||||||
|
|
||||||
{
|
|
||||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
|
||||||
<ImportArtistFooterConnector
|
|
||||||
selectedIds={this.getSelectedIds()}
|
|
||||||
showMetadataProfile={showMetadataProfile}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onImportPress={this.onImportPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtist.propTypes = {
|
|
||||||
rootFolderId: PropTypes.number.isRequired,
|
|
||||||
path: PropTypes.string,
|
|
||||||
rootFoldersFetching: PropTypes.bool.isRequired,
|
|
||||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
|
||||||
rootFoldersError: PropTypes.object,
|
|
||||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onImportPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ImportArtist.defaultProps = {
|
|
||||||
unmappedFolders: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtist;
|
|
@ -1,170 +0,0 @@
|
|||||||
/* eslint max-params: 0 */
|
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setImportArtistValue, importArtist, clearImportArtist } from 'Store/Actions/importArtistActions';
|
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
|
||||||
import { setAddDefault } from 'Store/Actions/searchActions';
|
|
||||||
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
|
||||||
import ImportArtist from './ImportArtist';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { match }) => match,
|
|
||||||
(state) => state.rootFolders,
|
|
||||||
(state) => state.addArtist,
|
|
||||||
(state) => state.importArtist,
|
|
||||||
(state) => state.settings.qualityProfiles,
|
|
||||||
(state) => state.settings.metadataProfiles,
|
|
||||||
(
|
|
||||||
match,
|
|
||||||
rootFolders,
|
|
||||||
addArtist,
|
|
||||||
importArtistState,
|
|
||||||
qualityProfiles,
|
|
||||||
metadataProfiles
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
isFetching: rootFoldersFetching,
|
|
||||||
isPopulated: rootFoldersPopulated,
|
|
||||||
error: rootFoldersError,
|
|
||||||
items
|
|
||||||
} = rootFolders;
|
|
||||||
|
|
||||||
const rootFolderId = parseInt(match.params.rootFolderId);
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
rootFolderId,
|
|
||||||
rootFoldersFetching,
|
|
||||||
rootFoldersPopulated,
|
|
||||||
rootFoldersError,
|
|
||||||
qualityProfiles: qualityProfiles.items,
|
|
||||||
metadataProfiles: metadataProfiles.items,
|
|
||||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
|
||||||
defaultQualityProfileId: addArtist.defaults.qualityProfileId,
|
|
||||||
defaultMetadataProfileId: addArtist.defaults.metadataProfileId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (items.length) {
|
|
||||||
const rootFolder = _.find(items, { id: rootFolderId });
|
|
||||||
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
...rootFolder,
|
|
||||||
items: importArtistState.items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetImportArtistValue: setImportArtistValue,
|
|
||||||
dispatchImportArtist: importArtist,
|
|
||||||
dispatchClearImportArtist: clearImportArtist,
|
|
||||||
dispatchFetchRootFolders: fetchRootFolders,
|
|
||||||
dispatchSetAddDefault: setAddDefault
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImportArtistConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
qualityProfiles,
|
|
||||||
metadataProfiles,
|
|
||||||
defaultQualityProfileId,
|
|
||||||
defaultMetadataProfileId,
|
|
||||||
dispatchFetchRootFolders,
|
|
||||||
dispatchSetAddDefault
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!this.props.rootFoldersPopulated) {
|
|
||||||
dispatchFetchRootFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
let setDefaults = false;
|
|
||||||
const setDefaultPayload = {};
|
|
||||||
|
|
||||||
if (
|
|
||||||
!defaultQualityProfileId ||
|
|
||||||
!qualityProfiles.some((p) => p.id === defaultQualityProfileId)
|
|
||||||
) {
|
|
||||||
setDefaults = true;
|
|
||||||
setDefaultPayload.qualityProfileId = qualityProfiles[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!defaultMetadataProfileId ||
|
|
||||||
!metadataProfiles.some((p) => p.id === defaultMetadataProfileId)
|
|
||||||
) {
|
|
||||||
setDefaults = true;
|
|
||||||
setDefaultPayload.metadataProfileId = metadataProfiles[0].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (setDefaults) {
|
|
||||||
dispatchSetAddDefault(setDefaultPayload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.dispatchClearImportArtist();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = (ids, name, value) => {
|
|
||||||
this.props.dispatchSetAddDefault({ [name]: value });
|
|
||||||
|
|
||||||
ids.forEach((id) => {
|
|
||||||
this.props.dispatchSetImportArtistValue({
|
|
||||||
id,
|
|
||||||
[name]: value
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onImportPress = (ids) => {
|
|
||||||
this.props.dispatchImportArtist({ ids });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ImportArtist
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onImportPress={this.onImportPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const routeMatchShape = createRouteMatchShape({
|
|
||||||
rootFolderId: PropTypes.string.isRequired
|
|
||||||
});
|
|
||||||
|
|
||||||
ImportArtistConnector.propTypes = {
|
|
||||||
match: routeMatchShape.isRequired,
|
|
||||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
|
||||||
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
metadataProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
defaultQualityProfileId: PropTypes.number.isRequired,
|
|
||||||
defaultMetadataProfileId: PropTypes.number.isRequired,
|
|
||||||
dispatchSetImportArtistValue: PropTypes.func.isRequired,
|
|
||||||
dispatchImportArtist: PropTypes.func.isRequired,
|
|
||||||
dispatchClearImportArtist: PropTypes.func.isRequired,
|
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
|
||||||
dispatchSetAddDefault: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistConnector);
|
|
@ -1,33 +0,0 @@
|
|||||||
.inputContainer {
|
|
||||||
margin-right: 20px;
|
|
||||||
min-width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-bottom: 3px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importButtonContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importButton {
|
|
||||||
composes: button from '~Components/Link/SpinnerButton.css';
|
|
||||||
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingButton {
|
|
||||||
composes: importButton;
|
|
||||||
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
|
||||||
|
|
||||||
margin: 0 10px 0 12px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
@ -1,261 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
|
||||||
import styles from './ImportArtistFooter.css';
|
|
||||||
|
|
||||||
const MIXED = 'mixed';
|
|
||||||
|
|
||||||
class ImportArtistFooter extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const {
|
|
||||||
defaultMonitor,
|
|
||||||
defaultQualityProfileId,
|
|
||||||
defaultMetadataProfileId,
|
|
||||||
defaultAlbumFolder
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
monitor: defaultMonitor,
|
|
||||||
qualityProfileId: defaultQualityProfileId,
|
|
||||||
metadataProfileId: defaultMetadataProfileId,
|
|
||||||
albumFolder: defaultAlbumFolder
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
defaultMonitor,
|
|
||||||
defaultQualityProfileId,
|
|
||||||
defaultMetadataProfileId,
|
|
||||||
defaultAlbumFolder,
|
|
||||||
isMonitorMixed,
|
|
||||||
isQualityProfileIdMixed,
|
|
||||||
isMetadataProfileIdMixed,
|
|
||||||
isAlbumFolderMixed
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
metadataProfileId,
|
|
||||||
albumFolder
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const newState = {};
|
|
||||||
|
|
||||||
if (isMonitorMixed && monitor !== MIXED) {
|
|
||||||
newState.monitor = MIXED;
|
|
||||||
} else if (!isMonitorMixed && monitor !== defaultMonitor) {
|
|
||||||
newState.monitor = defaultMonitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
|
|
||||||
newState.qualityProfileId = MIXED;
|
|
||||||
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
|
|
||||||
newState.qualityProfileId = defaultQualityProfileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMetadataProfileIdMixed && metadataProfileId !== MIXED) {
|
|
||||||
newState.metadataProfileId = MIXED;
|
|
||||||
} else if (!isMetadataProfileIdMixed && metadataProfileId !== defaultMetadataProfileId) {
|
|
||||||
newState.metadataProfileId = defaultMetadataProfileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAlbumFolderMixed && albumFolder != null) {
|
|
||||||
newState.albumFolder = null;
|
|
||||||
} else if (!isAlbumFolderMixed && albumFolder !== defaultAlbumFolder) {
|
|
||||||
newState.albumFolder = defaultAlbumFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(newState)) {
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.setState({ [name]: value });
|
|
||||||
this.props.onInputChange({ name, value });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedCount,
|
|
||||||
isImporting,
|
|
||||||
isLookingUpArtist,
|
|
||||||
isMonitorMixed,
|
|
||||||
isQualityProfileIdMixed,
|
|
||||||
isMetadataProfileIdMixed,
|
|
||||||
hasUnsearchedItems,
|
|
||||||
showMetadataProfile,
|
|
||||||
onImportPress,
|
|
||||||
onLookupPress,
|
|
||||||
onCancelLookupPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
metadataProfileId,
|
|
||||||
albumFolder
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContentFooter>
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
Monitor
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.MONITOR_ALBUMS_SELECT}
|
|
||||||
name="monitor"
|
|
||||||
value={monitor}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
includeMixed={isMonitorMixed}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
Quality Profile
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
|
||||||
name="qualityProfileId"
|
|
||||||
value={qualityProfileId}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
includeMixed={isQualityProfileIdMixed}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
showMetadataProfile &&
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
Metadata Profile
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
|
||||||
name="metadataProfileId"
|
|
||||||
value={metadataProfileId}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
includeMixed={isMetadataProfileIdMixed}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
Album Folder
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CheckInput
|
|
||||||
name="albumFolder"
|
|
||||||
value={albumFolder}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className={styles.label}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.importButtonContainer}>
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.importButton}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={isImporting}
|
|
||||||
isDisabled={!selectedCount || isLookingUpArtist}
|
|
||||||
onPress={onImportPress}
|
|
||||||
>
|
|
||||||
Import {selectedCount} Artist(s)
|
|
||||||
</SpinnerButton>
|
|
||||||
|
|
||||||
{
|
|
||||||
isLookingUpArtist &&
|
|
||||||
<Button
|
|
||||||
className={styles.loadingButton}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
onPress={onCancelLookupPress}
|
|
||||||
>
|
|
||||||
Cancel Processing
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
hasUnsearchedItems &&
|
|
||||||
<Button
|
|
||||||
className={styles.loadingButton}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
onPress={onLookupPress}
|
|
||||||
>
|
|
||||||
Start Processing
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isLookingUpArtist &&
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isLookingUpArtist &&
|
|
||||||
'Processing Folders'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageContentFooter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistFooter.propTypes = {
|
|
||||||
selectedCount: PropTypes.number.isRequired,
|
|
||||||
isImporting: PropTypes.bool.isRequired,
|
|
||||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
|
||||||
defaultMonitor: PropTypes.string.isRequired,
|
|
||||||
defaultQualityProfileId: PropTypes.number,
|
|
||||||
defaultMetadataProfileId: PropTypes.number,
|
|
||||||
defaultAlbumFolder: PropTypes.bool.isRequired,
|
|
||||||
isMonitorMixed: PropTypes.bool.isRequired,
|
|
||||||
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
|
||||||
isMetadataProfileIdMixed: PropTypes.bool.isRequired,
|
|
||||||
isAlbumFolderMixed: PropTypes.bool.isRequired,
|
|
||||||
hasUnsearchedItems: PropTypes.bool.isRequired,
|
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onImportPress: PropTypes.func.isRequired,
|
|
||||||
onLookupPress: PropTypes.func.isRequired,
|
|
||||||
onCancelLookupPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistFooter;
|
|
@ -1,61 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import ImportArtistFooter from './ImportArtistFooter';
|
|
||||||
import { lookupUnsearchedArtist, cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
|
||||||
|
|
||||||
function isMixed(items, selectedIds, defaultValue, key) {
|
|
||||||
return _.some(items, (artist) => {
|
|
||||||
return selectedIds.indexOf(artist.id) > -1 && artist[key] !== defaultValue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.addArtist,
|
|
||||||
(state) => state.importArtist,
|
|
||||||
(state, { selectedIds }) => selectedIds,
|
|
||||||
(addArtist, importArtist, selectedIds) => {
|
|
||||||
const {
|
|
||||||
monitor: defaultMonitor,
|
|
||||||
qualityProfileId: defaultQualityProfileId,
|
|
||||||
metadataProfileId: defaultMetadataProfileId,
|
|
||||||
albumFolder: defaultAlbumFolder
|
|
||||||
} = addArtist.defaults;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLookingUpArtist,
|
|
||||||
isImporting,
|
|
||||||
items
|
|
||||||
} = importArtist;
|
|
||||||
|
|
||||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
|
||||||
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
|
||||||
const isMetadataProfileIdMixed = isMixed(items, selectedIds, defaultMetadataProfileId, 'metadataProfileId');
|
|
||||||
const isAlbumFolderMixed = isMixed(items, selectedIds, defaultAlbumFolder, 'albumFolder');
|
|
||||||
const hasUnsearchedItems = !isLookingUpArtist && items.some((item) => !item.isPopulated);
|
|
||||||
|
|
||||||
return {
|
|
||||||
selectedCount: selectedIds.length,
|
|
||||||
isLookingUpArtist,
|
|
||||||
isImporting,
|
|
||||||
defaultMonitor,
|
|
||||||
defaultQualityProfileId,
|
|
||||||
defaultMetadataProfileId,
|
|
||||||
defaultAlbumFolder,
|
|
||||||
isMonitorMixed,
|
|
||||||
isQualityProfileIdMixed,
|
|
||||||
isMetadataProfileIdMixed,
|
|
||||||
isAlbumFolderMixed,
|
|
||||||
hasUnsearchedItems
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
onLookupPress: lookupUnsearchedArtist,
|
|
||||||
onCancelLookupPress: cancelLookupArtist
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistFooter);
|
|
@ -1,38 +0,0 @@
|
|||||||
.folder {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 1 0 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitor {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 200px;
|
|
||||||
min-width: 185px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qualityProfile,
|
|
||||||
.metadataProfile {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 250px;
|
|
||||||
min-width: 170px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.albumFolder {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 150px;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.artist {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 400px;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsIcon {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
|
||||||
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
|
|
||||||
// import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent';
|
|
||||||
import styles from './ImportArtistHeader.css';
|
|
||||||
|
|
||||||
function ImportArtistHeader(props) {
|
|
||||||
const {
|
|
||||||
showMetadataProfile,
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
onSelectAllChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeader>
|
|
||||||
<VirtualTableSelectAllHeaderCell
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.folder}
|
|
||||||
name="folder"
|
|
||||||
>
|
|
||||||
Folder
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.monitor}
|
|
||||||
name="monitor"
|
|
||||||
>
|
|
||||||
Monitor
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
className={styles.detailsIcon}
|
|
||||||
name={icons.INFO}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Monitoring Options"
|
|
||||||
body={<ArtistMonitoringOptionsPopoverContent />}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.qualityProfile}
|
|
||||||
name="qualityProfileId"
|
|
||||||
>
|
|
||||||
Quality Profile
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
|
|
||||||
{
|
|
||||||
showMetadataProfile &&
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.metadataProfile}
|
|
||||||
name="metadataProfileId"
|
|
||||||
>
|
|
||||||
Metadata Profile
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
}
|
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.albumFolder}
|
|
||||||
name="albumFolder"
|
|
||||||
>
|
|
||||||
Album Folder
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
className={styles.artist}
|
|
||||||
name="artist"
|
|
||||||
>
|
|
||||||
Artist
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
</VirtualTableHeader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistHeader.propTypes = {
|
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistHeader;
|
|
@ -1,45 +0,0 @@
|
|||||||
.selectInput {
|
|
||||||
composes: input from '~Components/Form/CheckInput.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.folder {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
flex: 1 0 200px;
|
|
||||||
line-height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitor {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 200px;
|
|
||||||
min-width: 185px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qualityProfile,
|
|
||||||
.metadataProfile {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 250px;
|
|
||||||
min-width: 170px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.albumFolder {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 150px;
|
|
||||||
min-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.artist {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
flex: 0 1 400px;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hideMetadataProfile {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
display: none;
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
|
||||||
import ImportArtistSelectArtistConnector from './SelectArtist/ImportArtistSelectArtistConnector';
|
|
||||||
import styles from './ImportArtistRow.css';
|
|
||||||
|
|
||||||
function ImportArtistRow(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
metadataProfileId,
|
|
||||||
albumFolder,
|
|
||||||
selectedArtist,
|
|
||||||
isExistingArtist,
|
|
||||||
showMetadataProfile,
|
|
||||||
isSelected,
|
|
||||||
onSelectedChange,
|
|
||||||
onInputChange
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<VirtualTableSelectCell
|
|
||||||
inputClassName={styles.selectInput}
|
|
||||||
id={id}
|
|
||||||
isSelected={isSelected}
|
|
||||||
isDisabled={!selectedArtist || isExistingArtist}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.folder}>
|
|
||||||
{id}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.monitor}>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.MONITOR_ALBUMS_SELECT}
|
|
||||||
name="monitor"
|
|
||||||
value={monitor}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.qualityProfile}>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
|
||||||
name="qualityProfileId"
|
|
||||||
value={qualityProfileId}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell
|
|
||||||
className={showMetadataProfile ? styles.metadataProfile : styles.hideMetadataProfile}
|
|
||||||
>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.METADATA_PROFILE_SELECT}
|
|
||||||
name="metadataProfileId"
|
|
||||||
value={metadataProfileId}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.albumFolder}>
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="albumFolder"
|
|
||||||
value={albumFolder}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.artist}>
|
|
||||||
<ImportArtistSelectArtistConnector
|
|
||||||
id={id}
|
|
||||||
isExistingArtist={isExistingArtist}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistRow.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
monitor: PropTypes.string.isRequired,
|
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
|
||||||
metadataProfileId: PropTypes.number.isRequired,
|
|
||||||
albumFolder: PropTypes.bool.isRequired,
|
|
||||||
selectedArtist: PropTypes.object,
|
|
||||||
isExistingArtist: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ImportArtistRow.defaultsProps = {
|
|
||||||
items: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistRow;
|
|
@ -1,87 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setImportArtistValue } from 'Store/Actions/importArtistActions';
|
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
|
||||||
import ImportArtistRow from './ImportArtistRow';
|
|
||||||
|
|
||||||
function createImportArtistItemSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.importArtist.items,
|
|
||||||
(id, items) => {
|
|
||||||
return _.find(items, { id }) || {};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createImportArtistItemSelector(),
|
|
||||||
createAllArtistSelector(),
|
|
||||||
(item, artist) => {
|
|
||||||
const selectedArtist = item && item.selectedArtist;
|
|
||||||
const isExistingArtist = !!selectedArtist && _.some(artist, { foreignArtistId: selectedArtist.foreignArtistId });
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
isExistingArtist
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setImportArtistValue
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImportArtistRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setImportArtistValue({
|
|
||||||
id: this.props.id,
|
|
||||||
[name]: value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
// Don't show the row until we have the information we require for it.
|
|
||||||
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
monitor,
|
|
||||||
albumFolder
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!items || !monitor || !albumFolder == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ImportArtistRow
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onArtistSelect={this.onArtistSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistRowConnector.propTypes = {
|
|
||||||
rootFolderId: PropTypes.number.isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
monitor: PropTypes.string,
|
|
||||||
albumFolder: PropTypes.bool,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
setImportArtistValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistRowConnector);
|
|
@ -1,3 +0,0 @@
|
|||||||
.input {
|
|
||||||
composes: input from '~Components/Form/CheckInput.css';
|
|
||||||
}
|
|
@ -1,195 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
|
||||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
|
||||||
import ImportArtistHeader from './ImportArtistHeader';
|
|
||||||
import ImportArtistRowConnector from './ImportArtistRowConnector';
|
|
||||||
|
|
||||||
class ImportArtistTable extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
unmappedFolders,
|
|
||||||
defaultMonitor,
|
|
||||||
defaultQualityProfileId,
|
|
||||||
defaultMetadataProfileId,
|
|
||||||
defaultAlbumFolder,
|
|
||||||
onArtistLookup,
|
|
||||||
onSetImportArtistValue
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const values = {
|
|
||||||
monitor: defaultMonitor,
|
|
||||||
qualityProfileId: defaultQualityProfileId,
|
|
||||||
metadataProfileId: defaultMetadataProfileId,
|
|
||||||
albumFolder: defaultAlbumFolder
|
|
||||||
};
|
|
||||||
|
|
||||||
unmappedFolders.forEach((unmappedFolder) => {
|
|
||||||
const id = unmappedFolder.name;
|
|
||||||
|
|
||||||
onArtistLookup(id, unmappedFolder.path);
|
|
||||||
|
|
||||||
onSetImportArtistValue({
|
|
||||||
id,
|
|
||||||
...values
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This isn't great, but it's the most reliable way to ensure the items
|
|
||||||
// are checked off even if they aren't actually visible since the cells
|
|
||||||
// are virtualized.
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
selectedState,
|
|
||||||
onSelectedChange,
|
|
||||||
onRemoveSelectedStateItem
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
prevProps.items.forEach((prevItem) => {
|
|
||||||
const {
|
|
||||||
id
|
|
||||||
} = prevItem;
|
|
||||||
|
|
||||||
const item = _.find(items, { id });
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
onRemoveSelectedStateItem(id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedArtist = item.selectedArtist;
|
|
||||||
const isSelected = selectedState[id];
|
|
||||||
|
|
||||||
const isExistingArtist = !!selectedArtist &&
|
|
||||||
_.some(prevProps.allArtists, { foreignArtistId: selectedArtist.foreignArtistId });
|
|
||||||
|
|
||||||
// Props doesn't have a selected artist or
|
|
||||||
// the selected artist is an existing artist.
|
|
||||||
if ((!selectedArtist && prevItem.selectedArtist) || (isExistingArtist && !prevItem.selectedArtist)) {
|
|
||||||
onSelectedChange({ id, value: false });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// State is selected, but a artist isn't selected or
|
|
||||||
// the selected artist is an existing artist.
|
|
||||||
if (isSelected && (!selectedArtist || isExistingArtist)) {
|
|
||||||
onSelectedChange({ id, value: false });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A artist is being selected that wasn't previously selected.
|
|
||||||
if (selectedArtist && selectedArtist !== prevItem.selectedArtist) {
|
|
||||||
onSelectedChange({ id, value: true });
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
rowRenderer = ({ key, rowIndex, style }) => {
|
|
||||||
const {
|
|
||||||
rootFolderId,
|
|
||||||
items,
|
|
||||||
selectedState,
|
|
||||||
showMetadataProfile,
|
|
||||||
onSelectedChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const item = items[rowIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableRow
|
|
||||||
key={key}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<ImportArtistRowConnector
|
|
||||||
key={item.id}
|
|
||||||
style={style}
|
|
||||||
rootFolderId={rootFolderId}
|
|
||||||
showMetadataProfile={showMetadataProfile}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
id={item.id}
|
|
||||||
/>
|
|
||||||
</VirtualTableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
isSmallScreen,
|
|
||||||
showMetadataProfile,
|
|
||||||
scroller,
|
|
||||||
selectedState,
|
|
||||||
onSelectAllChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTable
|
|
||||||
items={items}
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
scroller={scroller}
|
|
||||||
rowHeight={52}
|
|
||||||
overscanRowCount={2}
|
|
||||||
rowRenderer={this.rowRenderer}
|
|
||||||
header={
|
|
||||||
<ImportArtistHeader
|
|
||||||
showMetadataProfile={showMetadataProfile}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
selectedState={selectedState}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistTable.propTypes = {
|
|
||||||
rootFolderId: PropTypes.number.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
defaultMonitor: PropTypes.string.isRequired,
|
|
||||||
defaultQualityProfileId: PropTypes.number,
|
|
||||||
defaultMetadataProfileId: PropTypes.number,
|
|
||||||
defaultAlbumFolder: PropTypes.bool.isRequired,
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
selectedState: PropTypes.object.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
allArtists: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
|
||||||
onArtistLookup: PropTypes.func.isRequired,
|
|
||||||
onSetImportArtistValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistTable;
|
|
@ -1,43 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions';
|
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
|
||||||
import ImportArtistTable from './ImportArtistTable';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.addArtist,
|
|
||||||
(state) => state.importArtist,
|
|
||||||
(state) => state.app.dimensions,
|
|
||||||
createAllArtistSelector(),
|
|
||||||
(addArtist, importArtist, dimensions, allArtists) => {
|
|
||||||
return {
|
|
||||||
defaultMonitor: addArtist.defaults.monitor,
|
|
||||||
defaultQualityProfileId: addArtist.defaults.qualityProfileId,
|
|
||||||
defaultMetadataProfileId: addArtist.defaults.metadataProfileId,
|
|
||||||
defaultAlbumFolder: addArtist.defaults.albumFolder,
|
|
||||||
items: importArtist.items,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
allArtists
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onArtistLookup(name, path) {
|
|
||||||
dispatch(queueLookupArtist({
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
term: name
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSetImportArtistValue(values) {
|
|
||||||
dispatch(setImportArtistValue(values));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ImportArtistTable);
|
|
@ -1,19 +0,0 @@
|
|||||||
.artistNameContainer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 0 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.artistName {
|
|
||||||
@add-mixin truncate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.disambiguation {
|
|
||||||
margin-right: 5px;
|
|
||||||
color: $disabledColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.existing {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import styles from './ImportArtistName.css';
|
|
||||||
|
|
||||||
function ImportArtistName(props) {
|
|
||||||
const {
|
|
||||||
artistName,
|
|
||||||
disambiguation,
|
|
||||||
isExistingArtist
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.artistNameContainer}>
|
|
||||||
<div className={styles.artistName}>
|
|
||||||
{artistName}
|
|
||||||
</div>
|
|
||||||
<div className={styles.disambiguation}>
|
|
||||||
{disambiguation}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isExistingArtist &&
|
|
||||||
<Label
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
Existing
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistName.propTypes = {
|
|
||||||
artistName: PropTypes.string.isRequired,
|
|
||||||
disambiguation: PropTypes.string,
|
|
||||||
isExistingArtist: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistName;
|
|
@ -1,8 +0,0 @@
|
|||||||
.artist {
|
|
||||||
padding: 10px 20px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $menuItemHoverBackgroundColor;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import ImportArtistName from './ImportArtistName';
|
|
||||||
import styles from './ImportArtistSearchResult.css';
|
|
||||||
|
|
||||||
class ImportArtistSearchResult extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.props.onPress(this.props.foreignArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
artistName,
|
|
||||||
disambiguation,
|
|
||||||
// year,
|
|
||||||
isExistingArtist
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={styles.artist}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
<ImportArtistName
|
|
||||||
artistName={artistName}
|
|
||||||
disambiguation={disambiguation}
|
|
||||||
// year={year}
|
|
||||||
isExistingArtist={isExistingArtist}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistSearchResult.propTypes = {
|
|
||||||
foreignArtistId: PropTypes.string.isRequired,
|
|
||||||
artistName: PropTypes.string.isRequired,
|
|
||||||
disambiguation: PropTypes.string,
|
|
||||||
// year: PropTypes.number.isRequired,
|
|
||||||
isExistingArtist: PropTypes.bool.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistSearchResult;
|
|
@ -1,17 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createExistingArtistSelector from 'Store/Selectors/createExistingArtistSelector';
|
|
||||||
import ImportArtistSearchResult from './ImportArtistSearchResult';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createExistingArtistSelector(),
|
|
||||||
(isExistingArtist) => {
|
|
||||||
return {
|
|
||||||
isExistingArtist
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ImportArtistSearchResult);
|
|
@ -1,77 +0,0 @@
|
|||||||
.button {
|
|
||||||
composes: link from '~Components/Link/Link.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 6px 16px;
|
|
||||||
width: 100%;
|
|
||||||
height: 35px;
|
|
||||||
border: 1px solid $inputBorderColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $white;
|
|
||||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.warningIcon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.existing {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
margin-left: 5px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentContainer {
|
|
||||||
z-index: $popperZIndex;
|
|
||||||
margin-top: 4px;
|
|
||||||
/* 400px container witdh with 8px padding on each side */
|
|
||||||
width: 384px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 4px;
|
|
||||||
border: 1px solid $inputBorderColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchContainer {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchIconContainer {
|
|
||||||
width: 58px;
|
|
||||||
border: 1px solid $inputBorderColor;
|
|
||||||
border-right: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
background-color: #edf1f2;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 33px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchInput {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results {
|
|
||||||
@add-mixin scrollbar;
|
|
||||||
@add-mixin scrollbarTrack;
|
|
||||||
@add-mixin scrollbarThumb;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
max-height: 165px;
|
|
||||||
}
|
|
@ -1,303 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Portal from 'Components/Portal';
|
|
||||||
import FormInputButton from 'Components/Form/FormInputButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import ImportArtistSearchResultConnector from './ImportArtistSearchResultConnector';
|
|
||||||
import ImportArtistName from './ImportArtistName';
|
|
||||||
import styles from './ImportArtistSelectArtist.css';
|
|
||||||
|
|
||||||
class ImportArtistSelectArtist extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._artistLookupTimeout = null;
|
|
||||||
this._scheduleUpdate = null;
|
|
||||||
this._buttonId = getUniqueElememtId();
|
|
||||||
this._contentId = getUniqueElememtId();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
term: props.id,
|
|
||||||
isOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (this._scheduleUpdate) {
|
|
||||||
this._scheduleUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_addListener() {
|
|
||||||
window.addEventListener('click', this.onWindowClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeListener() {
|
|
||||||
window.removeEventListener('click', this.onWindowClick);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onWindowClick = (event) => {
|
|
||||||
const button = document.getElementById(this._buttonId);
|
|
||||||
const content = document.getElementById(this._contentId);
|
|
||||||
|
|
||||||
if (!button || !content) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!button.contains(event.target) &&
|
|
||||||
!content.contains(event.target) &&
|
|
||||||
this.state.isOpen
|
|
||||||
) {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
this._removeListener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
if (this.state.isOpen) {
|
|
||||||
this._removeListener();
|
|
||||||
} else {
|
|
||||||
this._addListener();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearchInputChange = ({ value }) => {
|
|
||||||
if (this._artistLookupTimeout) {
|
|
||||||
clearTimeout(this._artistLookupTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ term: value }, () => {
|
|
||||||
this._artistLookupTimeout = setTimeout(() => {
|
|
||||||
this.props.onSearchInputChange(value);
|
|
||||||
}, 200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onRefreshPress = () => {
|
|
||||||
this.props.onSearchInputChange(this.state.term);
|
|
||||||
}
|
|
||||||
|
|
||||||
onArtistSelect = (foreignArtistId) => {
|
|
||||||
this.setState({ isOpen: false });
|
|
||||||
|
|
||||||
this.props.onArtistSelect(foreignArtistId);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedArtist,
|
|
||||||
isExistingArtist,
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
isQueued,
|
|
||||||
isLookingUpArtist
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const errorMessage = error &&
|
|
||||||
error.responseJSON &&
|
|
||||||
error.responseJSON.message;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Manager>
|
|
||||||
<Reference>
|
|
||||||
{({ ref }) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
id={this._buttonId}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
ref={ref}
|
|
||||||
className={styles.button}
|
|
||||||
component="div"
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isLookingUpArtist && isQueued && !isPopulated ?
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && selectedArtist && isExistingArtist ?
|
|
||||||
<Icon
|
|
||||||
className={styles.warningIcon}
|
|
||||||
name={icons.WARNING}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && selectedArtist ?
|
|
||||||
<ImportArtistName
|
|
||||||
artistName={selectedArtist.artistName}
|
|
||||||
disambiguation={selectedArtist.disambiguation}
|
|
||||||
// year={selectedArtist.year}
|
|
||||||
isExistingArtist={isExistingArtist}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !selectedArtist ?
|
|
||||||
<div className={styles.noMatches}>
|
|
||||||
<Icon
|
|
||||||
className={styles.warningIcon}
|
|
||||||
name={icons.WARNING}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
/>
|
|
||||||
|
|
||||||
No match found!
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error ?
|
|
||||||
<div>
|
|
||||||
<Icon
|
|
||||||
className={styles.warningIcon}
|
|
||||||
title={errorMessage}
|
|
||||||
name={icons.WARNING}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Search failed, please try again later.
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.dropdownArrowContainer}>
|
|
||||||
<Icon
|
|
||||||
name={icons.CARET_DOWN}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Reference>
|
|
||||||
|
|
||||||
<Portal>
|
|
||||||
<Popper
|
|
||||||
placement="bottom"
|
|
||||||
modifiers={{
|
|
||||||
preventOverflow: {
|
|
||||||
boundariesElement: 'viewport'
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ ref, style, scheduleUpdate }) => {
|
|
||||||
this._scheduleUpdate = scheduleUpdate;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
id={this._contentId}
|
|
||||||
className={styles.contentContainer}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
this.state.isOpen ?
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.searchContainer}>
|
|
||||||
<div className={styles.searchIconContainer}>
|
|
||||||
<Icon name={icons.SEARCH} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
className={styles.searchInput}
|
|
||||||
name={`${name}_textInput`}
|
|
||||||
value={this.state.term}
|
|
||||||
onChange={this.onSearchInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormInputButton
|
|
||||||
kind={kinds.DEFAULT}
|
|
||||||
spinnerIcon={icons.REFRESH}
|
|
||||||
canSpin={true}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
onPress={this.onRefreshPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.REFRESH} />
|
|
||||||
</FormInputButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.results}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<ImportArtistSearchResultConnector
|
|
||||||
key={item.foreignArtistId}
|
|
||||||
foreignArtistId={item.foreignArtistId}
|
|
||||||
artistName={item.artistName}
|
|
||||||
disambiguation={item.disambiguation}
|
|
||||||
// year={item.year}
|
|
||||||
onPress={this.onArtistSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Popper>
|
|
||||||
</Portal>
|
|
||||||
</Manager>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistSelectArtist.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
selectedArtist: PropTypes.object,
|
|
||||||
isExistingArtist: PropTypes.bool.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isQueued: PropTypes.bool.isRequired,
|
|
||||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
|
||||||
onSearchInputChange: PropTypes.func.isRequired,
|
|
||||||
onArtistSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ImportArtistSelectArtist.defaultProps = {
|
|
||||||
isFetching: true,
|
|
||||||
isPopulated: false,
|
|
||||||
items: [],
|
|
||||||
isQueued: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistSelectArtist;
|
|
@ -1,76 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions';
|
|
||||||
import createImportArtistItemSelector from 'Store/Selectors/createImportArtistItemSelector';
|
|
||||||
import ImportArtistSelectArtist from './ImportArtistSelectArtist';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.importArtist.isLookingUpArtist,
|
|
||||||
createImportArtistItemSelector(),
|
|
||||||
(isLookingUpArtist, item) => {
|
|
||||||
return {
|
|
||||||
isLookingUpArtist,
|
|
||||||
...item
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
queueLookupArtist,
|
|
||||||
setImportArtistValue
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImportArtistSelectArtistConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSearchInputChange = (term) => {
|
|
||||||
this.props.queueLookupArtist({
|
|
||||||
name: this.props.id,
|
|
||||||
term,
|
|
||||||
topOfQueue: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onArtistSelect = (foreignArtistId) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.setImportArtistValue({
|
|
||||||
id,
|
|
||||||
selectedArtist: _.find(items, { foreignArtistId })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ImportArtistSelectArtist
|
|
||||||
{...this.props}
|
|
||||||
onSearchInputChange={this.onSearchInputChange}
|
|
||||||
onArtistSelect={this.onArtistSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistSelectArtistConnector.propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
selectedArtist: PropTypes.object,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
queueLookupArtist: PropTypes.func.isRequired,
|
|
||||||
setImportArtistValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistSelectArtistConnector);
|
|
@ -1,30 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
import Switch from 'Components/Router/Switch';
|
|
||||||
import ImportArtistSelectFolderConnector from 'AddArtist/ImportArtist/SelectFolder/ImportArtistSelectFolderConnector';
|
|
||||||
import ImportArtistConnector from 'AddArtist/ImportArtist/Import/ImportArtistConnector';
|
|
||||||
|
|
||||||
class ImportArtist extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/add/import"
|
|
||||||
component={ImportArtistSelectFolderConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/import/:rootFolderId"
|
|
||||||
component={ImportArtistConnector}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ImportArtist;
|
|
@ -1,32 +0,0 @@
|
|||||||
.header {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip {
|
|
||||||
font-size: $defaultFontSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: $monoSpaceFontFamily;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recentFolders {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.startImport {
|
|
||||||
margin-top: 40px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importButtonIcon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
|
||||||
import RootFolders from 'RootFolder/RootFolders';
|
|
||||||
import styles from './ImportArtistSelectFolder.css';
|
|
||||||
|
|
||||||
class ImportArtistSelectFolder extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddNewRootFolderModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
onAddNewRootFolderPress = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onNewRootFolderSelect = ({ value }) => {
|
|
||||||
this.props.onNewRootFolderSelect(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddRootFolderModalClose = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isWindows,
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title="Import Artist">
|
|
||||||
<PageContentBodyConnector>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div>Unable to load root folders</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.header}>
|
|
||||||
Import artist(s) you already have
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.tips}>
|
|
||||||
Some tips to ensure the import goes smoothly:
|
|
||||||
<ul>
|
|
||||||
<li className={styles.tip}>
|
|
||||||
Point Lidarr to the folder containing all of your music not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\music' : '/music'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\music\\sublime' : '/music/sublime'}"</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
items.length > 0 ?
|
|
||||||
<div className={styles.recentFolders}>
|
|
||||||
<FieldSet legend="Root Folders">
|
|
||||||
<RootFolders
|
|
||||||
isFetching={isFetching}
|
|
||||||
isPopulated={isPopulated}
|
|
||||||
error={error}
|
|
||||||
items={items}
|
|
||||||
/>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onPress={this.onAddNewRootFolderPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.importButtonIcon}
|
|
||||||
name={icons.DRIVE}
|
|
||||||
/>
|
|
||||||
Choose another folder
|
|
||||||
</Button>
|
|
||||||
</div> :
|
|
||||||
|
|
||||||
<div className={styles.startImport}>
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onPress={this.onAddNewRootFolderPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.importButtonIcon}
|
|
||||||
name={icons.DRIVE}
|
|
||||||
/>
|
|
||||||
Start Import
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
|
||||||
name="rootFolderPath"
|
|
||||||
value=""
|
|
||||||
onChange={this.onNewRootFolderSelect}
|
|
||||||
onModalClose={this.onAddRootFolderModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBodyConnector>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistSelectFolder.propTypes = {
|
|
||||||
isWindows: PropTypes.bool.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportArtistSelectFolder;
|
|
@ -1,84 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { push } from 'connected-react-router';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
|
|
||||||
import ImportArtistSelectFolder from './ImportArtistSelectFolder';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.rootFolders,
|
|
||||||
createSystemStatusSelector(),
|
|
||||||
(rootFolders, systemStatus) => {
|
|
||||||
return {
|
|
||||||
...rootFolders,
|
|
||||||
isWindows: systemStatus.isWindows
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchRootFolders,
|
|
||||||
addRootFolder,
|
|
||||||
push
|
|
||||||
};
|
|
||||||
|
|
||||||
class ImportArtistSelectFolderConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchRootFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
isSaving,
|
|
||||||
saveError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
|
||||||
const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id);
|
|
||||||
|
|
||||||
if (newRootFolders.length === 1) {
|
|
||||||
this.props.push(`${window.Lidarr.urlBase}/add/import/${newRootFolders[0].id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onNewRootFolderSelect = (path) => {
|
|
||||||
this.props.addRootFolder({ path });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<ImportArtistSelectFolder
|
|
||||||
{...this.props}
|
|
||||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportArtistSelectFolderConnector.propTypes = {
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchRootFolders: PropTypes.func.isRequired,
|
|
||||||
addRootFolder: PropTypes.func.isRequired,
|
|
||||||
push: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistSelectFolderConnector);
|
|
@ -1,27 +0,0 @@
|
|||||||
.link {
|
|
||||||
composes: link from '~Components/Link/Link.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.unavailablePath {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unavailableLabel {
|
|
||||||
composes: label from '~Components/Label.css';
|
|
||||||
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.freeSpace,
|
|
||||||
.unmappedFolders {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 45px;
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import styles from './RootFolderRow.css';
|
|
||||||
|
|
||||||
function RootFolderRow(props) {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
path,
|
|
||||||
accessible,
|
|
||||||
freeSpace,
|
|
||||||
unmappedFolders,
|
|
||||||
onDeletePress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const isUnavailable = !accessible;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
<TableRowCell>
|
|
||||||
{
|
|
||||||
isUnavailable ?
|
|
||||||
<div className={styles.unavailablePath}>
|
|
||||||
{path}
|
|
||||||
|
|
||||||
<Label
|
|
||||||
className={styles.unavailableLabel}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
>
|
|
||||||
Unavailable
|
|
||||||
</Label>
|
|
||||||
</div> :
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
to={`/add/import/${id}`}
|
|
||||||
>
|
|
||||||
{path}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.freeSpace}>
|
|
||||||
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.unmappedFolders}>
|
|
||||||
{isUnavailable ? '-' : unmappedFolders.length}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
<IconButton
|
|
||||||
title="Remove root folder"
|
|
||||||
name={icons.REMOVE}
|
|
||||||
onPress={onDeletePress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolderRow.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
path: PropTypes.string.isRequired,
|
|
||||||
accessible: PropTypes.bool.isRequired,
|
|
||||||
freeSpace: PropTypes.number,
|
|
||||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onDeletePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
RootFolderRow.defaultProps = {
|
|
||||||
unmappedFolders: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootFolderRow;
|
|
@ -1,13 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
|
||||||
import RootFolderRow from './RootFolderRow';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onDeletePress() {
|
|
||||||
dispatch(deleteRootFolder({ id: props.id }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(RootFolderRow);
|
|
@ -1,81 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import RootFolderRowConnector from './RootFolderRowConnector';
|
|
||||||
|
|
||||||
const rootFolderColumns = [
|
|
||||||
{
|
|
||||||
name: 'path',
|
|
||||||
label: 'Path',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'freeSpace',
|
|
||||||
label: 'Free Space',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'unmappedFolders',
|
|
||||||
label: 'Unmapped Folders',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function RootFolders(props) {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (isFetching && !isPopulated) {
|
|
||||||
return (
|
|
||||||
<LoadingIndicator />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isFetching && !!error) {
|
|
||||||
return (
|
|
||||||
<div>Unable to load root folders</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
columns={rootFolderColumns}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((rootFolder) => {
|
|
||||||
return (
|
|
||||||
<RootFolderRowConnector
|
|
||||||
key={rootFolder.id}
|
|
||||||
id={rootFolder.id}
|
|
||||||
path={rootFolder.path}
|
|
||||||
accessible={rootFolder.accessible}
|
|
||||||
freeSpace={rootFolder.freeSpace}
|
|
||||||
unmappedFolders={rootFolder.unmappedFolders}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFolders.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RootFolders;
|
|
@ -1,46 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
|
||||||
import RootFolders from './RootFolders';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.rootFolders,
|
|
||||||
(rootFolders) => {
|
|
||||||
return rootFolders;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchRootFolders: fetchRootFolders
|
|
||||||
};
|
|
||||||
|
|
||||||
class RootFoldersConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchRootFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<RootFolders
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RootFoldersConnector.propTypes = {
|
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);
|
|
@ -1,7 +0,0 @@
|
|||||||
.addRootFolderButtonContainer {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.importButtonIcon {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
|
||||||
import styles from './AddRootFolder.css';
|
|
||||||
|
|
||||||
class AddRootFolder extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddNewRootFolderModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
onAddNewRootFolderPress = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onNewRootFolderSelect = ({ value }) => {
|
|
||||||
this.props.onNewRootFolderSelect(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddRootFolderModalClose = () => {
|
|
||||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div className={styles.addRootFolderButtonContainer}>
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
onPress={this.onAddNewRootFolderPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.importButtonIcon}
|
|
||||||
name={icons.DRIVE}
|
|
||||||
/>
|
|
||||||
Add Root Folder
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<FileBrowserModal
|
|
||||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
|
||||||
name="rootFolderPath"
|
|
||||||
value=""
|
|
||||||
onChange={this.onNewRootFolderSelect}
|
|
||||||
onModalClose={this.onAddRootFolderModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddRootFolder.propTypes = {
|
|
||||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddRootFolder;
|
|
@ -1,13 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AddRootFolder from './AddRootFolder';
|
|
||||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch) {
|
|
||||||
return {
|
|
||||||
onNewRootFolderSelect(path) {
|
|
||||||
dispatch(addRootFolder({ path }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(AddRootFolder);
|
|
@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditRootFolderModalContentConnector from './EditRootFolderModalContentConnector';
|
||||||
|
|
||||||
|
function EditRootFolderModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditRootFolderModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditRootFolderModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditRootFolderModal;
|
@ -0,0 +1,58 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import { cancelSaveRootFolder } from 'Store/Actions/settingsActions';
|
||||||
|
import EditRootFolderModal from './EditRootFolderModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
const section = 'settings.rootFolders';
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispatchClearPendingChanges() {
|
||||||
|
dispatch(clearPendingChanges({ section }));
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchCancelSaveRootFolder() {
|
||||||
|
dispatch(cancelSaveRootFolder({ section }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditRootFolderModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.dispatchClearPendingChanges();
|
||||||
|
this.props.dispatchCancelSaveRootFolder();
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
dispatchCancelSaveRootFolder,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditRootFolderModal
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditRootFolderModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
dispatchCancelSaveRootFolder: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(EditRootFolderModalConnector);
|
@ -0,0 +1,15 @@
|
|||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideMetadataProfile {
|
||||||
|
composes: group from '~Components/Form/FormGroup.css';
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
@ -0,0 +1,217 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
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 Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent';
|
||||||
|
import ArtistMetadataProfilePopoverContent from 'AddArtist/ArtistMetadataProfilePopoverContent';
|
||||||
|
import styles from './EditRootFolderModalContent.css';
|
||||||
|
|
||||||
|
function EditRootFolderModalContent(props) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
advancedSettings,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onModalClose,
|
||||||
|
onSavePress,
|
||||||
|
onDeleteRootFolderPress,
|
||||||
|
showMetadataProfile,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
defaultQualityProfileId,
|
||||||
|
defaultMetadataProfileId,
|
||||||
|
defaultMonitorOption,
|
||||||
|
defaultTags
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? 'Edit Root Folder' : 'Add Root Folder'}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to add a new root folder, please try again.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error &&
|
||||||
|
<Form {...otherProps}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={id ? inputTypes.TEXT : inputTypes.PATH}
|
||||||
|
readOnly={!!id}
|
||||||
|
name="path"
|
||||||
|
helpText="Root Folder containing your music library"
|
||||||
|
helpTextWarning="This must be different to the directory where your download client puts files"
|
||||||
|
{...path}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
Monitor
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Monitoring Options"
|
||||||
|
body={<ArtistMonitoringOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_ALBUMS_SELECT}
|
||||||
|
name="defaultMonitorOption"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...defaultMonitorOption}
|
||||||
|
helpText="Default Monitoring Options for albums by artists detected in this folder"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Quality Profile</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="defaultQualityProfileId"
|
||||||
|
helpText="Default Quality Profile for artists detected in this folder"
|
||||||
|
{...defaultQualityProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
|
||||||
|
<FormLabel>
|
||||||
|
Metadata Profile
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Metadata Profile"
|
||||||
|
body={<ArtistMetadataProfilePopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.METADATA_PROFILE_SELECT}
|
||||||
|
name="defaultMetadataProfileId"
|
||||||
|
helpText="Default Metadata Profile for artists detected in this folder"
|
||||||
|
{...defaultMetadataProfileId}
|
||||||
|
includeNone={true}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Default Lidarr Tags</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="defaultTags"
|
||||||
|
helpText="Default Lidarr Tags for artists detected in this folder"
|
||||||
|
{...defaultTags}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteRootFolderPress}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditRootFolderModalContent.propTypes = {
|
||||||
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onDeleteRootFolderPress: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditRootFolderModalContent;
|
@ -0,0 +1,84 @@
|
|||||||
|
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 { setRootFolderValue, saveRootFolder } from 'Store/Actions/settingsActions';
|
||||||
|
import EditRootFolderModalContent from './EditRootFolderModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { id }) => id,
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
(state) => state.settings.metadataProfiles,
|
||||||
|
(state) => state.settings.rootFolders,
|
||||||
|
createProviderSettingsSelector('rootFolders'),
|
||||||
|
(id, advancedSettings, metadataProfiles, rootFolders, rootFolderSettings) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||||
|
...rootFolderSettings,
|
||||||
|
isFetching: rootFolders.isFetching
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setRootFolderValue,
|
||||||
|
saveRootFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditRootFolderModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setRootFolderValue({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveRootFolder({ id: this.props.id });
|
||||||
|
|
||||||
|
if (this.props.onRootFolderAdded) {
|
||||||
|
this.props.onRootFolderAdded(this.props.item.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditRootFolderModalContent
|
||||||
|
{...this.props}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditRootFolderModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setRootFolderValue: PropTypes.func.isRequired,
|
||||||
|
saveRootFolder: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onRootFolderAdded: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditRootFolderModalContentConnector);
|
@ -0,0 +1,19 @@
|
|||||||
|
.rootFolder {
|
||||||
|
composes: card from '~Components/Card.css';
|
||||||
|
|
||||||
|
width: 290px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enabled {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import EditRootFolderModalConnector from './EditRootFolderModalConnector';
|
||||||
|
import styles from './RootFolder.css';
|
||||||
|
|
||||||
|
class RootFolder extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditRootFolderModalOpen: false,
|
||||||
|
isDeleteRootFolderModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditRootFolderPress = () => {
|
||||||
|
this.setState({ isEditRootFolderModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditRootFolderModalClose = () => {
|
||||||
|
this.setState({ isEditRootFolderModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteRootFolderPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditRootFolderModalOpen: false,
|
||||||
|
isDeleteRootFolderModalOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteRootFolderModalClose= () => {
|
||||||
|
this.setState({ isDeleteRootFolderModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteRootFolder = () => {
|
||||||
|
this.props.onConfirmDeleteRootFolder(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
qualityProfile,
|
||||||
|
metadataProfile
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.rootFolder}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={this.onEditRootFolderPress}
|
||||||
|
>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.enabled}>
|
||||||
|
<Label kind={kinds.SUCCESS}>
|
||||||
|
{path}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Label kind={kinds.SUCCESS}>
|
||||||
|
{qualityProfile.name}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Label kind={kinds.SUCCESS}>
|
||||||
|
{metadataProfile.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditRootFolderModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditRootFolderModalOpen}
|
||||||
|
onModalClose={this.onEditRootFolderModalClose}
|
||||||
|
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteRootFolderModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Root Folder"
|
||||||
|
message={`Are you sure you want to delete the root folder '${name}'?`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDeleteRootFolder}
|
||||||
|
onCancel={this.onDeleteRootFolderModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RootFolder.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
qualityProfile: PropTypes.object.isRequired,
|
||||||
|
metadataProfile: PropTypes.object.isRequired,
|
||||||
|
onConfirmDeleteRootFolder: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RootFolder;
|
@ -0,0 +1,20 @@
|
|||||||
|
.rootFolders {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addRootFolder {
|
||||||
|
composes: rootFolder from '~./RootFolder.css';
|
||||||
|
|
||||||
|
background-color: $cardAlternateBackgroundColor;
|
||||||
|
color: $gray;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 20px 0;
|
||||||
|
border: 1px solid $borderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import RootFolder from './RootFolder';
|
||||||
|
import EditRootFolderModalConnector from './EditRootFolderModalConnector';
|
||||||
|
import styles from './RootFolders.css';
|
||||||
|
|
||||||
|
class RootFolders extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddRootFolderModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAddRootFolderPress = () => {
|
||||||
|
this.setState({ isAddRootFolderModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddRootFolderModalClose = () => {
|
||||||
|
this.setState({ isAddRootFolderModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
qualityProfiles,
|
||||||
|
metadataProfiles,
|
||||||
|
onConfirmDeleteRootFolder,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend="Root Folders">
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage="Unable to load root folders"
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.rootFolders}>
|
||||||
|
{
|
||||||
|
items.sort(sortByName).map((item) => {
|
||||||
|
const qualityProfile = qualityProfiles.find((profile) => profile.id === item.defaultQualityProfileId);
|
||||||
|
const metadataProfile = metadataProfiles.find((profile) => profile.id === item.defaultMetadataProfileId);
|
||||||
|
return (
|
||||||
|
<RootFolder
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
qualityProfile={qualityProfile}
|
||||||
|
metadataProfile={metadataProfile}
|
||||||
|
onConfirmDeleteRootFolder={onConfirmDeleteRootFolder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addRootFolder}
|
||||||
|
onPress={this.onAddRootFolderPress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.ADD}
|
||||||
|
size={45}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditRootFolderModalConnector
|
||||||
|
isOpen={this.state.isAddRootFolderModalOpen}
|
||||||
|
onModalClose={this.onAddRootFolderModalClose}
|
||||||
|
/>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RootFolders.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
metadataProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onConfirmDeleteRootFolder: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RootFolders;
|
@ -0,0 +1,62 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchRootFolders, deleteRootFolder } from 'Store/Actions/settingsActions';
|
||||||
|
import RootFolders from './RootFolders';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.rootFolders,
|
||||||
|
(state) => state.settings.qualityProfiles,
|
||||||
|
(state) => state.settings.metadataProfiles,
|
||||||
|
(rootFolders, quality, metadata) => {
|
||||||
|
return {
|
||||||
|
qualityProfiles: quality.items,
|
||||||
|
metadataProfiles: metadata.items,
|
||||||
|
...rootFolders
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchRootFolders: fetchRootFolders,
|
||||||
|
dispatchDeleteRootFolder: deleteRootFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
class RootFoldersConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onConfirmDeleteRootFolder = (id) => {
|
||||||
|
this.props.dispatchDeleteRootFolder({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<RootFolders
|
||||||
|
{...this.props}
|
||||||
|
onConfirmDeleteRootFolder={this.onConfirmDeleteRootFolder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RootFoldersConnector.propTypes = {
|
||||||
|
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||||
|
dispatchDeleteRootFolder: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(RootFoldersConnector);
|
@ -0,0 +1,76 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'settings.rootFolders';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_ROOT_FOLDERS = 'settings/rootFolders/fetchRootFolders';
|
||||||
|
export const SET_ROOT_FOLDER_VALUE = 'settings/rootFolders/setRootFolderValue';
|
||||||
|
export const SAVE_ROOT_FOLDER = 'settings/rootFolders/saveRootFolder';
|
||||||
|
export const CANCEL_SAVE_ROOT_FOLDER = 'settings/rootFolders/cancelSaveRootFolder';
|
||||||
|
export const DELETE_ROOT_FOLDER = 'settings/rootFolders/deleteRootFolder';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchRootFolders = createThunk(FETCH_ROOT_FOLDERS);
|
||||||
|
export const saveRootFolder = createThunk(SAVE_ROOT_FOLDER);
|
||||||
|
export const cancelSaveRootFolder = createThunk(CANCEL_SAVE_ROOT_FOLDER);
|
||||||
|
export const deleteRootFolder = createThunk(DELETE_ROOT_FOLDER);
|
||||||
|
|
||||||
|
export const setRootFolderValue = createAction(SET_ROOT_FOLDER_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
schema: {
|
||||||
|
defaultTags: []
|
||||||
|
},
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
|
||||||
|
[FETCH_ROOT_FOLDERS]: createFetchHandler(section, '/rootFolder'),
|
||||||
|
|
||||||
|
[SAVE_ROOT_FOLDER]: createSaveProviderHandler(section, '/rootFolder'),
|
||||||
|
[CANCEL_SAVE_ROOT_FOLDER]: createCancelSaveProviderHandler(section),
|
||||||
|
[DELETE_ROOT_FOLDER]: createRemoveItemHandler(section, '/rootFolder')
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_ROOT_FOLDER_VALUE]: createSetSettingValueReducer(section)
|
||||||
|
}
|
||||||
|
};
|
@ -1,327 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createAction } from 'redux-actions';
|
|
||||||
import { batchActions } from 'redux-batched-actions';
|
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
|
||||||
import getNewArtist from 'Utilities/Artist/getNewArtist';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
import { set, removeItem, updateItem } from './baseActions';
|
|
||||||
import { fetchRootFolders } from './rootFolderActions';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'importArtist';
|
|
||||||
let concurrentLookups = 0;
|
|
||||||
let abortCurrentLookup = null;
|
|
||||||
const queue = [];
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isLookingUpArtist: false,
|
|
||||||
isImporting: false,
|
|
||||||
isImported: false,
|
|
||||||
importError: null,
|
|
||||||
items: []
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist';
|
|
||||||
export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist';
|
|
||||||
export const CANCEL_LOOKUP_ARTIST = 'importArtist/cancelLookupArtist';
|
|
||||||
export const LOOKUP_UNSEARCHED_ARTIST = 'importArtist/lookupUnsearchedArtist';
|
|
||||||
export const CLEAR_IMPORT_ARTIST = 'importArtist/clearImportArtist';
|
|
||||||
export const SET_IMPORT_ARTIST_VALUE = 'importArtist/setImportArtistValue';
|
|
||||||
export const IMPORT_ARTIST = 'importArtist/importArtist';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const queueLookupArtist = createThunk(QUEUE_LOOKUP_ARTIST);
|
|
||||||
export const startLookupArtist = createThunk(START_LOOKUP_ARTIST);
|
|
||||||
export const importArtist = createThunk(IMPORT_ARTIST);
|
|
||||||
export const lookupUnsearchedArtist = createThunk(LOOKUP_UNSEARCHED_ARTIST);
|
|
||||||
export const clearImportArtist = createAction(CLEAR_IMPORT_ARTIST);
|
|
||||||
export const cancelLookupArtist = createAction(CANCEL_LOOKUP_ARTIST);
|
|
||||||
|
|
||||||
export const setImportArtistValue = createAction(SET_IMPORT_ARTIST_VALUE, (payload) => {
|
|
||||||
return {
|
|
||||||
section,
|
|
||||||
...payload
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
|
|
||||||
[QUEUE_LOOKUP_ARTIST]: function(getState, payload, dispatch) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
term,
|
|
||||||
topOfQueue = false
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const state = getState().importArtist;
|
|
||||||
const item = _.find(state.items, { id: name }) || {
|
|
||||||
id: name,
|
|
||||||
term,
|
|
||||||
path,
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(updateItem({
|
|
||||||
section,
|
|
||||||
...item,
|
|
||||||
term,
|
|
||||||
isQueued: true,
|
|
||||||
items: []
|
|
||||||
}));
|
|
||||||
|
|
||||||
const itemIndex = queue.indexOf(item.id);
|
|
||||||
|
|
||||||
if (itemIndex >= 0) {
|
|
||||||
queue.splice(itemIndex, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (topOfQueue) {
|
|
||||||
queue.unshift(item.id);
|
|
||||||
} else {
|
|
||||||
queue.push(item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (term && term.length > 2) {
|
|
||||||
dispatch(startLookupArtist({ start: true }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
[START_LOOKUP_ARTIST]: function(getState, payload, dispatch) {
|
|
||||||
if (concurrentLookups >= 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = getState().importArtist;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isLookingUpArtist,
|
|
||||||
items
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
const queueId = queue[0];
|
|
||||||
|
|
||||||
if (payload.start && !isLookingUpArtist) {
|
|
||||||
dispatch(set({ section, isLookingUpArtist: true }));
|
|
||||||
} else if (!isLookingUpArtist) {
|
|
||||||
return;
|
|
||||||
} else if (!queueId) {
|
|
||||||
dispatch(set({ section, isLookingUpArtist: false }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
concurrentLookups++;
|
|
||||||
queue.splice(0, 1);
|
|
||||||
|
|
||||||
const queued = items.find((i) => i.id === queueId);
|
|
||||||
|
|
||||||
dispatch(updateItem({
|
|
||||||
section,
|
|
||||||
id: queued.id,
|
|
||||||
isFetching: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { request, abortRequest } = createAjaxRequest({
|
|
||||||
url: '/artist/lookup',
|
|
||||||
data: {
|
|
||||||
term: queued.term
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
abortCurrentLookup = abortRequest;
|
|
||||||
|
|
||||||
request.done((data) => {
|
|
||||||
dispatch(updateItem({
|
|
||||||
section,
|
|
||||||
id: queued.id,
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: true,
|
|
||||||
error: null,
|
|
||||||
items: data,
|
|
||||||
isQueued: false,
|
|
||||||
selectedArtist: queued.selectedArtist || data[0],
|
|
||||||
updateOnly: true
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
request.fail((xhr) => {
|
|
||||||
dispatch(updateItem({
|
|
||||||
section,
|
|
||||||
id: queued.id,
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: xhr,
|
|
||||||
isQueued: false,
|
|
||||||
updateOnly: true
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
request.always(() => {
|
|
||||||
concurrentLookups--;
|
|
||||||
|
|
||||||
dispatch(startLookupArtist());
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[LOOKUP_UNSEARCHED_ARTIST]: function(getState, payload, dispatch) {
|
|
||||||
const state = getState().importArtist;
|
|
||||||
|
|
||||||
if (state.isLookingUpArtist) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.items.forEach((item) => {
|
|
||||||
const id = item.id;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!item.isPopulated &&
|
|
||||||
!queue.includes(id)
|
|
||||||
) {
|
|
||||||
queue.push(item.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (queue.length) {
|
|
||||||
dispatch(startLookupArtist({ start: true }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
[IMPORT_ARTIST]: function(getState, payload, dispatch) {
|
|
||||||
dispatch(set({ section, isImporting: true }));
|
|
||||||
|
|
||||||
const ids = payload.ids;
|
|
||||||
const items = getState().importArtist.items;
|
|
||||||
const addedIds = [];
|
|
||||||
|
|
||||||
const allNewArtist = ids.reduce((acc, id) => {
|
|
||||||
const item = _.find(items, { id });
|
|
||||||
const selectedArtist = item.selectedArtist;
|
|
||||||
|
|
||||||
// Make sure we have a selected artist and
|
|
||||||
// the same artist hasn't been added yet.
|
|
||||||
if (selectedArtist && !_.some(acc, { foreignArtistId: selectedArtist.foreignArtistId })) {
|
|
||||||
const newArtist = getNewArtist(_.cloneDeep(selectedArtist), item);
|
|
||||||
newArtist.path = item.path;
|
|
||||||
|
|
||||||
addedIds.push(id);
|
|
||||||
acc.push(newArtist);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: '/artist/import',
|
|
||||||
method: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(allNewArtist)
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(batchActions([
|
|
||||||
set({
|
|
||||||
section,
|
|
||||||
isImporting: false,
|
|
||||||
isImported: true
|
|
||||||
}),
|
|
||||||
|
|
||||||
...data.map((artist) => updateItem({ section: 'artist', ...artist })),
|
|
||||||
|
|
||||||
...addedIds.map((id) => removeItem({ section, id }))
|
|
||||||
]));
|
|
||||||
|
|
||||||
dispatch(fetchRootFolders());
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(batchActions(
|
|
||||||
set({
|
|
||||||
section,
|
|
||||||
isImporting: false,
|
|
||||||
isImported: true
|
|
||||||
}),
|
|
||||||
|
|
||||||
addedIds.map((id) => updateItem({
|
|
||||||
section,
|
|
||||||
id,
|
|
||||||
importError: xhr
|
|
||||||
}))
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
|
||||||
|
|
||||||
[CANCEL_LOOKUP_ARTIST]: function(state) {
|
|
||||||
queue.splice(0, queue.length);
|
|
||||||
|
|
||||||
const items = state.items.map((item) => {
|
|
||||||
if (item.isQueued) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
isQueued: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
isLookingUpArtist: false,
|
|
||||||
items
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[CLEAR_IMPORT_ARTIST]: function(state) {
|
|
||||||
if (abortCurrentLookup) {
|
|
||||||
abortCurrentLookup();
|
|
||||||
|
|
||||||
abortCurrentLookup = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
queue.splice(0, queue.length);
|
|
||||||
|
|
||||||
return Object.assign({}, state, defaultState);
|
|
||||||
},
|
|
||||||
|
|
||||||
[SET_IMPORT_ARTIST_VALUE]: function(state, { payload }) {
|
|
||||||
const newState = getSectionState(state, section);
|
|
||||||
const items = newState.items;
|
|
||||||
const index = _.findIndex(items, { id: payload.id });
|
|
||||||
|
|
||||||
newState.items = [...items];
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
const item = items[index];
|
|
||||||
|
|
||||||
newState.items.splice(index, 1, { ...item, ...payload });
|
|
||||||
} else {
|
|
||||||
newState.items.push({ ...payload });
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateSectionState(state, section, newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, defaultState, section);
|
|
@ -1,97 +0,0 @@
|
|||||||
import { batchActions } from 'redux-batched-actions';
|
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
|
||||||
import { set, updateItem } from './baseActions';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'rootFolders';
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null,
|
|
||||||
items: []
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const FETCH_ROOT_FOLDERS = 'rootFolders/fetchRootFolders';
|
|
||||||
export const ADD_ROOT_FOLDER = 'rootFolders/addRootFolder';
|
|
||||||
export const DELETE_ROOT_FOLDER = 'rootFolders/deleteRootFolder';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const fetchRootFolders = createThunk(FETCH_ROOT_FOLDERS);
|
|
||||||
export const addRootFolder = createThunk(ADD_ROOT_FOLDER);
|
|
||||||
export const deleteRootFolder = createThunk(DELETE_ROOT_FOLDER);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
|
|
||||||
[FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'),
|
|
||||||
|
|
||||||
[DELETE_ROOT_FOLDER]: createRemoveItemHandler(
|
|
||||||
'rootFolders',
|
|
||||||
'/rootFolder',
|
|
||||||
(state) => state.rootFolders
|
|
||||||
),
|
|
||||||
|
|
||||||
[ADD_ROOT_FOLDER]: function(getState, payload, dispatch) {
|
|
||||||
const path = payload.path;
|
|
||||||
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: '/rootFolder',
|
|
||||||
method: 'POST',
|
|
||||||
data: JSON.stringify({ path }),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(batchActions([
|
|
||||||
updateItem({
|
|
||||||
section,
|
|
||||||
...data
|
|
||||||
}),
|
|
||||||
|
|
||||||
set({
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null
|
|
||||||
})
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: xhr
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
export const reducers = createHandleActions({}, defaultState, section);
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue