Remove Artists Editor

(cherry picked from commit 67232290e5fb94c96734a8ca59aad0f7de79fa20)
pull/4254/head
Mark McDowall 1 year ago committed by Bogdan
parent 9a4c6a8db8
commit 57206da77b

@ -7,7 +7,6 @@ import QueueConnector from 'Activity/Queue/QueueConnector';
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
import ArtistIndex from 'Artist/Index/ArtistIndex';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import NotFound from 'Components/NotFound';
@ -78,7 +77,15 @@ function AppRoutes(props) {
<Route
path="/artisteditor"
component={ArtistEditorConnector}
exact={true}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}}
/>
<Route

@ -1,281 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import NoArtist from 'Artist/NoArtist';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import ArtistEditorFilterModalConnector from './ArtistEditorFilterModalConnector';
import ArtistEditorFooter from './ArtistEditorFooter';
import ArtistEditorRowConnector from './ArtistEditorRowConnector';
import RetagArtistModal from './AudioTags/RetagArtistModal';
import OrganizeArtistModal from './Organize/OrganizeArtistModal';
class ArtistEditor extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isOrganizingArtistModalOpen: false,
isRetaggingArtistModalOpen: false
};
}
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({
artistIds: this.getSelectedIds(),
...changes
});
};
onOrganizeArtistPress = () => {
this.setState({ isOrganizingArtistModalOpen: true });
};
onOrganizeArtistModalClose = (organized) => {
this.setState({ isOrganizingArtistModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
};
onRetagArtistPress = () => {
this.setState({ isRetaggingArtistModalOpen: true });
};
onRetagArtistModalClose = (organized) => {
this.setState({ isRetaggingArtistModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
isSaving,
saveError,
isDeleting,
deleteError,
isOrganizingArtist,
isRetaggingArtist,
onTableOptionChange,
onSortPress,
onFilterSelect
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
const selectedArtistIds = this.getSelectedIds();
return (
<PageContent title={translate('ArtistEditor')}>
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={ArtistEditorFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>{getErrorMessage(error, 'Failed to load artist 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 (
<ArtistEditorRowConnector
key={item.id}
{...item}
columns={columns}
isSaving={isSaving}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
</div>
}
{
!error && isPopulated && !items.length &&
<NoArtist totalItems={totalItems} />
}
</PageContentBody>
<ArtistEditorFooter
artistIds={selectedArtistIds}
selectedCount={selectedArtistIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingArtist={isOrganizingArtist}
isRetaggingArtist={isRetaggingArtist}
columns={columns}
showMetadataProfile={columns.find((column) => column.name === 'metadataProfileId').isVisible}
onSaveSelected={this.onSaveSelected}
onOrganizeArtistPress={this.onOrganizeArtistPress}
onRetagArtistPress={this.onRetagArtistPress}
/>
<OrganizeArtistModal
isOpen={this.state.isOrganizingArtistModalOpen}
artistIds={selectedArtistIds}
onModalClose={this.onOrganizeArtistModalClose}
/>
<RetagArtistModal
isOpen={this.state.isRetaggingArtistModalOpen}
artistIds={selectedArtistIds}
onModalClose={this.onRetagArtistModalClose}
/>
</PageContent>
);
}
}
ArtistEditor.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: 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,
isOrganizingArtist: PropTypes.bool.isRequired,
isRetaggingArtist: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default ArtistEditor;

@ -1,97 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { saveArtistEditor, setArtistEditorFilter, setArtistEditorSort, setArtistEditorTableOption } from 'Store/Actions/artistEditorActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchRootFolders } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import ArtistEditor from './ArtistEditor';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('artist', 'artistEditor'),
createCommandExecutingSelector(commandNames.RENAME_ARTIST),
createCommandExecutingSelector(commandNames.RETAG_ARTIST),
(artist, isOrganizingArtist, isRetaggingArtist) => {
return {
isOrganizingArtist,
isRetaggingArtist,
...artist
};
}
);
}
const mapDispatchToProps = {
dispatchSetArtistEditorSort: setArtistEditorSort,
dispatchSetArtistEditorFilter: setArtistEditorFilter,
dispatchSetArtistEditorTableOption: setArtistEditorTableOption,
dispatchSaveArtistEditor: saveArtistEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
};
class ArtistEditorConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
//
// Listeners
onSortPress = (sortKey) => {
this.props.dispatchSetArtistEditorSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.dispatchSetArtistEditorFilter({ selectedFilterKey });
};
onTableOptionChange = (payload) => {
this.props.dispatchSetArtistEditorTableOption(payload);
};
onSaveSelected = (payload) => {
this.props.dispatchSaveArtistEditor(payload);
};
onMoveSelected = (payload) => {
this.props.dispatchExecuteCommand({
name: commandNames.MOVE_ARTIST,
...payload
});
};
//
// Render
render() {
return (
<ArtistEditor
{...this.props}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
}
ArtistEditorConnector.propTypes = {
dispatchSetArtistEditorSort: PropTypes.func.isRequired,
dispatchSetArtistEditorFilter: PropTypes.func.isRequired,
dispatchSetArtistEditorTableOption: PropTypes.func.isRequired,
dispatchSaveArtistEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistEditorConnector);

@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setArtistEditorFilter } from 'Store/Actions/artistEditorActions';
function createMapStateToProps() {
return createSelector(
(state) => state.artist.items,
(state) => state.artistEditor.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'artistEditor'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setArtistEditorFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

@ -1,70 +0,0 @@
.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;
}
.selectedArtistLabel {
text-align: left;
}
}

@ -1,14 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'buttonContainer': string;
'buttonContainerContent': string;
'buttons': string;
'deleteSelectedButton': string;
'inputContainer': string;
'organizeSelectedButton': string;
'selectedArtistLabel': string;
'tagsButton': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,382 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal';
import FormInputGroup from 'Components/Form/FormInputGroup';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
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 { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import ArtistEditorFooterLabel from './ArtistEditorFooterLabel';
import DeleteArtistModal from './Delete/DeleteArtistModal';
import TagsModal from './Tags/TagsModal';
import styles from './ArtistEditorFooter.css';
const NO_CHANGE = 'noChange';
class ArtistEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
isDeleteArtistModalOpen: false,
isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
monitorNewItems: 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({ isDeleteArtistModalOpen: true });
};
onDeleteArtistModalClose = () => {
this.setState({ isDeleteArtistModalOpen: 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 });
};
onMoveArtistPress = () => {
this.setState({
isConfirmMoveModalOpen: false,
destinationRootFolder: null
});
this.props.onSaveSelected({
rootFolderPath: this.state.destinationRootFolder,
moveFiles: true
});
};
//
// Render
render() {
const {
artistIds,
selectedCount,
isSaving,
isDeleting,
isOrganizingArtist,
isRetaggingArtist,
columns,
onOrganizeArtistPress,
onRetagArtistPress
} = this.props;
const {
monitored,
monitorNewItems,
qualityProfileId,
metadataProfileId,
rootFolderPath,
savingTags,
isTagsModalOpen,
isDeleteArtistModalOpen,
isConfirmMoveModalOpen,
destinationRootFolder
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: 'Monitored' },
{ key: 'unmonitored', value: 'Unmonitored' }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<ArtistEditorFooterLabel
label={translate('MonitorArtist')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<FormInputGroup
type={inputTypes.SELECT}
name="monitored"
value={monitored}
values={monitoredOptions}
includeNoChangeDisabled={false}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<ArtistEditorFooterLabel
label={translate('MonitorNewItems')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
includeNoChangeDisabled={false}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'qualityProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<ArtistEditorFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
includeNoChangeDisabled={false}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'metadataProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<ArtistEditorFooterLabel
label={translate('MetadataProfile')}
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
includeNoChangeDisabled={false}
includeNone={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'path') {
return (
<div
key={name}
className={styles.inputContainer}
>
<ArtistEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
);
}
return null;
})
}
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<ArtistEditorFooterLabel
label={translate('SelectedCountArtistsSelectedInterp', { selectedCount })}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isOrganizingArtist}
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
onPress={onOrganizeArtistPress}
>
Rename Files
</SpinnerButton>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isRetaggingArtist}
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
onPress={onRetagArtistPress}
>
Write Metadata Tags
</SpinnerButton>
<SpinnerButton
className={styles.tagsButton}
isSpinning={isSaving && savingTags}
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
onPress={this.onTagsPress}
>
Set Lidarr 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}
artistIds={artistIds}
onApplyTagsPress={this.onApplyTagsPress}
onModalClose={this.onTagsModalClose}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistIds={artistIds}
onModalClose={this.onDeleteArtistModalClose}
/>
<MoveArtistModal
destinationRootFolder={destinationRootFolder}
isOpen={isConfirmMoveModalOpen}
onSavePress={this.onSaveRootFolderPress}
onMoveArtistPress={this.onMoveArtistPress}
/>
</PageContentFooter>
);
}
}
ArtistEditorFooter.propTypes = {
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingArtist: PropTypes.bool.isRequired,
isRetaggingArtist: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeArtistPress: PropTypes.func.isRequired,
onRetagArtistPress: PropTypes.func.isRequired
};
export default ArtistEditorFooter;

@ -1,8 +0,0 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}

@ -1,8 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'label': string;
'savingIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import SpinnerIcon from 'Components/SpinnerIcon';
import { icons } from 'Helpers/Props';
import styles from './ArtistEditorFooterLabel.css';
function ArtistEditorFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
ArtistEditorFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
ArtistEditorFooterLabel.defaultProps = {
className: styles.label
};
export default ArtistEditorFooterLabel;

@ -1,168 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ArtistNameLink from 'Artist/ArtistNameLink';
import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import monitorNewItemsOptions from 'Utilities/Artist/monitorNewItemsOptions';
import formatBytes from 'Utilities/Number/formatBytes';
class ArtistEditorRow extends Component {
//
// Render
render() {
const {
id,
status,
foreignArtistId,
artistName,
artistType,
monitored,
monitorNewItems,
metadataProfile,
qualityProfile,
path,
statistics,
tags,
columns,
isSaving,
isSelected,
onArtistMonitoredPress,
onSelectedChange
} = this.props;
const monitorNewItemsName = monitorNewItemsOptions.find((o) => o.key === monitorNewItems)?.value;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<ArtistStatusCell
key={name}
artistType={artistType}
monitored={monitored}
status={status}
isSaving={isSaving}
onMonitoredPress={onArtistMonitoredPress}
/>
);
}
if (name === 'sortName') {
return (
<TableRowCell
key={name}
>
<ArtistNameLink
foreignArtistId={foreignArtistId}
artistName={artistName}
/>
</TableRowCell>
);
}
if (name === 'monitorNewItems') {
return (
<TableRowCell key={name}>
{monitorNewItemsName ?? 'Unknown'}
</TableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<TableRowCell key={name}>
{qualityProfile.name}
</TableRowCell>
);
}
if (name === 'metadataProfileId') {
return (
<TableRowCell key={name}>
{metadataProfile.name}
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{path}
</TableRowCell>
);
}
if (name === 'sizeOnDisk') {
return (
<TableRowCell key={name}>
{formatBytes(statistics.sizeOnDisk)}
</TableRowCell>
);
}
if (name === 'tags') {
return (
<TableRowCell key={name}>
<TagListConnector
tags={tags}
/>
</TableRowCell>
);
}
return null;
})
}
</TableRow>
);
}
}
ArtistEditorRow.propTypes = {
id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
artistType: PropTypes.string,
monitored: PropTypes.bool.isRequired,
monitorNewItems: PropTypes.string.isRequired,
metadataProfile: PropTypes.object.isRequired,
qualityProfile: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onArtistMonitoredPress: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired
};
ArtistEditorRow.defaultProps = {
tags: [],
statistics: {}
};
export default ArtistEditorRow;

@ -1,61 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
import createMetadataProfileSelector from 'Store/Selectors/createMetadataProfileSelector';
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
import ArtistEditorRow from './ArtistEditorRow';
function createMapStateToProps() {
return createSelector(
createMetadataProfileSelector(),
createQualityProfileSelector(),
(metadataProfile, qualityProfile) => {
return {
metadataProfile,
qualityProfile
};
}
);
}
const mapDispatchToProps = {
toggleArtistMonitored
};
class ArtistEditorRowConnector extends Component {
//
// Listeners
onArtistMonitoredPress = () => {
const {
id,
monitored
} = this.props;
this.props.toggleArtistMonitored({
artistId: id,
monitored: !monitored
});
};
render() {
return (
<ArtistEditorRow
{...this.props}
onArtistMonitoredPress={this.onArtistMonitoredPress}
/>
);
}
}
ArtistEditorRowConnector.propTypes = {
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
qualityProfileId: PropTypes.number.isRequired,
toggleArtistMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistEditorRowConnector);

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import RetagArtistModalContentConnector from './RetagArtistModalContentConnector';
function RetagArtistModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<RetagArtistModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
RetagArtistModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RetagArtistModal;

@ -1,8 +0,0 @@
.retagIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}

@ -1,8 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'retagIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,74 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
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 { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RetagArtistModalContent.css';
function RetagArtistModalContent(props) {
const {
artistNames,
onModalClose,
onRetagArtistPress
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Retag Selected Artist
</ModalHeader>
<ModalBody>
<Alert>
Tip: To preview the tags that will be written... select "Cancel" then click any artist 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 {artistNames.length} selected artist?
</div>
<ul>
{
artistNames.map((artistName) => {
return (
<li key={artistName}>
{artistName}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.DANGER}
onPress={onRetagArtistPress}
>
{translate('Retag')}
</Button>
</ModalFooter>
</ModalContent>
);
}
RetagArtistModalContent.propTypes = {
artistNames: PropTypes.arrayOf(PropTypes.string).isRequired,
onModalClose: PropTypes.func.isRequired,
onRetagArtistPress: PropTypes.func.isRequired
};
export default RetagArtistModalContent;

@ -1,67 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import RetagArtistModalContent from './RetagArtistModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
createAllArtistSelector(),
(artistIds, allArtists) => {
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
return s.id === id;
});
const sortedArtist = _.orderBy(artist, 'sortName');
const artistNames = _.map(sortedArtist, 'artistName');
return {
artistNames
};
}
);
}
const mapDispatchToProps = {
executeCommand
};
class RetagArtistModalContentConnector extends Component {
//
// Listeners
onRetagArtistPress = () => {
this.props.executeCommand({
name: commandNames.RETAG_ARTIST,
artistIds: this.props.artistIds
});
this.props.onModalClose(true);
};
//
// Render
render(props) {
return (
<RetagArtistModalContent
{...this.props}
onRetagArtistPress={this.onRetagArtistPress}
/>
);
}
}
RetagArtistModalContentConnector.propTypes = {
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(RetagArtistModalContentConnector);

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DeleteArtistModalContentConnector from './DeleteArtistModalContentConnector';
function DeleteArtistModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<DeleteArtistModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteArtistModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteArtistModal;

@ -1,13 +0,0 @@
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.pathContainer {
margin-left: 5px;
}
.path {
margin-left: 5px;
color: var(--dangerColor);
}

@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'path': string;
'pathContainer': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,124 +0,0 @@
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 './DeleteArtistModalContent.css';
class DeleteArtistModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onDeleteArtistConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
this.setState({ deleteFiles: false });
this.props.onDeleteSelectedPress(deleteFiles);
};
//
// Render
render() {
const {
artist,
onModalClose
} = this.props;
const deleteFiles = this.state.deleteFiles;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('DeleteArtist')}
</ModalHeader>
<ModalBody>
<div>
<FormGroup>
<FormLabel>{`Delete Artist Folder${artist.length > 1 ? 's' : ''}`}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={`Delete Artist Folder${artist.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 ${artist.length} selected artist${artist.length > 1 ? 's' : ''}${deleteFiles ? ' and all contents' : ''}?`}
</div>
<ul>
{
artist.map((s) => {
return (
<li key={s.artistName}>
<span>{s.artistName}</span>
{
deleteFiles &&
<span className={styles.pathContainer}>
-
<span className={styles.path}>
{s.path}
</span>
</span>
}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteArtistConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteArtistModalContent.propTypes = {
artist: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired
};
export default DeleteArtistModalContent;

@ -1,45 +0,0 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { bulkDeleteArtist } from 'Store/Actions/artistEditorActions';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import DeleteArtistModalContent from './DeleteArtistModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
createAllArtistSelector(),
(artistIds, allArtists) => {
const selectedArtist = _.intersectionWith(allArtists, artistIds, (s, id) => {
return s.id === id;
});
const sortedArtist = _.orderBy(selectedArtist, 'sortName');
const artist = _.map(sortedArtist, (s) => {
return {
artistName: s.artistName,
path: s.path
};
});
return {
artist
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteSelectedPress(deleteFiles) {
dispatch(bulkDeleteArtist({
artistIds: props.artistIds,
deleteFiles
}));
props.onModalClose();
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteArtistModalContent);

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import OrganizeArtistModalContentConnector from './OrganizeArtistModalContentConnector';
function OrganizeArtistModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<OrganizeArtistModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
OrganizeArtistModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default OrganizeArtistModal;

@ -1,8 +0,0 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}

@ -1,8 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'renameIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,75 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
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 { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './OrganizeArtistModalContent.css';
function OrganizeArtistModalContent(props) {
const {
artistNames,
onModalClose,
onOrganizeArtistPress
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('OrganizeSelectedArtists')}
</ModalHeader>
<ModalBody>
<Alert>
Tip: To preview a rename, select "Cancel", then select any artist 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 {artistNames.length} selected artist?
</div>
<ul>
{
artistNames.map((artistName) => {
return (
<li key={artistName}>
{artistName}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.DANGER}
onPress={onOrganizeArtistPress}
>
{translate('Organize')}
</Button>
</ModalFooter>
</ModalContent>
);
}
OrganizeArtistModalContent.propTypes = {
artistNames: PropTypes.arrayOf(PropTypes.string).isRequired,
onModalClose: PropTypes.func.isRequired,
onOrganizeArtistPress: PropTypes.func.isRequired
};
export default OrganizeArtistModalContent;

@ -1,67 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import OrganizeArtistModalContent from './OrganizeArtistModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
createAllArtistSelector(),
(artistIds, allArtists) => {
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
return s.id === id;
});
const sortedArtist = _.orderBy(artist, 'sortName');
const artistNames = _.map(sortedArtist, 'artistName');
return {
artistNames
};
}
);
}
const mapDispatchToProps = {
executeCommand
};
class OrganizeArtistModalContentConnector extends Component {
//
// Listeners
onOrganizeArtistPress = () => {
this.props.executeCommand({
name: commandNames.RENAME_ARTIST,
artistIds: this.props.artistIds
});
this.props.onModalClose(true);
};
//
// Render
render(props) {
return (
<OrganizeArtistModalContent
{...this.props}
onOrganizeArtistPress={this.onOrganizeArtistPress}
/>
);
}
}
OrganizeArtistModalContentConnector.propTypes = {
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeArtistModalContentConnector);

@ -1,31 +0,0 @@
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;

@ -1,12 +0,0 @@
.renameIcon {
margin-left: 5px;
}
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.result {
padding-top: 4px;
}

@ -1,9 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'message': string;
'renameIcon': string;
'result': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,194 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Label from 'Components/Label';
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, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
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 {
artistTags,
tagList,
onModalClose
} = this.props;
const {
tags,
applyTags
} = this.state;
const applyTagsOptions = [
{ key: 'add', value: translate('Add') },
{ key: 'remove', value: translate('Remove') },
{ key: 'replace', value: translate('Replace') }
];
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Tags
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>
{translate('Tags')}
</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('ApplyTags')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="applyTags"
value={applyTags}
values={applyTagsOptions}
helpTexts={[
translate('ApplyTagsHelpTextHowToApplyArtists'),
translate('ApplyTagsHelpTextAdd'),
translate('ApplyTagsHelpTextRemove'),
translate('ApplyTagsHelpTextReplace')
]}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('Result')}
</FormLabel>
<div className={styles.result}>
{
artistTags.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 ? translate('RemoveTagRemovingTag') : translate('RemoveTagExistingTag')}
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 (artistTags.indexOf(t) > -1) {
return null;
}
return (
<Label
key={tag.id}
title={translate('AddingTag')}
kind={kinds.SUCCESS}
size={sizes.LARGE}
>
{tag.label}
</Label>
);
})
}
</div>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={this.onApplyTagsPress}
>
{translate('Apply')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
TagsModalContent.propTypes = {
artistTags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onApplyTagsPress: PropTypes.func.isRequired
};
export default TagsModalContent;

@ -1,36 +0,0 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import TagsModalContent from './TagsModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
createAllArtistSelector(),
createTagsSelector(),
(artistIds, allArtists, tagList) => {
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
return s.id === id;
});
const artistTags = _.uniq(_.concat(..._.map(artist, 'tags')));
return {
artistTags,
tagList
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onAction() {
// Do something
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(TagsModalContent);

@ -29,10 +29,6 @@ const links = [
title: () => translate('AddNew'),
to: '/add/search'
},
{
title: () => translate('MassEditor'),
to: '/artisteditor'
},
{
title: () => translate('AlbumStudio'),
to: '/albumstudio'

@ -1,259 +0,0 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import translate from 'Utilities/String/translate';
import { filterPredicates, filters, sortPredicates } from './artistActions';
import { set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
//
// Variables
export const section = 'artistEditor';
//
// State
export const defaultState = {
isSaving: false,
saveError: null,
isDeleting: false,
deleteError: null,
sortKey: 'sortName',
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'sortName',
secondarySortDirection: sortDirections.ASCENDING,
selectedFilterKey: 'all',
filters,
filterPredicates,
columns: [
{
name: 'status',
columnLabel: () => translate('Status'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'sortName',
label: () => translate('Name'),
isSortable: true,
isVisible: true
},
{
name: 'monitorNewItems',
label: () => translate('MonitorNewItems'),
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: () => translate('QualityProfile'),
isSortable: true,
isVisible: true
},
{
name: 'metadataProfileId',
label: () => translate('MetadataProfile'),
isSortable: true,
isVisible: true
},
{
name: 'path',
label: () => translate('Path'),
isSortable: true,
isVisible: true
},
{
name: 'sizeOnDisk',
label: () => translate('SizeOnDisk'),
isSortable: true,
isVisible: false
},
{
name: 'tags',
label: () => translate('Tags'),
isSortable: true,
isVisible: true
}
],
filterBuilderProps: [
{
name: 'monitored',
label: () => translate('Monitored'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'status',
label: () => translate('Status'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.ARTIST_STATUS
},
{
name: 'qualityProfileId',
label: () => translate('QualityProfile'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.QUALITY_PROFILE
},
{
name: 'metadataProfileId',
label: () => translate('MetadataProfile'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.METADATA_PROFILE
},
{
name: 'path',
label: () => translate('Path'),
type: filterBuilderTypes.STRING
},
{
name: 'rootFolderPath',
label: () => translate('RootFolderPath'),
type: filterBuilderTypes.EXACT
},
{
name: 'sizeOnDisk',
label: () => translate('SizeOnDisk'),
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{
name: 'tags',
label: () => translate('Tags'),
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG
}
],
sortPredicates
};
export const persistState = [
'artistEditor.sortKey',
'artistEditor.sortDirection',
'artistEditor.selectedFilterKey',
'artistEditor.customFilters',
'artistEditor.columns'
];
//
// Actions Types
export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort';
export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter';
export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor';
export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist';
export const SET_ARTIST_EDITOR_TABLE_OPTION = 'artistEditor/setArtistEditorTableOption';
//
// Action Creators
export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT);
export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER);
export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR);
export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST);
export const setArtistEditorTableOption = createAction(SET_ARTIST_EDITOR_TABLE_OPTION);
//
// Action Handlers
export const actionHandlers = handleThunks({
[SAVE_ARTIST_EDITOR]: function(getState, payload, dispatch) {
dispatch(set({
section,
isSaving: true
}));
const promise = createAjaxRequest({
url: '/artist/editor',
method: 'PUT',
data: JSON.stringify(payload),
dataType: 'json'
}).request;
promise.done((data) => {
dispatch(batchActions([
...data.map((artist) => {
const {
images,
rootFolderPath,
statistics,
...propsToUpdate
} = artist;
return updateItem({
id: artist.id,
section: 'artist',
...propsToUpdate
});
}),
set({
section,
isSaving: false,
saveError: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isSaving: false,
saveError: xhr
}));
});
},
[BULK_DELETE_ARTIST]: function(getState, payload, dispatch) {
dispatch(set({
section,
isDeleting: true
}));
const promise = createAjaxRequest({
url: '/artist/editor',
method: 'DELETE',
data: JSON.stringify(payload),
dataType: 'json'
}).request;
promise.done(() => {
// SignalR will take care of removing the artist from the collection
dispatch(set({
section,
isDeleting: false,
deleteError: null
}));
});
promise.fail((xhr) => {
dispatch(set({
section,
isDeleting: false,
deleteError: xhr
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[SET_ARTIST_EDITOR_TABLE_OPTION]: createSetTableOptionReducer(section),
[SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
}, defaultState, section);

@ -3,7 +3,6 @@ import * as albumHistory from './albumHistoryActions';
import * as albumStudio from './albumStudioActions';
import * as app from './appActions';
import * as artist from './artistActions';
import * as artistEditor from './artistEditorActions';
import * as artistHistory from './artistHistoryActions';
import * as artistIndex from './artistIndexActions';
import * as blocklist from './blocklistActions';
@ -49,7 +48,6 @@ export default [
releases,
albumStudio,
artist,
artistEditor,
artistHistory,
artistIndex,
search,

@ -575,7 +575,6 @@
"MarkAsFailedMessageText": "Are you sure you want to mark '{0}' as failed?",
"MassAlbumsCutoffUnmetWarning": "Are you sure you want to search for all '{0}' Cutoff Unmet albums?",
"MassAlbumsSearchWarning": "Are you sure you want to search for all '{0}' missing albums?",
"MassEditor": "Mass Editor",
"MaximumLimits": "Maximum Limits",
"MaximumSize": "Maximum Size",
"MaximumSizeHelpText": "Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited.",

Loading…
Cancel
Save