mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-06 04:52:21 -07:00
New: Artist History Modal in Artist Details Page
This commit is contained in:
parent
0981260887
commit
7e4a8c8ff7
17 changed files with 659 additions and 96 deletions
|
@ -1,3 +0,0 @@
|
||||||
.absoluteEpisodeNumber {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
|
@ -1,15 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import padNumber from 'Utilities/Number/padNumber';
|
import EpisodeNumber from './EpisodeNumber';
|
||||||
import styles from './SeasonEpisodeNumber.css';
|
|
||||||
|
|
||||||
function SeasonEpisodeNumber(props) {
|
function SeasonEpisodeNumber(props) {
|
||||||
const {
|
const {
|
||||||
seasonNumber,
|
|
||||||
episodeNumber,
|
|
||||||
absoluteEpisodeNumber,
|
|
||||||
airDate,
|
airDate,
|
||||||
artistType
|
artistType,
|
||||||
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (artistType === 'daily' && airDate) {
|
if (artistType === 'daily' && airDate) {
|
||||||
|
@ -18,32 +15,16 @@ function SeasonEpisodeNumber(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artistType === 'anime') {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
|
||||||
|
|
||||||
{
|
|
||||||
absoluteEpisodeNumber &&
|
|
||||||
<span className={styles.absoluteEpisodeNumber}>
|
|
||||||
({absoluteEpisodeNumber})
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<EpisodeNumber
|
||||||
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
seriesType={artistType}
|
||||||
</span>
|
showSeasonNumber={true}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SeasonEpisodeNumber.propTypes = {
|
SeasonEpisodeNumber.propTypes = {
|
||||||
seasonNumber: PropTypes.number.isRequired,
|
|
||||||
episodeNumber: PropTypes.number.isRequired,
|
|
||||||
absoluteEpisodeNumber: PropTypes.number,
|
|
||||||
airDate: PropTypes.string,
|
airDate: PropTypes.string,
|
||||||
artistType: PropTypes.string
|
artistType: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,7 @@ import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfil
|
||||||
import ArtistPoster from 'Artist/ArtistPoster';
|
import ArtistPoster from 'Artist/ArtistPoster';
|
||||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||||
|
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||||
import ArtistAlternateTitles from './ArtistAlternateTitles';
|
import ArtistAlternateTitles from './ArtistAlternateTitles';
|
||||||
import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
|
import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
|
||||||
import ArtistTagsConnector from './ArtistTagsConnector';
|
import ArtistTagsConnector from './ArtistTagsConnector';
|
||||||
|
@ -92,6 +93,7 @@ class ArtistDetails extends Component {
|
||||||
isManageEpisodesOpen: false,
|
isManageEpisodesOpen: false,
|
||||||
isEditArtistModalOpen: false,
|
isEditArtistModalOpen: false,
|
||||||
isDeleteArtistModalOpen: false,
|
isDeleteArtistModalOpen: false,
|
||||||
|
isArtistHistoryModalOpen: false,
|
||||||
allExpanded: false,
|
allExpanded: false,
|
||||||
allCollapsed: false,
|
allCollapsed: false,
|
||||||
expandedState: {}
|
expandedState: {}
|
||||||
|
@ -136,6 +138,14 @@ class ArtistDetails extends Component {
|
||||||
this.setState({ isDeleteArtistModalOpen: false });
|
this.setState({ isDeleteArtistModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onArtistHistoryPress = () => {
|
||||||
|
this.setState({ isArtistHistoryModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onArtistHistoryModalClose = () => {
|
||||||
|
this.setState({ isArtistHistoryModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
onExpandAllPress = () => {
|
onExpandAllPress = () => {
|
||||||
const {
|
const {
|
||||||
allExpanded,
|
allExpanded,
|
||||||
|
@ -197,6 +207,7 @@ class ArtistDetails extends Component {
|
||||||
isManageEpisodesOpen,
|
isManageEpisodesOpen,
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen,
|
isDeleteArtistModalOpen,
|
||||||
|
isArtistHistoryModalOpen,
|
||||||
allExpanded,
|
allExpanded,
|
||||||
allCollapsed,
|
allCollapsed,
|
||||||
expandedState
|
expandedState
|
||||||
|
@ -254,6 +265,12 @@ class ArtistDetails extends Component {
|
||||||
onPress={this.onManageEpisodesPress}
|
onPress={this.onManageEpisodesPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="History"
|
||||||
|
iconName={icons.HISTORY}
|
||||||
|
onPress={this.onArtistHistoryPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
@ -540,6 +557,12 @@ class ArtistDetails extends Component {
|
||||||
onModalClose={this.onManageEpisodesModalClose}
|
onModalClose={this.onManageEpisodesModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ArtistHistoryModal
|
||||||
|
isOpen={isArtistHistoryModalOpen}
|
||||||
|
artistId={id}
|
||||||
|
onModalClose={this.onArtistHistoryModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<EditArtistModalConnector
|
<EditArtistModalConnector
|
||||||
isOpen={isEditArtistModalOpen}
|
isOpen={isEditArtistModalOpen}
|
||||||
artistId={id}
|
artistId={id}
|
||||||
|
|
|
@ -171,71 +171,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
}
|
}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{
|
|
||||||
isSmallScreen ?
|
|
||||||
<Menu
|
|
||||||
className={styles.actionsMenu}
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
enforceMaxHeight={false}
|
|
||||||
>
|
|
||||||
<MenuButton>
|
|
||||||
<Icon
|
|
||||||
name={icons.ACTIONS}
|
|
||||||
size={22}
|
|
||||||
/>
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
<MenuContent className={styles.actionsMenuContent}>
|
|
||||||
<MenuItem
|
|
||||||
isDisabled={isSearching}
|
|
||||||
onPress={onSearchPress}
|
|
||||||
>
|
|
||||||
<SpinnerIcon
|
|
||||||
className={styles.actionMenuIcon}
|
|
||||||
name={icons.SEARCH}
|
|
||||||
isSpinning={isSearching}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Search
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
onPress={this.onOrganizePress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.actionMenuIcon}
|
|
||||||
name={icons.ORGANIZE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Preview Rename
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
onPress={this.onManageEpisodesPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.actionMenuIcon}
|
|
||||||
name={icons.EPISODE_FILE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
Manage Tracks
|
|
||||||
</MenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</Menu> :
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={styles.actionButton}
|
|
||||||
name={icons.SEARCH}
|
|
||||||
title="Search for album"
|
|
||||||
size={24}
|
|
||||||
isSpinning={isSearching}
|
|
||||||
onPress={onSearchPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
31
frontend/src/Artist/History/ArtistHistoryModal.js
Normal file
31
frontend/src/Artist/History/ArtistHistoryModal.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector';
|
||||||
|
|
||||||
|
function ArtistHistoryModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ArtistHistoryModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistHistoryModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistHistoryModal;
|
132
frontend/src/Artist/History/ArtistHistoryModalContent.js
Normal file
132
frontend/src/Artist/History/ArtistHistoryModalContent.js
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import ArtistHistoryRowConnector from './ArtistHistoryRowConnector';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'eventType',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'album',
|
||||||
|
label: 'Album',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceTitle',
|
||||||
|
label: 'Source Title',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: 'Quality',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: 'Date',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
label: 'Details',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: 'Actions',
|
||||||
|
isVisible: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
class ArtistHistoryModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
albumId,
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
onMarkAsFailedPress,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const fullArtist = albumId == null;
|
||||||
|
const hasItems = !!items.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
History
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to load history.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !hasItems && !error &&
|
||||||
|
<div>No history.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && hasItems && !error &&
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ArtistHistoryRowConnector
|
||||||
|
key={item.id}
|
||||||
|
fullArtist={fullArtist}
|
||||||
|
{...item}
|
||||||
|
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistHistoryModalContent.propTypes = {
|
||||||
|
albumId: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistHistoryModalContent;
|
|
@ -0,0 +1,81 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchArtistHistory, clearArtistHistory, artistHistoryMarkAsFailed } from 'Store/Actions/artistHistoryActions';
|
||||||
|
import ArtistHistoryModalContent from './ArtistHistoryModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.artistHistory,
|
||||||
|
(artistHistory) => {
|
||||||
|
return artistHistory;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchArtistHistory,
|
||||||
|
clearArtistHistory,
|
||||||
|
artistHistoryMarkAsFailed
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArtistHistoryModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
artistId,
|
||||||
|
albumId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.fetchArtistHistory({
|
||||||
|
artistId,
|
||||||
|
albumId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.clearArtistHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMarkAsFailedPress = (historyId) => {
|
||||||
|
const {
|
||||||
|
artistId,
|
||||||
|
albumId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.artistHistoryMarkAsFailed({
|
||||||
|
historyId,
|
||||||
|
artistId,
|
||||||
|
albumId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ArtistHistoryModalContent
|
||||||
|
{...this.props}
|
||||||
|
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistHistoryModalContentConnector.propTypes = {
|
||||||
|
artistId: PropTypes.number.isRequired,
|
||||||
|
albumId: PropTypes.number,
|
||||||
|
fetchArtistHistory: PropTypes.func.isRequired,
|
||||||
|
clearArtistHistory: PropTypes.func.isRequired,
|
||||||
|
artistHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryModalContentConnector);
|
6
frontend/src/Artist/History/ArtistHistoryRow.css
Normal file
6
frontend/src/Artist/History/ArtistHistoryRow.css
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.details,
|
||||||
|
.actions {
|
||||||
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 65px;
|
||||||
|
}
|
160
frontend/src/Artist/History/ArtistHistoryRow.js
Normal file
160
frontend/src/Artist/History/ArtistHistoryRow.js
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||||
|
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||||
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
|
import styles from './ArtistHistoryRow.css';
|
||||||
|
|
||||||
|
function getTitle(eventType) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'grabbed': return 'Grabbed';
|
||||||
|
case 'artistFolderImported': return 'Artist Folder Imported';
|
||||||
|
case 'downloadFolderImported': return 'Download Folder Imported';
|
||||||
|
case 'downloadFailed': return 'Download Failed';
|
||||||
|
case 'trackFileDeleted': return 'Track File Deleted';
|
||||||
|
case 'trackFileRenamed': return 'Track File Renamed';
|
||||||
|
default: return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistHistoryRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isMarkAsFailedModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMarkAsFailedPress = () => {
|
||||||
|
this.setState({ isMarkAsFailedModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmMarkAsFailed = () => {
|
||||||
|
this.props.onMarkAsFailedPress(this.props.id);
|
||||||
|
this.setState({ isMarkAsFailedModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMarkAsFailedModalClose = () => {
|
||||||
|
this.setState({ isMarkAsFailedModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
quality,
|
||||||
|
qualityCutoffNotMet,
|
||||||
|
date,
|
||||||
|
data,
|
||||||
|
fullArtist,
|
||||||
|
artist,
|
||||||
|
album
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isMarkAsFailedModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<HistoryEventTypeCell
|
||||||
|
eventType={eventType}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{album.title}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
{sourceTitle}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>
|
||||||
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
date={date}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.details}>
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={getTitle(eventType)}
|
||||||
|
body={
|
||||||
|
<HistoryDetailsConnector
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
{
|
||||||
|
eventType === 'grabbed' &&
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={this.onMarkAsFailedPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isMarkAsFailedModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Mark as Failed"
|
||||||
|
message={`Are you sure you want to mark '${sourceTitle}' as failed?`}
|
||||||
|
confirmLabel="Mark as Failed"
|
||||||
|
onConfirm={this.onConfirmMarkAsFailed}
|
||||||
|
onCancel={this.onMarkAsFailedModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistHistoryRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
eventType: PropTypes.string.isRequired,
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
quality: PropTypes.object.isRequired,
|
||||||
|
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
|
data: PropTypes.object.isRequired,
|
||||||
|
fullArtist: PropTypes.bool.isRequired,
|
||||||
|
artist: PropTypes.object.isRequired,
|
||||||
|
album: PropTypes.object.isRequired,
|
||||||
|
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistHistoryRow;
|
26
frontend/src/Artist/History/ArtistHistoryRowConnector.js
Normal file
26
frontend/src/Artist/History/ArtistHistoryRowConnector.js
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||||
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
|
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||||
|
import ArtistHistoryRow from './ArtistHistoryRow';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createArtistSelector(),
|
||||||
|
createEpisodeSelector(),
|
||||||
|
(artist, album) => {
|
||||||
|
return {
|
||||||
|
artist,
|
||||||
|
album
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchHistory,
|
||||||
|
markAsFailed
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryRow);
|
|
@ -39,6 +39,7 @@ export const FOLDER_OPEN = 'fa fa-folder-open';
|
||||||
export const GROUP = 'fa fa-object-group';
|
export const GROUP = 'fa fa-object-group';
|
||||||
export const HEALTH = 'fa fa-medkit';
|
export const HEALTH = 'fa fa-medkit';
|
||||||
export const HEART = 'fa fa-heart';
|
export const HEART = 'fa fa-heart';
|
||||||
|
export const HISTORY = 'fa fa-history';
|
||||||
export const HOUSEKEEPING = 'fa fa-home';
|
export const HOUSEKEEPING = 'fa fa-home';
|
||||||
export const INFO = 'fa fa-info-circle';
|
export const INFO = 'fa fa-info-circle';
|
||||||
export const INTERACTIVE = 'fa fa-user';
|
export const INTERACTIVE = 'fa fa-user';
|
||||||
|
|
104
frontend/src/Store/Actions/artistHistoryActions.js
Normal file
104
frontend/src/Store/Actions/artistHistoryActions.js
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import { set, update } from './baseActions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'artistHistory';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_ARTIST_HISTORY = 'artistHistory/fetchArtistHistory';
|
||||||
|
export const CLEAR_ARTIST_HISTORY = 'artistHistory/clearArtistHistory';
|
||||||
|
export const ARTIST_HISTORY_MARK_AS_FAILED = 'artistHistory/artistHistoryMarkAsFailed';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchArtistHistory = createThunk(FETCH_ARTIST_HISTORY);
|
||||||
|
export const clearArtistHistory = createAction(CLEAR_ARTIST_HISTORY);
|
||||||
|
export const artistHistoryMarkAsFailed = createThunk(ARTIST_HISTORY_MARK_AS_FAILED);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
[FETCH_ARTIST_HISTORY]: function(getState, payload, dispatch) {
|
||||||
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
|
const promise = $.ajax({
|
||||||
|
url: '/history/artist',
|
||||||
|
data: payload
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.done((data) => {
|
||||||
|
dispatch(batchActions([
|
||||||
|
update({ section, data }),
|
||||||
|
|
||||||
|
set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: true,
|
||||||
|
error: null
|
||||||
|
})
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.fail((xhr) => {
|
||||||
|
dispatch(set({
|
||||||
|
section,
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: xhr
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[ARTIST_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) {
|
||||||
|
const {
|
||||||
|
historyId,
|
||||||
|
artistId,
|
||||||
|
albumId
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const promise = $.ajax({
|
||||||
|
url: '/history/failed',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
id: historyId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.done(() => {
|
||||||
|
dispatch(fetchArtistHistory({ artistId, albumId }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[CLEAR_ARTIST_HISTORY]: (state) => {
|
||||||
|
return Object.assign({}, state, defaultState);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, defaultState, section);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import * as rootFolders from './rootFolderActions';
|
||||||
import * as albumStudio from './albumStudioActions';
|
import * as albumStudio from './albumStudioActions';
|
||||||
import * as artist from './artistActions';
|
import * as artist from './artistActions';
|
||||||
import * as artistEditor from './artistEditorActions';
|
import * as artistEditor from './artistEditorActions';
|
||||||
|
import * as artistHistory from './artistHistoryActions';
|
||||||
import * as artistIndex from './artistIndexActions';
|
import * as artistIndex from './artistIndexActions';
|
||||||
import * as settings from './settingsActions';
|
import * as settings from './settingsActions';
|
||||||
import * as system from './systemActions';
|
import * as system from './systemActions';
|
||||||
|
@ -48,6 +49,7 @@ export default [
|
||||||
albumStudio,
|
albumStudio,
|
||||||
artist,
|
artist,
|
||||||
artistEditor,
|
artistEditor,
|
||||||
|
artistHistory,
|
||||||
artistIndex,
|
artistIndex,
|
||||||
settings,
|
settings,
|
||||||
system,
|
system,
|
||||||
|
|
|
@ -31,6 +31,12 @@ export const defaultState = {
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
importMode: 'move',
|
importMode: 'move',
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
|
relativePath: function(item, direction) {
|
||||||
|
const relativePath = item.relativePath;
|
||||||
|
|
||||||
|
return relativePath.toLowerCase();
|
||||||
|
},
|
||||||
|
|
||||||
artist: function(item, direction) {
|
artist: function(item, direction) {
|
||||||
const artist = item.artist;
|
const artist = item.artist;
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ namespace Lidarr.Api.V1.History
|
||||||
GetResourcePaged = GetHistory;
|
GetResourcePaged = GetHistory;
|
||||||
|
|
||||||
Get["/since"] = x => GetHistorySince();
|
Get["/since"] = x => GetHistorySince();
|
||||||
|
Get["/artist"] = x => GetArtistHistory();
|
||||||
Post["/failed"] = x => MarkAsFailed();
|
Post["/failed"] = x => MarkAsFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +110,38 @@ namespace Lidarr.Api.V1.History
|
||||||
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<HistoryResource> GetArtistHistory()
|
||||||
|
{
|
||||||
|
var queryArtistId = Request.Query.ArtistId;
|
||||||
|
var queryAlbumId = Request.Query.AlbumId;
|
||||||
|
var queryEventType = Request.Query.EventType;
|
||||||
|
|
||||||
|
if (!queryArtistId.HasValue)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("artistId is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
int artistId = Convert.ToInt32(queryArtistId.Value);
|
||||||
|
HistoryEventType? eventType = null;
|
||||||
|
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
|
||||||
|
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
|
||||||
|
var includeTrack = Request.GetBooleanQueryParameter("includeTrack");
|
||||||
|
|
||||||
|
if (queryEventType.HasValue)
|
||||||
|
{
|
||||||
|
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queryAlbumId.HasValue)
|
||||||
|
{
|
||||||
|
int albumId = Convert.ToInt32(queryAlbumId.Value);
|
||||||
|
|
||||||
|
return _historyService.GetByAlbum(artistId, albumId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _historyService.GetByArtist(artistId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private Response MarkAsFailed()
|
private Response MarkAsFailed()
|
||||||
{
|
{
|
||||||
var id = (int)Request.Form.Id;
|
var id = (int)Request.Form.Id;
|
||||||
|
|
|
@ -14,6 +14,8 @@ namespace NzbDrone.Core.History
|
||||||
History MostRecentForAlbum(int albumId);
|
History MostRecentForAlbum(int albumId);
|
||||||
History MostRecentForDownloadId(string downloadId);
|
History MostRecentForDownloadId(string downloadId);
|
||||||
List<History> FindByDownloadId(string downloadId);
|
List<History> FindByDownloadId(string downloadId);
|
||||||
|
List<History> GetByArtist(int artistId, HistoryEventType? eventType);
|
||||||
|
List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType);
|
||||||
List<History> FindDownloadHistory(int idArtistId, QualityModel quality);
|
List<History> FindDownloadHistory(int idArtistId, QualityModel quality);
|
||||||
void DeleteForArtist(int artistId);
|
void DeleteForArtist(int artistId);
|
||||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||||
|
@ -48,6 +50,36 @@ namespace NzbDrone.Core.History
|
||||||
return Query.Where(h => h.DownloadId == downloadId);
|
return Query.Where(h => h.DownloadId == downloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<History> GetByArtist(int artistId, HistoryEventType? eventType)
|
||||||
|
{
|
||||||
|
var query = Query.Where(h => h.ArtistId == artistId);
|
||||||
|
|
||||||
|
if (eventType.HasValue)
|
||||||
|
{
|
||||||
|
query.AndWhere(h => h.EventType == eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
query.OrderByDescending(h => h.Date);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType)
|
||||||
|
{
|
||||||
|
var query = Query.Join<History, Album>(JoinType.Inner, h => h.Album, (h, e) => h.AlbumId == e.Id)
|
||||||
|
.Where(h => h.ArtistId == artistId)
|
||||||
|
.AndWhere(h => h.AlbumId == albumId);
|
||||||
|
|
||||||
|
if (eventType.HasValue)
|
||||||
|
{
|
||||||
|
query.AndWhere(h => h.EventType == eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
query.OrderByDescending(h => h.Date);
|
||||||
|
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
public List<History> FindDownloadHistory(int idArtistId, QualityModel quality)
|
public List<History> FindDownloadHistory(int idArtistId, QualityModel quality)
|
||||||
{
|
{
|
||||||
return Query.Where(h =>
|
return Query.Where(h =>
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Marr.Data.QGen;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
@ -13,7 +14,7 @@ using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.Profiles.Languages;
|
using NzbDrone.Core.Profiles.Languages;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Music;
|
||||||
using NzbDrone.Core.Music.Events;
|
using NzbDrone.Core.Music.Events;
|
||||||
|
|
||||||
namespace NzbDrone.Core.History
|
namespace NzbDrone.Core.History
|
||||||
|
@ -24,6 +25,8 @@ namespace NzbDrone.Core.History
|
||||||
History MostRecentForAlbum(int episodeId);
|
History MostRecentForAlbum(int episodeId);
|
||||||
History MostRecentForDownloadId(string downloadId);
|
History MostRecentForDownloadId(string downloadId);
|
||||||
History Get(int historyId);
|
History Get(int historyId);
|
||||||
|
List<History> GetByArtist(int artistId, HistoryEventType? eventType);
|
||||||
|
List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType);
|
||||||
List<History> Find(string downloadId, HistoryEventType eventType);
|
List<History> Find(string downloadId, HistoryEventType eventType);
|
||||||
List<History> FindByDownloadId(string downloadId);
|
List<History> FindByDownloadId(string downloadId);
|
||||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||||
|
@ -66,6 +69,16 @@ namespace NzbDrone.Core.History
|
||||||
return _historyRepository.Get(historyId);
|
return _historyRepository.Get(historyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<History> GetByArtist(int artistId, HistoryEventType? eventType)
|
||||||
|
{
|
||||||
|
return _historyRepository.GetByArtist(artistId, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType)
|
||||||
|
{
|
||||||
|
return _historyRepository.GetByAlbum(artistId, albumId, eventType);
|
||||||
|
}
|
||||||
|
|
||||||
public List<History> Find(string downloadId, HistoryEventType eventType)
|
public List<History> Find(string downloadId, HistoryEventType eventType)
|
||||||
{
|
{
|
||||||
return _historyRepository.FindByDownloadId(downloadId).Where(c => c.EventType == eventType).ToList();
|
return _historyRepository.FindByDownloadId(downloadId).Where(c => c.EventType == eventType).ToList();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue