New: Book editor on author details page

This commit is contained in:
ta264
2021-11-19 11:33:23 +00:00
parent bf852cadbe
commit 4887ed0d2f
19 changed files with 939 additions and 9 deletions
@@ -0,0 +1,70 @@
.inputContainer {
margin-right: 20px;
min-width: 150px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.buttonContainerContent {
flex-grow: 0;
}
.buttons {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.organizeSelectedButton,
.tagsButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
height: 35px;
}
.deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-left: 50px;
height: 35px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.deleteSelectedButton {
margin-left: 0;
}
}
@media only screen and (max-width: $breakpointLarge) {
.buttonContainer {
justify-content: flex-start;
margin-top: 10px;
}
}
@media only screen and (max-width: $breakpointSmall) {
.inputContainer {
margin-right: 0;
}
.buttonContainer {
justify-content: flex-start;
}
.buttonContainerContent {
flex-grow: 1;
}
.buttons {
justify-content: space-between;
}
.selectedAuthorLabel {
text-align: left;
}
}
@@ -0,0 +1,156 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BookEditorFooterLabel from './BookEditorFooterLabel';
import DeleteBookModal from './Delete/DeleteBookModal';
import styles from './BookEditorFooter.css';
const NO_CHANGE = 'noChange';
class BookEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
isDeleteBookModalOpen: false,
isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
if (value === NO_CHANGE) {
return;
}
switch (name) {
case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' });
break;
default:
this.props.onSaveSelected({ [name]: value });
}
}
onDeleteSelectedPress = () => {
this.setState({ isDeleteBookModalOpen: true });
}
onDeleteBookModalClose = () => {
this.setState({ isDeleteBookModalOpen: false });
}
//
// Render
render() {
const {
bookIds,
selectedCount,
isSaving,
isDeleting
} = this.props;
const {
monitored,
isDeleteBookModalOpen
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'monitored', value: 'Monitored' },
{ key: 'unmonitored', value: 'Unmonitored' }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<BookEditorFooterLabel
label={translate('MonitorBook')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<BookEditorFooterLabel
label={translate('SelectedCountBooksSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
</div>
</div>
</div>
<DeleteBookModal
isOpen={isDeleteBookModalOpen}
bookIds={bookIds}
onModalClose={this.onDeleteBookModalClose}
/>
</PageContentFooter>
);
}
}
BookEditorFooter.propTypes = {
bookIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSaveSelected: PropTypes.func.isRequired
};
export default BookEditorFooter;
@@ -0,0 +1,8 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}
@@ -0,0 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import SpinnerIcon from 'Components/SpinnerIcon';
import { icons } from 'Helpers/Props';
import styles from './BookEditorFooterLabel.css';
function BookEditorFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
BookEditorFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
BookEditorFooterLabel.defaultProps = {
className: styles.label
};
export default BookEditorFooterLabel;
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DeleteBookModalContentConnector from './DeleteBookModalContentConnector';
function DeleteBookModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<DeleteBookModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteBookModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteBookModal;
@@ -0,0 +1,9 @@
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.deleteFilesMessage {
margin-top: 20px;
color: $dangerColor;
}
@@ -0,0 +1,172 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './DeleteBookModalContent.css';
class DeleteBookModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportListExclusion: true
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
}
onAddImportListExclusionChange = ({ value }) => {
this.setState({ addImportListExclusion: value });
}
onDeleteBookConfirmed = () => {
const {
deleteFiles,
addImportListExclusion
} = this.state;
this.setState({ deleteFiles: false });
this.props.onDeleteSelectedPress(deleteFiles, addImportListExclusion);
}
//
// Render
render() {
const {
book,
files,
onModalClose
} = this.props;
const {
deleteFiles,
addImportListExclusion
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Delete Selected Book
</ModalHeader>
<ModalBody>
<div>
<FormGroup>
<FormLabel>{`Delete File${book.length > 1 ? 's' : ''}`}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={'Delete book files'}
kind={kinds.DANGER}
isDisabled={files.length === 0}
onChange={this.onDeleteFilesChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AddListExclusion')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportListExclusion"
value={addImportListExclusion}
helpText={translate('AddImportListExclusionHelpText')}
kind={kinds.DANGER}
onChange={this.onAddImportListExclusionChange}
/>
</FormGroup>
{
!addImportListExclusion &&
<div className={styles.deleteFilesMessage}>
<div>
{translate('IfYouDontAddAnImportListExclusionAndTheAuthorHasAMetadataProfileOtherThanNoneThenThisBookMayBeReaddedDuringTheNextAuthorRefresh')}
</div>
</div>
}
</div>
<div className={styles.message}>
{`Are you sure you want to delete ${book.length} selected book${book.length > 1 ? 's' : ''}${deleteFiles ? ' and their files' : ''}?`}
</div>
<ul>
{
book.map((s) => {
return (
<li key={s.title}>
<span>{s.title}</span>
</li>
);
})
}
</ul>
{
deleteFiles &&
<div>
<div className={styles.deleteFilesMessage}>
{translate('TheFollowingFilesWillBeDeleted')}
</div>
<ul>
{
files.map((s) => {
return (
<li key={s.path}>
<span>{s.path}</span>
</li>
);
})
}
</ul>
</div>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteBookConfirmed}
>
Delete
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteBookModalContent.propTypes = {
book: PropTypes.arrayOf(PropTypes.object).isRequired,
files: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired
};
export default DeleteBookModalContent;
@@ -0,0 +1,54 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { bulkDeleteBook } from 'Store/Actions/bookEditorActions';
import DeleteBookModalContent from './DeleteBookModalContent';
function createMapStateToProps() {
return createSelector(
(state, { bookIds }) => bookIds,
(state) => state.books.items,
(state) => state.bookFiles.items,
(bookIds, allBooks, allBookFiles) => {
const selectedBook = _.intersectionWith(allBooks, bookIds, (s, id) => {
return s.id === id;
});
const sortedBook = _.orderBy(selectedBook, 'title');
const selectedFiles = _.intersectionWith(allBookFiles, bookIds, (s, id) => {
return s.bookId === id;
});
const files = _.orderBy(selectedFiles, ['bookId', 'path']);
const book = _.map(sortedBook, (s) => {
return {
title: s.title,
path: s.path
};
});
return {
book,
files
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteSelectedPress(deleteFiles, addImportListExclusion) {
dispatch(bulkDeleteBook({
bookIds: props.bookIds,
deleteFiles,
addImportListExclusion
}));
props.onModalClose();
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteBookModalContent);