Renames in Frontend

This commit is contained in:
Qstick
2020-05-15 23:32:52 -04:00
committed by ta264
parent ee4e44b81a
commit ee43ccf620
387 changed files with 4036 additions and 4364 deletions
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import RetagAuthorModalContentConnector from './RetagAuthorModalContentConnector';
function RetagAuthorModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<RetagAuthorModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
RetagAuthorModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RetagAuthorModal;
@@ -0,0 +1,8 @@
.retagIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
@@ -0,0 +1,73 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons, kinds } from 'Helpers/Props';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
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 styles from './RetagAuthorModalContent.css';
function RetagAuthorModalContent(props) {
const {
authorNames,
onModalClose,
onRetagAuthorPress
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Retag Selected Author
</ModalHeader>
<ModalBody>
<Alert>
Tip: To preview the tags that will be written... select "Cancel" then click any author name and use the
<Icon
className={styles.retagIcon}
name={icons.RETAG}
/>
</Alert>
<div className={styles.message}>
Are you sure you want to re-tag all files in the {authorNames.length} selected author?
</div>
<ul>
{
authorNames.map((authorName) => {
return (
<li key={authorName}>
{authorName}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={onRetagAuthorPress}
>
Retag
</Button>
</ModalFooter>
</ModalContent>
);
}
RetagAuthorModalContent.propTypes = {
authorNames: PropTypes.arrayOf(PropTypes.string).isRequired,
onModalClose: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired
};
export default RetagAuthorModalContent;
@@ -0,0 +1,67 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import RetagAuthorModalContent from './RetagAuthorModalContent';
function createMapStateToProps() {
return createSelector(
(state, { authorIds }) => authorIds,
createAllAuthorSelector(),
(authorIds, allAuthors) => {
const author = _.intersectionWith(allAuthors, authorIds, (s, id) => {
return s.id === id;
});
const sortedAuthor = _.orderBy(author, 'sortName');
const authorNames = _.map(sortedAuthor, 'authorName');
return {
authorNames
};
}
);
}
const mapDispatchToProps = {
executeCommand
};
class RetagAuthorModalContentConnector extends Component {
//
// Listeners
onRetagAuthorPress = () => {
this.props.executeCommand({
name: commandNames.RETAG_AUTHOR,
authorIds: this.props.authorIds
});
this.props.onModalClose(true);
}
//
// Render
render(props) {
return (
<RetagAuthorModalContent
{...this.props}
onRetagAuthorPress={this.onRetagAuthorPress}
/>
);
}
}
RetagAuthorModalContentConnector.propTypes = {
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(RetagAuthorModalContentConnector);
+303
View File
@@ -0,0 +1,303 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import FilterMenu from 'Components/Menu/FilterMenu';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import NoAuthor from 'Author/NoAuthor';
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
import RetagAuthorModal from './AudioTags/RetagAuthorModal';
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
import AuthorEditorFooter from './AuthorEditorFooter';
import AuthorEditorFilterModalConnector from './AuthorEditorFilterModalConnector';
function getColumns(showMetadataProfile) {
return [
{
name: 'status',
isSortable: true,
isVisible: true
},
{
name: 'sortName',
label: 'Name',
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true
},
{
name: 'metadataProfileId',
label: 'Metadata Profile',
isSortable: true,
isVisible: showMetadataProfile
},
{
name: 'path',
label: 'Path',
isSortable: true,
isVisible: true
},
{
name: 'tags',
label: 'Tags',
isSortable: false,
isVisible: true
}
];
}
class AuthorEditor extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isOrganizingAuthorModalOpen: false,
isRetaggingAuthorModalOpen: false,
columns: getColumns(props.showMetadataProfile)
};
}
componentDidUpdate(prevProps) {
const {
isDeleting,
deleteError
} = this.props;
const hasFinishedDeleting = prevProps.isDeleting &&
!isDeleting &&
!deleteError;
if (hasFinishedDeleting) {
this.onSelectAllChange({ value: false });
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
authorIds: this.getSelectedIds(),
...changes
});
}
onOrganizeAuthorPress = () => {
this.setState({ isOrganizingAuthorModalOpen: true });
}
onOrganizeAuthorModalClose = (organized) => {
this.setState({ isOrganizingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
onRetagAuthorPress = () => {
this.setState({ isRetaggingAuthorModalOpen: true });
}
onRetagAuthorModalClose = (organized) => {
this.setState({ isRetaggingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
isSaving,
saveError,
isDeleting,
deleteError,
isOrganizingAuthor,
isRetaggingAuthor,
showMetadataProfile,
onSortPress,
onFilterSelect
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
columns
} = this.state;
const selectedAuthorIds = this.getSelectedIds();
return (
<PageContent title="Author Editor">
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={AuthorEditorFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>{getErrorMessage(error, 'Failed to load author from API')}</div>
}
{
!error && isPopulated && !!items.length &&
<div>
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSortPress={onSortPress}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<AuthorEditorRowConnector
key={item.id}
{...item}
columns={columns}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
</div>
}
{
!error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} />
}
</PageContentBodyConnector>
<AuthorEditorFooter
authorIds={selectedAuthorIds}
selectedCount={selectedAuthorIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingAuthor={isOrganizingAuthor}
isRetaggingAuthor={isRetaggingAuthor}
showMetadataProfile={showMetadataProfile}
onSaveSelected={this.onSaveSelected}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
onRetagAuthorPress={this.onRetagAuthorPress}
/>
<OrganizeAuthorModal
isOpen={this.state.isOrganizingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onOrganizeAuthorModalClose}
/>
<RetagAuthorModal
isOpen={this.state.isRetaggingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onRetagAuthorModalClose}
/>
</PageContent>
);
}
}
AuthorEditor.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default AuthorEditor;
@@ -0,0 +1,92 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort } from 'Store/Actions/authorEditorActions';
import { fetchRootFolders } from 'Store/Actions/settingsActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import AuthorEditor from './AuthorEditor';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.metadataProfiles,
createClientSideCollectionSelector('authors', 'authorEditor'),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
(metadataProfiles, author, isOrganizingAuthor, isRetaggingAuthor) => {
return {
isOrganizingAuthor,
isRetaggingAuthor,
showMetadataProfile: metadataProfiles.items.length > 1,
...author
};
}
);
}
const mapDispatchToProps = {
dispatchSetAuthorEditorSort: setAuthorEditorSort,
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
dispatchSaveAuthorEditor: saveAuthorEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
};
class AuthorEditorConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
//
// Listeners
onSortPress = (sortKey) => {
this.props.dispatchSetAuthorEditorSort({ sortKey });
}
onFilterSelect = (selectedFilterKey) => {
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
}
onSaveSelected = (payload) => {
this.props.dispatchSaveAuthorEditor(payload);
}
onMoveSelected = (payload) => {
this.props.dispatchExecuteCommand({
name: commandNames.MOVE_AUTHOR,
...payload
});
}
//
// Render
render() {
return (
<AuthorEditor
{...this.props}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
/>
);
}
}
AuthorEditorConnector.propTypes = {
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector);
@@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions';
import FilterModal from 'Components/Filter/FilterModal';
function createMapStateToProps() {
return createSelector(
(state) => state.authors.items,
(state) => state.authorEditor.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'authorEditor'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setAuthorEditorFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
@@ -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,323 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import SelectInput from 'Components/Form/SelectInput';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import TagsModal from './Tags/TagsModal';
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import styles from './AuthorEditorFooter.css';
const NO_CHANGE = 'noChange';
class AuthorEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
isDeleteAuthorModalOpen: false,
isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
if (value === NO_CHANGE) {
return;
}
switch (name) {
case 'rootFolderPath':
this.setState({
isConfirmMoveModalOpen: true,
destinationRootFolder: value
});
break;
case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' });
break;
default:
this.props.onSaveSelected({ [name]: value });
}
}
onApplyTagsPress = (tags, applyTags) => {
this.setState({
savingTags: true,
isTagsModalOpen: false
});
this.props.onSaveSelected({
tags,
applyTags
});
}
onDeleteSelectedPress = () => {
this.setState({ isDeleteAuthorModalOpen: true });
}
onDeleteAuthorModalClose = () => {
this.setState({ isDeleteAuthorModalOpen: false });
}
onTagsPress = () => {
this.setState({ isTagsModalOpen: true });
}
onTagsModalClose = () => {
this.setState({ isTagsModalOpen: false });
}
onSaveRootFolderPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
}
onMoveAuthorPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({
rootFolderPath: this.state.destinationRootFolder,
moveFiles: true
});
}
//
// Render
render() {
const {
authorIds,
selectedCount,
isSaving,
isDeleting,
isOrganizingAuthor,
isRetaggingAuthor,
showMetadataProfile,
onOrganizeAuthorPress,
onRetagAuthorPress
} = this.props;
const {
monitored,
qualityProfileId,
metadataProfileId,
rootFolderPath,
savingTags,
isTagsModalOpen,
isDeleteAuthorModalOpen,
isConfirmMoveModalOpen,
destinationRootFolder
} = 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}>
<AuthorEditorFooterLabel
label="Monitor Author"
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
{
showMetadataProfile &&
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Metadata Profile"
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
includeNone={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
}
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={`${selectedCount} Author(s) Selected`}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isOrganizingAuthor}
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onOrganizeAuthorPress}
>
Rename Files
</SpinnerButton>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isRetaggingAuthor}
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onRetagAuthorPress}
>
Write Metadata Tags
</SpinnerButton>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags}
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={this.onTagsPress}
>
Set Readarr Tags
</SpinnerButton>
</div>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
</div>
</div>
</div>
<TagsModal
isOpen={isTagsModalOpen}
authorIds={authorIds}
onApplyTagsPress={this.onApplyTagsPress}
onModalClose={this.onTagsModalClose}
/>
<DeleteAuthorModal
isOpen={isDeleteAuthorModalOpen}
authorIds={authorIds}
onModalClose={this.onDeleteAuthorModalClose}
/>
<MoveAuthorModal
destinationRootFolder={destinationRootFolder}
isOpen={isConfirmMoveModalOpen}
onSavePress={this.onSaveRootFolderPress}
onMoveAuthorPress={this.onMoveAuthorPress}
/>
</PageContentFooter>
);
}
}
AuthorEditorFooter.propTypes = {
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired
};
export default AuthorEditorFooter;
@@ -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 { icons } from 'Helpers/Props';
import SpinnerIcon from 'Components/SpinnerIcon';
import styles from './AuthorEditorFooterLabel.css';
function AuthorEditorFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
AuthorEditorFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
AuthorEditorFooterLabel.defaultProps = {
className: styles.label
};
export default AuthorEditorFooterLabel;
@@ -0,0 +1,107 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TagListConnector from 'Components/TagListConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import AuthorNameLink from 'Author/AuthorNameLink';
import AuthorStatusCell from 'Author/Index/Table/AuthorStatusCell';
class AuthorEditorRow extends Component {
//
// Listeners
onBookFolderChange = () => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
//
}
//
// Render
render() {
const {
id,
status,
titleSlug,
authorName,
authorType,
monitored,
metadataProfile,
qualityProfile,
path,
tags,
columns,
isSelected,
onSelectedChange
} = this.props;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<AuthorStatusCell
authorType={authorType}
monitored={monitored}
status={status}
/>
<TableRowCell>
<AuthorNameLink
titleSlug={titleSlug}
authorName={authorName}
/>
</TableRowCell>
<TableRowCell>
{qualityProfile.name}
</TableRowCell>
{
_.find(columns, { name: 'metadataProfileId' }).isVisible &&
<TableRowCell>
{metadataProfile.name}
</TableRowCell>
}
<TableRowCell>
{path}
</TableRowCell>
<TableRowCell>
<TagListConnector
tags={tags}
/>
</TableRowCell>
</TableRow>
);
}
}
AuthorEditorRow.propTypes = {
id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
authorType: PropTypes.string,
monitored: PropTypes.bool.isRequired,
metadataProfile: PropTypes.object.isRequired,
qualityProfile: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
};
AuthorEditorRow.defaultProps = {
tags: []
};
export default AuthorEditorRow;
@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createMetadataProfileSelector from 'Store/Selectors/createMetadataProfileSelector';
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
import AuthorEditorRow from './AuthorEditorRow';
function createMapStateToProps() {
return createSelector(
createMetadataProfileSelector(),
createQualityProfileSelector(),
(metadataProfile, qualityProfile) => {
return {
metadataProfile,
qualityProfile
};
}
);
}
function AuthorEditorRowConnector(props) {
return (
<AuthorEditorRow
{...props}
/>
);
}
AuthorEditorRowConnector.propTypes = {
qualityProfileId: PropTypes.number.isRequired
};
export default connect(createMapStateToProps)(AuthorEditorRowConnector);
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DeleteAuthorModalContentConnector from './DeleteAuthorModalContentConnector';
function DeleteAuthorModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<DeleteAuthorModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteAuthorModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteAuthorModal;
@@ -0,0 +1,13 @@
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.pathContainer {
margin-left: 5px;
}
.path {
margin-left: 5px;
color: $dangerColor;
}
@@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import Button from 'Components/Link/Button';
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 styles from './DeleteAuthorModalContent.css';
class DeleteAuthorModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
}
onDeleteAuthorConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
this.setState({ deleteFiles: false });
this.props.onDeleteSelectedPress(deleteFiles);
}
//
// Render
render() {
const {
author,
onModalClose
} = this.props;
const deleteFiles = this.state.deleteFiles;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Delete Selected Author
</ModalHeader>
<ModalBody>
<div>
<FormGroup>
<FormLabel>{`Delete Author Folder${author.length > 1 ? 's' : ''}`}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={`Delete Author Folder${author.length > 1 ? 's' : ''} and all contents`}
kind={kinds.DANGER}
onChange={this.onDeleteFilesChange}
/>
</FormGroup>
</div>
<div className={styles.message}>
{`Are you sure you want to delete ${author.length} selected author${author.length > 1 ? 's' : ''}${deleteFiles ? ' and all contents' : ''}?`}
</div>
<ul>
{
author.map((s) => {
return (
<li key={s.authorName}>
<span>{s.authorName}</span>
{
deleteFiles &&
<span className={styles.pathContainer}>
-
<span className={styles.path}>
{s.path}
</span>
</span>
}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteAuthorConfirmed}
>
Delete
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteAuthorModalContent.propTypes = {
author: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired
};
export default DeleteAuthorModalContent;
@@ -0,0 +1,45 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions';
import DeleteAuthorModalContent from './DeleteAuthorModalContent';
function createMapStateToProps() {
return createSelector(
(state, { authorIds }) => authorIds,
createAllAuthorSelector(),
(authorIds, allAuthors) => {
const selectedAuthor = _.intersectionWith(allAuthors, authorIds, (s, id) => {
return s.id === id;
});
const sortedAuthor = _.orderBy(selectedAuthor, 'sortName');
const author = _.map(sortedAuthor, (s) => {
return {
authorName: s.authorName,
path: s.path
};
});
return {
author
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteSelectedPress(deleteFiles) {
dispatch(bulkDeleteAuthor({
authorIds: props.authorIds,
deleteFiles
}));
props.onModalClose();
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteAuthorModalContent);
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import OrganizeAuthorModalContentConnector from './OrganizeAuthorModalContentConnector';
function OrganizeAuthorModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<OrganizeAuthorModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
OrganizeAuthorModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default OrganizeAuthorModal;
@@ -0,0 +1,8 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
@@ -0,0 +1,74 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons, kinds } from 'Helpers/Props';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import Icon from 'Components/Icon';
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 styles from './OrganizeAuthorModalContent.css';
function OrganizeAuthorModalContent(props) {
const {
authorNames,
onModalClose,
onOrganizeAuthorPress
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Organize Selected Author
</ModalHeader>
<ModalBody>
<Alert>
Tip: To preview a rename... select "Cancel" then click any author name and use the
<Icon
className={styles.renameIcon}
name={icons.ORGANIZE}
/>
</Alert>
<div className={styles.message}>
Are you sure you want to organize all files in the {authorNames.length} selected author?
</div>
<ul>
{
authorNames.map((authorName) => {
return (
<li key={authorName}>
{authorName}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={onOrganizeAuthorPress}
>
Organize
</Button>
</ModalFooter>
</ModalContent>
);
}
OrganizeAuthorModalContent.propTypes = {
authorNames: PropTypes.arrayOf(PropTypes.string).isRequired,
onModalClose: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired
};
export default OrganizeAuthorModalContent;
@@ -0,0 +1,67 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import OrganizeAuthorModalContent from './OrganizeAuthorModalContent';
function createMapStateToProps() {
return createSelector(
(state, { authorIds }) => authorIds,
createAllAuthorSelector(),
(authorIds, allAuthors) => {
const author = _.intersectionWith(allAuthors, authorIds, (s, id) => {
return s.id === id;
});
const sortedAuthor = _.orderBy(author, 'sortName');
const authorNames = _.map(sortedAuthor, 'authorName');
return {
authorNames
};
}
);
}
const mapDispatchToProps = {
executeCommand
};
class OrganizeAuthorModalContentConnector extends Component {
//
// Listeners
onOrganizeAuthorPress = () => {
this.props.executeCommand({
name: commandNames.RENAME_AUTHOR,
authorIds: this.props.authorIds
});
this.props.onModalClose(true);
}
//
// Render
render(props) {
return (
<OrganizeAuthorModalContent
{...this.props}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
/>
);
}
}
OrganizeAuthorModalContentConnector.propTypes = {
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeAuthorModalContentConnector);
@@ -0,0 +1,31 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import TagsModalContentConnector from './TagsModalContentConnector';
function TagsModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<TagsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
TagsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default TagsModal;
@@ -0,0 +1,12 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}
@@ -0,0 +1,187 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import Label from 'Components/Label';
import Button from 'Components/Link/Button';
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 styles from './TagsModalContent.css';
class TagsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
tags: [],
applyTags: 'add'
};
}
//
// Lifecycle
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
}
onApplyTagsPress = () => {
const {
tags,
applyTags
} = this.state;
this.props.onApplyTagsPress(tags, applyTags);
}
//
// Render
render() {
const {
authorTags,
tagList,
onModalClose
} = this.props;
const {
tags,
applyTags
} = this.state;
const applyTagsOptions = [
{ key: 'add', value: 'Add' },
{ key: 'remove', value: 'Remove' },
{ key: 'replace', value: 'Replace' }
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Tags
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Apply Tags</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="applyTags"
value={applyTags}
values={applyTagsOptions}
helpTexts={[
'How to apply tags to the selected author',
'Add: Add the tags the existing list of tags',
'Remove: Remove the entered tags',
'Replace: Replace the tags with the entered tags (enter no tags to clear all tags)'
]}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Result</FormLabel>
<div className={styles.result}>
{
authorTags.map((t) => {
const tag = _.find(tagList, { id: t });
if (!tag) {
return null;
}
const removeTag = (applyTags === 'remove' && tags.indexOf(t) > -1) ||
(applyTags === 'replace' && tags.indexOf(t) === -1);
return (
<Label
key={tag.id}
title={removeTag ? 'Removing tag' : 'Existing tag'}
kind={removeTag ? kinds.INVERSE : kinds.INFO}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})
}
{
(applyTags === 'add' || applyTags === 'replace') &&
tags.map((t) => {
const tag = _.find(tagList, { id: t });
if (!tag) {
return null;
}
if (authorTags.indexOf(t) > -1) {
return null;
}
return (
<Label
key={tag.id}
title={'Adding tag'}
kind={kinds.SUCCESS}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})
}
</div>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onApplyTagsPress}
>
Apply
</Button>
</ModalFooter>
</ModalContent>
);
}
}
TagsModalContent.propTypes = {
authorTags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onApplyTagsPress: PropTypes.func.isRequired
};
export default TagsModalContent;
@@ -0,0 +1,36 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import TagsModalContent from './TagsModalContent';
function createMapStateToProps() {
return createSelector(
(state, { authorIds }) => authorIds,
createAllAuthorSelector(),
createTagsSelector(),
(authorIds, allAuthors, tagList) => {
const author = _.intersectionWith(allAuthors, authorIds, (s, id) => {
return s.id === id;
});
const authorTags = _.uniq(_.concat(..._.map(author, 'tags')));
return {
authorTags,
tagList
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onAction() {
// Do something
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(TagsModalContent);