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