mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-31 04:00:18 -07:00
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:
parent
61cea37f05
commit
188e0e1040
45 changed files with 1295 additions and 371 deletions
|
@ -65,6 +65,7 @@ class SelectAlbumModalContentConnector extends Component {
|
|||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
album,
|
||||
albumReleaseId: undefined,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
|
@ -0,0 +1,3 @@
|
|||
.albumRow {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue