New: Manual import improvements (#683)

* New: Manual import improvements

 - Detect and merge import with files already in library.
 - Allow selection of album release from Manual Import modal.
 - Loading indicator while fetching updated decisions

* Disable release switching if user manually overrode release
This commit is contained in:
ta264 2019-04-04 09:20:47 +01:00 committed by GitHub
parent 61cea37f05
commit 188e0e1040
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 1295 additions and 371 deletions

View file

@ -65,6 +65,7 @@ class SelectAlbumModalContentConnector extends Component {
this.props.updateInteractiveImportItem({
id,
album,
albumReleaseId: undefined,
tracks: [],
rejections: []
});

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectAlbumReleaseModalContentConnector from './SelectAlbumReleaseModalContentConnector';
class SelectAlbumReleaseModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectAlbumReleaseModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectAlbumReleaseModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectAlbumReleaseModal;

View file

@ -0,0 +1,18 @@
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
.filterInput {
composes: input from '~Components/Form/TextInput.css';
flex: 0 0 auto;
margin-bottom: 20px;
}
.scroller {
flex: 1 1 auto;
}

View file

@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import { scrollDirections } from 'Helpers/Props';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectAlbumReleaseRow from './SelectAlbumReleaseRow';
import Alert from 'Components/Alert';
import styles from './SelectAlbumReleaseModalContent.css';
const columns = [
{
name: 'album',
label: 'Album',
isVisible: true
},
{
name: 'release',
label: 'Album Release',
isVisible: true
}
];
class SelectAlbumReleaseModalContent extends Component {
//
// Render
render() {
const {
albums,
onAlbumReleaseSelect,
onModalClose,
...otherProps
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Manual Import - Select Album Release
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Alert>
Overrriding a release here will <b>disable automatic release selection</b> for that album in future.
</Alert>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
albums.map((item) => {
return (
<SelectAlbumReleaseRow
key={item.album.id}
matchedReleaseId={item.matchedReleaseId}
columns={columns}
onAlbumReleaseSelect={onAlbumReleaseSelect}
{...item.album}
/>
);
})
}
</TableBody>
</Table>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectAlbumReleaseModalContent.propTypes = {
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
onAlbumReleaseSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectAlbumReleaseModalContent;

View file

@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {
updateInteractiveImportItem,
saveInteractiveImportItem
} from 'Store/Actions/interactiveImportActions';
import SelectAlbumReleaseModalContent from './SelectAlbumReleaseModalContent';
function createMapStateToProps() {
return {};
}
const mapDispatchToProps = {
updateInteractiveImportItem,
saveInteractiveImportItem
};
class SelectAlbumReleaseModalContentConnector extends Component {
//
// Listeners
// onSortPress = (sortKey, sortDirection) => {
// this.props.setInteractiveImportAlbumsSort({ sortKey, sortDirection });
// }
onAlbumReleaseSelect = (albumId, albumReleaseId) => {
const ids = this.props.importIdsByAlbum[albumId];
ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
albumReleaseId,
disableReleaseSwitching: true,
tracks: [],
rejections: []
});
});
this.props.saveInteractiveImportItem({ id: ids });
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectAlbumReleaseModalContent
{...this.props}
onAlbumReleaseSelect={this.onAlbumReleaseSelect}
/>
);
}
}
SelectAlbumReleaseModalContentConnector.propTypes = {
importIdsByAlbum: PropTypes.object.isRequired,
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
saveInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectAlbumReleaseModalContentConnector);

View file

@ -0,0 +1,3 @@
.albumRow {
cursor: pointer;
}

View file

@ -0,0 +1,96 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes } from 'Helpers/Props';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import FormInputGroup from 'Components/Form/FormInputGroup';
import titleCase from 'Utilities/String/titleCase';
class SelectAlbumReleaseRow extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.onAlbumReleaseSelect(parseInt(name), parseInt(value));
}
//
// Render
render() {
const {
id,
matchedReleaseId,
title,
disambiguation,
releases,
columns
} = this.props;
const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title;
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'album') {
return (
<TableRowCell key={name}>
{extendedTitle}
</TableRowCell>
);
}
if (name === 'release') {
return (
<TableRowCell key={name}>
<FormInputGroup
type={inputTypes.SELECT}
name={id.toString()}
values={_.map(releases, (r) => ({
key: r.id,
value: `${r.title}` +
`${r.disambiguation ? ' (' : ''}${titleCase(r.disambiguation)}${r.disambiguation ? ')' : ''}` +
`, ${r.mediumCount} med, ${r.trackCount} tracks` +
`${r.country.length > 0 ? ', ' : ''}${r.country}` +
`${r.format ? ', [' : ''}${r.format}${r.format ? ']' : ''}` +
`${r.monitored ? ', Monitored' : ''}`
}))}
value={matchedReleaseId}
onChange={this.onInputChange}
/>
</TableRowCell>
);
}
return null;
})
}
</TableRow>
);
}
}
SelectAlbumReleaseRow.propTypes = {
id: PropTypes.number.isRequired,
matchedReleaseId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string.isRequired,
releases: PropTypes.arrayOf(PropTypes.object).isRequired,
onAlbumReleaseSelect: PropTypes.func.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default SelectAlbumReleaseRow;

View file

@ -41,17 +41,21 @@ class SelectArtistModalContentConnector extends Component {
onArtistSelect = (artistId) => {
const artist = _.find(this.props.items, { id: artistId });
this.props.ids.forEach((id) => {
const ids = this.props.ids;
ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
artist,
album: undefined,
albumReleaseId: undefined,
tracks: [],
rejections: []
});
this.props.saveInteractiveImportItem({ id });
});
this.props.saveInteractiveImportItem({ id: ids });
this.props.onModalClose(true);
}

View file

@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import ConfirmImportModalContentConnector from './ConfirmImportModalContentConnector';
class ConfirmImportModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ConfirmImportModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
ConfirmImportModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ConfirmImportModal;

View file

@ -0,0 +1,135 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { kinds } from 'Helpers/Props';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Alert from 'Components/Alert';
function formatAlbumFiles(items, album) {
return (
<div key={album.id}>
<b> {album.title} </b>
<ul>
{
_.sortBy(items, 'path').map((item) => {
return (
<li key={item.id}>
{item.path}
</li>
);
})
}
</ul>
</div>
);
}
class ConfirmImportModalContent extends Component {
//
// Lifecycle
componentDidUpdate(prevProps) {
const {
items,
isFetching,
isPopulated
} = this.props;
if (!isFetching && isPopulated && !items.length) {
this.props.onModalClose();
this.props.onConfirmImportPress();
}
}
//
// Render
render() {
const {
albums,
items,
onConfirmImportPress,
onModalClose,
isFetching,
isPopulated
} = this.props;
// don't render if nothing to do
if (!isFetching && isPopulated && !items.length) {
return null;
}
return (
<ModalContent onModalClose={onModalClose}>
{
!isFetching && isPopulated &&
<ModalHeader>
Are you sure?
</ModalHeader>
}
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && isPopulated &&
<div>
<Alert>
You are already have files imported for the albums listed below. If you continue, the existing files <b>will be deleted</b> and the new files imported in their place.
To avoid deleting existing files, press 'Cancel' and use the 'Combine with existing files' option.
</Alert>
{ _.chain(items)
.groupBy('albumId')
.mapValues((value, key) => formatAlbumFiles(value, _.find(albums, (a) => a.id === parseInt(key))))
.values()
.value() }
</div>
}
</ModalBody>
{
!isFetching && isPopulated &&
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={onConfirmImportPress}
>
Proceed
</Button>
</ModalFooter>
}
</ModalContent>
);
}
}
ConfirmImportModalContent.propTypes = {
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
onConfirmImportPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ConfirmImportModalContent;

View file

@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchInteractiveImportTrackFiles, clearInteractiveImportTrackFiles } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import ConfirmImportModalContent from './ConfirmImportModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('interactiveImport.trackFiles'),
(trackFiles) => {
return trackFiles;
}
);
}
const mapDispatchToProps = {
fetchInteractiveImportTrackFiles,
clearInteractiveImportTrackFiles
};
class ConfirmImportModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
albums
} = this.props;
this.props.fetchInteractiveImportTrackFiles({ albumId: albums.map((x) => x.id) });
}
componentWillUnmount() {
this.props.clearInteractiveImportTrackFiles();
}
//
// Render
render() {
return (
<ConfirmImportModalContent
{...this.props}
/>
);
}
}
ConfirmImportModalContentConnector.propTypes = {
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchInteractiveImportTrackFiles: PropTypes.func.isRequired,
clearInteractiveImportTrackFiles: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ConfirmImportModalContentConnector);

View file

@ -19,13 +19,13 @@
.centerButtons,
.rightButtons {
display: flex;
flex: 1 2 25%;
flex: 1 2 20%;
flex-wrap: wrap;
}
.centerButtons {
justify-content: center;
flex: 2 1 50%;
flex: 2 1 60%;
}
.rightButtons {

View file

@ -22,6 +22,8 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
import SelectAlbumReleaseModal from 'InteractiveImport/AlbumRelease/SelectAlbumReleaseModal';
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
@ -80,6 +82,11 @@ const filterExistingFilesOptions = {
NEW: 'new'
};
const replaceExistingFilesOptions = {
COMBINE: 'combine',
DELETE: 'delete'
};
class InteractiveImportModalContent extends Component {
//
@ -95,10 +102,36 @@ class InteractiveImportModalContent extends Component {
selectedState: {},
invalidRowsSelected: [],
isSelectArtistModalOpen: false,
isSelectAlbumModalOpen: false
isSelectAlbumModalOpen: false,
isSelectAlbumReleaseModalOpen: false,
albumsImported: [],
isConfirmImportModalOpen: false,
showClearTracks: false,
inconsistentAlbumReleases: false
};
}
componentDidUpdate(prevProps) {
const selectedIds = this.getSelectedIds();
const selectedItems = _.filter(this.props.items, (x) => _.includes(selectedIds, x.id));
const selectionHasTracks = _.some(selectedItems, (x) => x.tracks.length);
if (this.state.showClearTracks !== selectionHasTracks) {
this.setState({ showClearTracks: selectionHasTracks });
}
const inconsistent = _(selectedItems)
.map((x) => ({ albumId: x.album ? x.album.id : 0, releaseId: x.albumReleaseId }))
.groupBy('albumId')
.mapValues((album) => _(album).groupBy((x) => x.releaseId).values().value().length)
.values()
.some((x) => x !== undefined && x > 1);
if (inconsistent !== this.state.inconsistentAlbumReleases) {
this.setState({ inconsistentAlbumReleases: inconsistent });
}
}
//
// Control
@ -120,20 +153,38 @@ class InteractiveImportModalContent extends Component {
}
onValidRowChange = (id, isValid) => {
this.setState((state) => {
if (isValid) {
return {
invalidRowsSelected: _.without(state.invalidRowsSelected, id)
};
}
return {
invalidRowsSelected: [...state.invalidRowsSelected, id]
};
this.setState((state, props) => {
// make sure to exclude any invalidRows that are no longer present in props
const diff = _.difference(state.invalidRowsSelected, _.map(props.items, 'id'));
const currentInvalid = _.difference(state.invalidRowsSelected, diff);
const newstate = isValid ? _.without(currentInvalid, id) : _.union(currentInvalid, [id]);
return { invalidRowsSelected: newstate };
});
}
onImportSelectedPress = () => {
if (!this.props.replaceExistingFiles) {
this.onConfirmImportPress();
return;
}
// potentially deleting files
const selectedIds = this.getSelectedIds();
const albumsImported = _(this.props.items)
.filter((x) => _.includes(selectedIds, x.id))
.keyBy((x) => x.album.id)
.map((x) => x.album)
.value();
console.log(albumsImported);
this.setState({
albumsImported,
isConfirmImportModalOpen: true
});
}
onConfirmImportPress = () => {
const {
downloadId,
showImportMode,
@ -151,6 +202,10 @@ class InteractiveImportModalContent extends Component {
this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL);
}
onReplaceExistingFilesChange = (value) => {
this.props.onReplaceExistingFilesChange(value === replaceExistingFilesOptions.DELETE);
}
onImportModeChange = ({ value }) => {
this.props.onImportModeChange(value);
}
@ -163,6 +218,10 @@ class InteractiveImportModalContent extends Component {
this.setState({ isSelectAlbumModalOpen: true });
}
onSelectAlbumReleasePress = () => {
this.setState({ isSelectAlbumReleaseModalOpen: true });
}
onClearTrackMappingPress = () => {
const selectedIds = this.getSelectedIds();
@ -175,6 +234,10 @@ class InteractiveImportModalContent extends Component {
});
}
onGetTrackMappingPress = () => {
this.props.saveInteractiveImportItem({ id: this.getSelectedIds() });
}
onSelectArtistModalClose = () => {
this.setState({ isSelectArtistModalOpen: false });
}
@ -183,6 +246,14 @@ class InteractiveImportModalContent extends Component {
this.setState({ isSelectAlbumModalOpen: false });
}
onSelectAlbumReleaseModalClose = () => {
this.setState({ isSelectAlbumReleaseModalOpen: false });
}
onConfirmImportModalClose = () => {
this.setState({ isConfirmImportModalOpen: false });
}
//
// Render
@ -191,12 +262,15 @@ class InteractiveImportModalContent extends Component {
downloadId,
allowArtistChange,
showFilterExistingFiles,
showReplaceExistingFiles,
showImportMode,
filterExistingFiles,
replaceExistingFiles,
title,
folder,
isFetching,
isPopulated,
isSaving,
error,
items,
sortKey,
@ -213,7 +287,12 @@ class InteractiveImportModalContent extends Component {
selectedState,
invalidRowsSelected,
isSelectArtistModalOpen,
isSelectAlbumModalOpen
isSelectAlbumModalOpen,
isSelectAlbumReleaseModalOpen,
albumsImported,
isConfirmImportModalOpen,
showClearTracks,
inconsistentAlbumReleases
} = this.state;
const selectedIds = this.getSelectedIds();
@ -232,43 +311,78 @@ class InteractiveImportModalContent extends Component {
</ModalHeader>
<ModalBody>
{
showFilterExistingFiles &&
<div className={styles.filterContainer}>
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon
name={icons.FILTER}
size={22}
/>
<div className={styles.filterContainer}>
{
showFilterExistingFiles &&
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon
name={icons.FILTER}
size={22}
/>
<div className={styles.filterText}>
{
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
}
</div>
</MenuButton>
<div className={styles.filterText}>
{
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
}
</div>
</MenuButton>
<MenuContent>
<SelectedMenuItem
name={filterExistingFilesOptions.ALL}
isSelected={!filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
All Files
</SelectedMenuItem>
<MenuContent>
<SelectedMenuItem
name={filterExistingFilesOptions.ALL}
isSelected={!filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
All Files
</SelectedMenuItem>
<SelectedMenuItem
name={filterExistingFilesOptions.NEW}
isSelected={filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
Unmapped Files Only
</SelectedMenuItem>
</MenuContent>
</Menu>
</div>
}
<SelectedMenuItem
name={filterExistingFilesOptions.NEW}
isSelected={filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
Unmapped Files Only
</SelectedMenuItem>
</MenuContent>
</Menu>
}
{
showReplaceExistingFiles &&
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon
name={icons.CLONE}
size={22}
/>
<div className={styles.filterText}>
{
replaceExistingFiles ? 'Existing files will be deleted' : 'Combine with existing files'
}
</div>
</MenuButton>
<MenuContent>
<SelectedMenuItem
name={replaceExistingFiles.COMBINE}
isSelected={!replaceExistingFiles}
onPress={this.onReplaceExistingFilesChange}
>
Combine With Existing Files
</SelectedMenuItem>
<SelectedMenuItem
name={replaceExistingFilesOptions.DELETE}
isSelected={replaceExistingFiles}
onPress={this.onReplaceExistingFilesChange}
>
Replace Existing Files
</SelectedMenuItem>
</MenuContent>
</Menu>
}
</div>
{
isFetching &&
@ -299,6 +413,7 @@ class InteractiveImportModalContent extends Component {
<InteractiveImportRow
key={item.id}
isSelected={selectedState[item.id]}
isSaving={isSaving}
{...item}
allowArtistChange={allowArtistChange}
onSelectedChange={this.onSelectedChange}
@ -349,9 +464,30 @@ class InteractiveImportModalContent extends Component {
Select Album
</Button>
<Button onPress={this.onClearTrackMappingPress}>
Clear Track Mapping
<Button
onPress={this.onSelectAlbumReleasePress}
isDisabled={!selectedIds.length}
>
Select Release
</Button>
{
showClearTracks ? (
<Button
onPress={this.onClearTrackMappingPress}
isDisabled={!selectedIds.length}
>
Clear Tracks
</Button>
) : (
<Button
onPress={this.onGetTrackMappingPress}
isDisabled={!selectedIds.length}
>
Map Tracks
</Button>
)
}
</div>
<div className={styles.rightButtons}>
@ -366,7 +502,7 @@ class InteractiveImportModalContent extends Component {
<Button
kind={kinds.SUCCESS}
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
isDisabled={!selectedIds.length || !!invalidRowsSelected.length || inconsistentAlbumReleases}
onPress={this.onImportSelectedPress}
>
Import
@ -387,6 +523,20 @@ class InteractiveImportModalContent extends Component {
onModalClose={this.onSelectAlbumModalClose}
/>
<SelectAlbumReleaseModal
isOpen={isSelectAlbumReleaseModalOpen}
importIdsByAlbum={_.chain(items).filter((x) => x.album).groupBy((x) => x.album.id).mapValues((x) => x.map((y) => y.id)).value()}
albums={_.chain(items).filter((x) => x.album).keyBy((x) => x.album.id).mapValues((x) => ({ matchedReleaseId: x.albumReleaseId, album: x.album })).values().value()}
onModalClose={this.onSelectAlbumReleaseModalClose}
/>
<ConfirmImportModal
isOpen={isConfirmImportModalOpen}
albums={albumsImported}
onModalClose={this.onConfirmImportModalClose}
onConfirmImportPress={this.onConfirmImportPress}
/>
</ModalContent>
);
}
@ -397,12 +547,15 @@ InteractiveImportModalContent.propTypes = {
allowArtistChange: PropTypes.bool.isRequired,
showImportMode: PropTypes.bool.isRequired,
showFilterExistingFiles: PropTypes.bool.isRequired,
showReplaceExistingFiles: PropTypes.bool.isRequired,
filterExistingFiles: PropTypes.bool.isRequired,
replaceExistingFiles: PropTypes.bool.isRequired,
importMode: PropTypes.string.isRequired,
title: PropTypes.string,
folder: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
@ -410,8 +563,10 @@ InteractiveImportModalContent.propTypes = {
interactiveImportErrorMessage: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
onFilterExistingFilesChange: PropTypes.func.isRequired,
onReplaceExistingFilesChange: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired,
saveInteractiveImportItem: PropTypes.func.isRequired,
updateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
@ -419,6 +574,7 @@ InteractiveImportModalContent.propTypes = {
InteractiveImportModalContent.defaultProps = {
allowArtistChange: true,
showFilterExistingFiles: false,
showReplaceExistingFiles: false,
showImportMode: true,
importMode: 'move'
};

View file

@ -3,7 +3,14 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import {
fetchInteractiveImportItems,
setInteractiveImportSort,
clearInteractiveImport,
setInteractiveImportMode,
updateInteractiveImportItem,
saveInteractiveImportItem
} from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
@ -24,6 +31,7 @@ const mapDispatchToProps = {
setInteractiveImportMode,
clearInteractiveImport,
updateInteractiveImportItem,
saveInteractiveImportItem,
executeCommand
};
@ -37,7 +45,8 @@ class InteractiveImportModalContentConnector extends Component {
this.state = {
interactiveImportErrorMessage: null,
filterExistingFiles: true
filterExistingFiles: props.filterExistingFiles,
replaceExistingFiles: props.replaceExistingFiles
};
}
@ -48,22 +57,26 @@ class InteractiveImportModalContentConnector extends Component {
} = this.props;
const {
filterExistingFiles
filterExistingFiles,
replaceExistingFiles
} = this.state;
this.props.fetchInteractiveImportItems({
downloadId,
folder,
filterExistingFiles
filterExistingFiles,
replaceExistingFiles
});
}
componentDidUpdate(prevProps, prevState) {
const {
filterExistingFiles
filterExistingFiles,
replaceExistingFiles
} = this.state;
if (prevState.filterExistingFiles !== filterExistingFiles) {
if (prevState.filterExistingFiles !== filterExistingFiles ||
prevState.replaceExistingFiles !== replaceExistingFiles) {
const {
downloadId,
folder
@ -72,7 +85,8 @@ class InteractiveImportModalContentConnector extends Component {
this.props.fetchInteractiveImportItems({
downloadId,
folder,
filterExistingFiles
filterExistingFiles,
replaceExistingFiles
});
}
}
@ -92,6 +106,10 @@ class InteractiveImportModalContentConnector extends Component {
this.setState({ filterExistingFiles });
}
onReplaceExistingFilesChange = (replaceExistingFiles) => {
this.setState({ replaceExistingFiles });
}
onImportModeChange = (importMode) => {
this.props.setInteractiveImportMode({ importMode });
}
@ -109,7 +127,8 @@ class InteractiveImportModalContentConnector extends Component {
albumReleaseId,
tracks,
quality,
language
language,
disableReleaseSwitching
} = item;
if (!artist) {
@ -146,7 +165,8 @@ class InteractiveImportModalContentConnector extends Component {
trackIds: _.map(tracks, 'id'),
quality,
language,
downloadId: this.props.downloadId
downloadId: this.props.downloadId,
disableReleaseSwitching
});
}
});
@ -158,7 +178,8 @@ class InteractiveImportModalContentConnector extends Component {
this.props.executeCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
importMode,
replaceExistingFiles: this.state.replaceExistingFiles
});
this.props.onModalClose();
@ -170,7 +191,8 @@ class InteractiveImportModalContentConnector extends Component {
render() {
const {
interactiveImportErrorMessage,
filterExistingFiles
filterExistingFiles,
replaceExistingFiles
} = this.state;
return (
@ -178,8 +200,10 @@ class InteractiveImportModalContentConnector extends Component {
{...this.props}
interactiveImportErrorMessage={interactiveImportErrorMessage}
filterExistingFiles={filterExistingFiles}
replaceExistingFiles={replaceExistingFiles}
onSortPress={this.onSortPress}
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
onReplaceExistingFilesChange={this.onReplaceExistingFilesChange}
onImportModeChange={this.onImportModeChange}
onImportSelectedPress={this.onImportSelectedPress}
/>
@ -191,6 +215,7 @@ InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string,
folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
replaceExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchInteractiveImportItems: PropTypes.func.isRequired,
setInteractiveImportSort: PropTypes.func.isRequired,
@ -202,7 +227,8 @@ InteractiveImportModalContentConnector.propTypes = {
};
InteractiveImportModalContentConnector.defaultProps = {
filterExistingFiles: true
filterExistingFiles: true,
replaceExistingFiles: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);

View file

@ -16,3 +16,15 @@
cursor: pointer;
}
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 0;
}
.additionalFile {
composes: row from '~Components/Table/TableRow.css';
color: $disabledColor;
}

View file

@ -9,6 +9,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import TrackQuality from 'Album/TrackQuality';
import TrackLanguage from 'Album/TrackLanguage';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
@ -17,6 +18,7 @@ import SelectTrackModal from 'InteractiveImport/Track/SelectTrackModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import styles from './InteractiveImportRow.css';
class InteractiveImportRow extends Component {
@ -174,7 +176,9 @@ class InteractiveImportRow extends Component {
size,
rejections,
audioTags,
additionalFile,
isSelected,
isSaving,
onSelectedChange
} = this.props;
@ -196,12 +200,29 @@ class InteractiveImportRow extends Component {
const showArtistPlaceholder = isSelected && !artist;
const showAlbumNumberPlaceholder = isSelected && !!artist && !album;
const showTrackNumbersPlaceholder = isSelected && !!album && !tracks.length;
const showTrackNumbersPlaceholder = !isSaving && isSelected && !!album && !tracks.length;
const showTrackNumbersLoading = isSaving && !tracks.length;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !language;
const pathCellContents = (
<div>
{relativePath}
</div>
);
const pathCell = additionalFile ? (
<Tooltip
anchor={pathCellContents}
tooltip='This file is already in your library for a release you are currently importing'
position={tooltipPositions.TOP}
/>
) : pathCellContents;
return (
<TableRow>
<TableRow
className={additionalFile ? styles.additionalFile : undefined}
>
<TableSelectCell
id={id}
isSelected={isSelected}
@ -212,7 +233,7 @@ class InteractiveImportRow extends Component {
className={styles.relativePath}
title={relativePath}
>
{relativePath}
{pathCell}
</TableRowCell>
<TableRowCellButton
@ -237,6 +258,9 @@ class InteractiveImportRow extends Component {
isDisabled={!artist || !album}
onPress={this.onSelectTrackPress}
>
{
showTrackNumbersLoading && <LoadingIndicator size={20} className={styles.loading} />
}
{
showTrackNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : trackNumbers
}
@ -372,7 +396,9 @@ InteractiveImportRow.propTypes = {
size: PropTypes.number.isRequired,
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
audioTags: PropTypes.object.isRequired,
additionalFile: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onValidRowChange: PropTypes.func.isRequired
};