mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-25 22:36:59 -04:00
New: Add index views for all books
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class BookIndexActionsCell extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
isRefreshingAuthor,
|
||||
onRefreshAuthorPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isEditAuthorModalOpen,
|
||||
isDeleteAuthorModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
{...otherProps}
|
||||
>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshAuthor')}
|
||||
isSpinning={isRefreshingAuthor}
|
||||
onPress={onRefreshAuthorPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditAuthor')}
|
||||
onPress={this.onEditAuthorPress}
|
||||
/>
|
||||
|
||||
<EditAuthorModalConnector
|
||||
isOpen={isEditAuthorModalOpen}
|
||||
authorId={id}
|
||||
onModalClose={this.onEditAuthorModalClose}
|
||||
onDeleteAuthorPress={this.onDeleteAuthorPress}
|
||||
/>
|
||||
|
||||
<DeleteAuthorModal
|
||||
isOpen={isDeleteAuthorModalOpen}
|
||||
authorId={id}
|
||||
onModalClose={this.onDeleteAuthorModalClose}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexActionsCell.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexActionsCell;
|
||||
@@ -0,0 +1,63 @@
|
||||
.status {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 4 0 110px;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 110px;
|
||||
}
|
||||
|
||||
.bookFileCount,
|
||||
.qualityProfileId,
|
||||
.metadataProfileId {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.releaseDate,
|
||||
.added,
|
||||
.genres {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 180px;
|
||||
}
|
||||
|
||||
.path {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 150px;
|
||||
}
|
||||
|
||||
.sizeOnDisk {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 60px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 90px;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import BookIndexTableOptionsConnector from './BookIndexTableOptionsConnector';
|
||||
import styles from './BookIndexHeader.css';
|
||||
|
||||
function BookIndexHeader(props) {
|
||||
const {
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
isSortable,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={BookIndexTableOptionsConnector}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name]
|
||||
)}
|
||||
name={name}
|
||||
isSortable={isSortable}
|
||||
{...otherProps}
|
||||
>
|
||||
{label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
BookIndexHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexHeader;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { setBookTableOption } from 'Store/Actions/bookIndexActions';
|
||||
import BookIndexHeader from './BookIndexHeader';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onTableOptionChange(payload) {
|
||||
dispatch(setBookTableOption(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, createMapDispatchToProps)(BookIndexHeader);
|
||||
@@ -0,0 +1,115 @@
|
||||
.cell {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 30px;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: cell;
|
||||
|
||||
flex: 4 0 110px;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 110px;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 70px;
|
||||
background-color: $defaultColor;
|
||||
}
|
||||
|
||||
.bannerImage {
|
||||
width: 379px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.overlayTitle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $offWhite;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.bookFileCount,
|
||||
.qualityProfileId,
|
||||
.metadataProfileId {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.releaseDate,
|
||||
.added,
|
||||
.genres {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 180px;
|
||||
}
|
||||
|
||||
.bookProgress {
|
||||
composes: cell;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 150px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.path {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 150px;
|
||||
}
|
||||
|
||||
.sizeOnDisk {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.ratings {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
|
||||
.tags {
|
||||
composes: cell;
|
||||
|
||||
flex: 1 0 60px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 1 90px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.checkInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorNameLink from 'Author/AuthorNameLink';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import BookNameLink from 'Book/BookNameLink';
|
||||
import EditBookModalConnector from 'Book/Edit/EditBookModalConnector';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookStatusCell from './BookStatusCell';
|
||||
import styles from './BookIndexRow.css';
|
||||
|
||||
class BookIndexRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasBannerError: false,
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: false,
|
||||
isEditBookModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onEditBookPress = () => {
|
||||
this.setState({ isEditBookModalOpen: true });
|
||||
}
|
||||
|
||||
onEditBookModalClose = () => {
|
||||
this.setState({ isEditBookModalOpen: false });
|
||||
}
|
||||
|
||||
onUseSceneNumberingChange = () => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
//
|
||||
}
|
||||
|
||||
onBannerLoad = () => {
|
||||
if (this.state.hasBannerError) {
|
||||
this.setState({ hasBannerError: false });
|
||||
}
|
||||
}
|
||||
|
||||
onBannerLoadError = () => {
|
||||
if (!this.state.hasBannerError) {
|
||||
this.setState({ hasBannerError: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
authorId,
|
||||
monitored,
|
||||
title,
|
||||
author,
|
||||
titleSlug,
|
||||
qualityProfile,
|
||||
releaseDate,
|
||||
added,
|
||||
statistics,
|
||||
genres,
|
||||
ratings,
|
||||
tags,
|
||||
showSearchAction,
|
||||
columns,
|
||||
isRefreshingBook,
|
||||
isSearchingBook,
|
||||
onRefreshBookPress,
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isEditAuthorModalOpen,
|
||||
isDeleteAuthorModalOpen,
|
||||
isEditBookModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<BookStatusCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name]
|
||||
)}
|
||||
>
|
||||
<BookNameLink
|
||||
titleSlug={titleSlug}
|
||||
title={title}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'authorName') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name]
|
||||
)}
|
||||
>
|
||||
<AuthorNameLink
|
||||
titleSlug={author.titleSlug}
|
||||
authorName={author.authorName}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseDate') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={releaseDate}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={added}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'bookFileCount') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{bookFileCount}
|
||||
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{author.path}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'genres') {
|
||||
const joinedGenres = genres.join(', ');
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<span title={joinedGenres}>
|
||||
{joinedGenres}
|
||||
</span>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshBook')}
|
||||
isSpinning={isRefreshingBook}
|
||||
onPress={onRefreshBookPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredBooks')}
|
||||
isSpinning={isSearchingBook}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('EditAuthor')}
|
||||
onPress={this.onEditAuthorPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditBook')}
|
||||
onPress={this.onEditBookPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<EditAuthorModalConnector
|
||||
isOpen={isEditAuthorModalOpen}
|
||||
authorId={authorId}
|
||||
onModalClose={this.onEditAuthorModalClose}
|
||||
onDeleteAuthorPress={this.onDeleteAuthorPress}
|
||||
/>
|
||||
|
||||
<DeleteAuthorModal
|
||||
isOpen={isDeleteAuthorModalOpen}
|
||||
authorId={authorId}
|
||||
onModalClose={this.onDeleteAuthorModalClose}
|
||||
/>
|
||||
|
||||
<EditBookModalConnector
|
||||
isOpen={isEditBookModalOpen}
|
||||
authorId={authorId}
|
||||
bookId={id}
|
||||
onModalClose={this.onEditBookModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
authorId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
author: PropTypes.object.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isRefreshingBook: PropTypes.bool.isRequired,
|
||||
isSearchingBook: PropTypes.bool.isRequired,
|
||||
onRefreshBookPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
BookIndexRow.defaultProps = {
|
||||
statistics: {
|
||||
bookCount: 0,
|
||||
bookFileCount: 0,
|
||||
totalBookCount: 0
|
||||
},
|
||||
genres: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default BookIndexRow;
|
||||
@@ -0,0 +1,5 @@
|
||||
.tableContainer {
|
||||
composes: tableContainer from '~Components/Table/VirtualTable.css';
|
||||
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import BookIndexItemConnector from 'Book/Index/BookIndexItemConnector';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import BookIndexHeaderConnector from './BookIndexHeaderConnector';
|
||||
import BookIndexRow from './BookIndexRow';
|
||||
import styles from './BookIndexTable.css';
|
||||
|
||||
class BookIndexTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scrollIndex: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
jumpToCharacter
|
||||
} = this.props;
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
|
||||
const scrollIndex = getIndexOfFirstCharacter(items, sortKey, jumpToCharacter);
|
||||
|
||||
if (scrollIndex != null) {
|
||||
this.setState({ scrollIndex });
|
||||
}
|
||||
} else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
|
||||
this.setState({ scrollIndex: null });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
const book = items[rowIndex];
|
||||
|
||||
return (
|
||||
<VirtualTableRow
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<BookIndexItemConnector
|
||||
key={book.id}
|
||||
component={BookIndexRow}
|
||||
style={style}
|
||||
columns={columns}
|
||||
authorId={book.authorId}
|
||||
bookId={book.id}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
columns,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isSmallScreen,
|
||||
onSortPress,
|
||||
scroller,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<VirtualTable
|
||||
className={styles.tableContainer}
|
||||
items={items}
|
||||
scrollIndex={this.state.scrollIndex}
|
||||
scrollTop={scrollTop}
|
||||
isSmallScreen={isSmallScreen}
|
||||
scroller={scroller}
|
||||
rowHeight={38}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<BookIndexHeaderConnector
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
/>
|
||||
}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexTable.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexTable;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setBookSort } from 'Store/Actions/bookIndexActions';
|
||||
import BookIndexTable from './BookIndexTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app.dimensions,
|
||||
(state) => state.bookIndex.tableOptions,
|
||||
(state) => state.bookIndex.columns,
|
||||
(dimensions, tableOptions, columns) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
showBanners: tableOptions.showBanners,
|
||||
columns
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onSortPress(sortKey) {
|
||||
dispatch(setBookSort({ sortKey }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BookIndexTable);
|
||||
@@ -0,0 +1,86 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class BookIndexTableOptions extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
showSearchAction: props.showSearchAction
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
showSearchAction
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
showSearchAction !== prevProps.showSearchAction
|
||||
) {
|
||||
this.setState({
|
||||
showSearchAction
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTableOptionChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onTableOptionChange({
|
||||
tableOptions: {
|
||||
...this.state,
|
||||
[name]: value
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
showSearchAction
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={this.onTableOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexTableOptions.propTypes = {
|
||||
showBanners: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexTableOptions;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import BookIndexTableOptions from './BookIndexTableOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.authorIndex.tableOptions,
|
||||
(tableOptions) => {
|
||||
return tableOptions;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(BookIndexTableOptions);
|
||||
@@ -0,0 +1,9 @@
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
width: 20px !important;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './BookStatusCell.css';
|
||||
|
||||
function BookStatusCell(props) {
|
||||
const {
|
||||
className,
|
||||
monitored,
|
||||
component: Component,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
title={monitored ? translate('MonitoredAuthorIsMonitored') : translate('MonitoredAuthorIsUnmonitored')}
|
||||
/>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
BookStatusCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
component: PropTypes.elementType
|
||||
};
|
||||
|
||||
BookStatusCell.defaultProps = {
|
||||
className: styles.status,
|
||||
component: VirtualTableRowCell
|
||||
};
|
||||
|
||||
export default BookStatusCell;
|
||||
@@ -0,0 +1,16 @@
|
||||
const growableColumns = [
|
||||
'qualityProfileId',
|
||||
'path',
|
||||
'tags'
|
||||
];
|
||||
|
||||
export default function hasGrowableColumns(columns) {
|
||||
return columns.some((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
return growableColumns.includes(name) && isVisible;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user