mirror of https://github.com/lidarr/Lidarr
Fixed: Speed up AlbumStudio
This commit is contained in:
parent
dce69212fc
commit
e681469857
|
@ -0,0 +1,36 @@
|
|||
.pageContentBodyWrapper {
|
||||
display: flex;
|
||||
flex: 1 0 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.contentBody {
|
||||
composes: contentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tableInnerContentBody {
|
||||
composes: innerContentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.contentBodyContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.pageContentBodyWrapper {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.contentBody {
|
||||
flex-basis: 1px;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
|
@ -8,17 +11,24 @@ import { align, sortDirections } from 'Helpers/Props';
|
|||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector';
|
||||
import AlbumStudioRowConnector from './AlbumStudioRowConnector';
|
||||
import AlbumStudioTableHeader from './AlbumStudioTableHeader';
|
||||
import AlbumStudioFooter from './AlbumStudioFooter';
|
||||
import styles from './AlbumStudio.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'monitored',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
isVisible: true
|
||||
|
@ -29,14 +39,10 @@ const columns = [
|
|||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'monitored',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'albumCount',
|
||||
label: 'Albums',
|
||||
isSortable: true,
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
@ -50,11 +56,25 @@ class AlbumStudio extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
estimatedRowSize: 100,
|
||||
scroller: null,
|
||||
jumpBarItems: { order: [] },
|
||||
scrollIndex: null,
|
||||
jumpCount: 0,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
|
||||
this.cache = new CellMeasurerCache({
|
||||
defaultHeight: 100,
|
||||
fixedWidth: true
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -63,18 +83,186 @@ class AlbumStudio extends Component {
|
|||
saveError
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
scrollIndex,
|
||||
jumpCount
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
|
||||
// nasty hack to fix react-virtualized jumping incorrectly
|
||||
// due to variable row heights
|
||||
if (scrollIndex != null) {
|
||||
if (jumpCount === 0) {
|
||||
this.setState({
|
||||
scrollIndex: scrollIndex + 1,
|
||||
jumpCount: 1
|
||||
});
|
||||
} else if (jumpCount === 1) {
|
||||
this.setState({
|
||||
scrollIndex: scrollIndex - 1,
|
||||
jumpCount: 2
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
scrollIndex: null,
|
||||
jumpCount: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
|
||||
setJumpBarItems() {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
// Reset if not sorting by sortName
|
||||
if (sortKey !== 'sortName') {
|
||||
this.setState({ jumpBarItems: { order: [] } });
|
||||
return;
|
||||
}
|
||||
|
||||
const characters = _.reduce(items, (acc, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
if (char in acc) {
|
||||
acc[char] = acc[char] + 1;
|
||||
} else {
|
||||
acc[char] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const order = Object.keys(characters).sort();
|
||||
|
||||
// Reverse if sorting descending
|
||||
if (sortDirection === sortDirections.DESCENDING) {
|
||||
order.reverse();
|
||||
}
|
||||
|
||||
const jumpBarItems = {
|
||||
characters,
|
||||
order
|
||||
};
|
||||
|
||||
this.setState({ jumpBarItems });
|
||||
}
|
||||
|
||||
getSelectedIds = () => {
|
||||
if (this.state.allUnselected) {
|
||||
return [];
|
||||
}
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
setSelectedState = () => {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((artist) => {
|
||||
const isItemSelected = selectedState[artist.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[artist.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[artist.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
estimateRowHeight = (width) => {
|
||||
const {
|
||||
albumCount,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (albumCount === undefined || albumCount === 0 || items.length === 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// guess 250px per album entry
|
||||
// available width is total width less 186px for select, status etc
|
||||
const cols = Math.max(Math.floor((width - 186) / 250), 1);
|
||||
const albumsPerArtist = albumCount / items.length;
|
||||
const albumRowsPerArtist = albumsPerArtist / cols;
|
||||
|
||||
// each row is 23px per album row plus 16px padding
|
||||
return albumRowsPerArtist * 23 + 16;
|
||||
}
|
||||
|
||||
rowRenderer = ({ key, rowIndex, parent, style }) => {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={this.cache}
|
||||
parent={parent}
|
||||
columnIndex={0}
|
||||
rowIndex={rowIndex}
|
||||
>
|
||||
{({ registerChild }) => (
|
||||
<VirtualTableRow
|
||||
ref={registerChild}
|
||||
style={style}
|
||||
>
|
||||
<AlbumStudioRowConnector
|
||||
key={item.id}
|
||||
artistId={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
)}
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
|
@ -88,6 +276,10 @@ class AlbumStudio extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
}
|
||||
|
||||
onUpdateSelectedPress = (changes) => {
|
||||
this.props.onUpdateSelectedPress({
|
||||
artistIds: this.getSelectedIds(),
|
||||
|
@ -95,6 +287,21 @@ class AlbumStudio extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
const scrollIndex = getIndexOfFirstCharacter(this.props.items, jumpToCharacter);
|
||||
|
||||
if (scrollIndex != null) {
|
||||
this.setState({ scrollIndex });
|
||||
}
|
||||
}
|
||||
|
||||
onGridRecompute = (width) => {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
this.setState({ estimatedRowSize: this.estimateRowHeight(width) });
|
||||
this.cache.clearAll();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -112,6 +319,7 @@ class AlbumStudio extends Component {
|
|||
sortDirection,
|
||||
isSaving,
|
||||
saveError,
|
||||
isSmallScreen,
|
||||
onSortPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
@ -119,7 +327,10 @@ class AlbumStudio extends Component {
|
|||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
estimatedRowSize,
|
||||
scroller,
|
||||
jumpBarItems,
|
||||
scrollIndex
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
|
@ -138,53 +349,68 @@ class AlbumStudio extends Component {
|
|||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles.innerContentBody}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>{getErrorMessage(error, 'Failed to load artist from API')}</div>
|
||||
}
|
||||
{
|
||||
!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 (
|
||||
<AlbumStudioRowConnector
|
||||
key={item.id}
|
||||
artistId={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
{
|
||||
!error && isPopulated && !!items.length &&
|
||||
<div className={styles.contentBodyContainer}>
|
||||
<VirtualTable
|
||||
items={items}
|
||||
scrollIndex={scrollIndex}
|
||||
columns={columns}
|
||||
scroller={scroller}
|
||||
isSmallScreen={isSmallScreen}
|
||||
overscanRowCount={5}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<AlbumStudioTableHeader
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
deferredMeasurementCache={this.cache}
|
||||
rowHeight={this.cache.rowHeight}
|
||||
estimatedRowSize={estimatedRowSize}
|
||||
onRecompute={this.onGridRecompute}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoArtist totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoArtist totalItems={totalItems} />
|
||||
isPopulated && !!jumpBarItems.order.length &&
|
||||
<PageJumpBar
|
||||
items={jumpBarItems}
|
||||
onItemPress={this.onJumpBarItemPress}
|
||||
/>
|
||||
}
|
||||
</PageContentBody>
|
||||
</div>
|
||||
|
||||
<AlbumStudioFooter
|
||||
selectedCount={this.getSelectedIds().length}
|
||||
|
@ -203,6 +429,7 @@ AlbumStudio.propTypes = {
|
|||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
albumCount: PropTypes.number.isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
|
@ -210,6 +437,7 @@ AlbumStudio.propTypes = {
|
|||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
||||
|
|
|
@ -2,17 +2,42 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { setAlbumStudioSort, setAlbumStudioFilter, saveAlbumStudio } from 'Store/Actions/albumStudioActions';
|
||||
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
|
||||
import AlbumStudio from './AlbumStudio';
|
||||
|
||||
function createAlbumFetchStateSelector() {
|
||||
return createSelector(
|
||||
(state) => state.albums.items.length,
|
||||
(state) => state.albums.isFetching,
|
||||
(state) => state.albums.isPopulated,
|
||||
(length, isFetching, isPopulated) => {
|
||||
const albumCount = (!isFetching && isPopulated) ? length : 0;
|
||||
return {
|
||||
albumCount,
|
||||
isFetching,
|
||||
isPopulated
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('artist', 'albumStudio'),
|
||||
(artist) => {
|
||||
createAlbumFetchStateSelector(),
|
||||
createArtistClientSideCollectionItemsSelector('albumStudio'),
|
||||
createDimensionsSelector(),
|
||||
(albums, artist, dimensionsState) => {
|
||||
const isPopulated = albums.isPopulated && artist.isPopulated;
|
||||
const isFetching = artist.isFetching || albums.isFetching;
|
||||
return {
|
||||
...artist
|
||||
...artist,
|
||||
isPopulated,
|
||||
isFetching,
|
||||
albumCount: albums.albumCount,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,20 +1,46 @@
|
|||
.cell {
|
||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.selectCell {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status,
|
||||
.monitored {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
width: 20px !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 1px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
flex-shrink: 0;
|
||||
min-width: 110px;
|
||||
}
|
||||
|
||||
.albums {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
composes: cell;
|
||||
|
||||
display: flex;
|
||||
flex-grow: 4;
|
||||
flex-wrap: wrap;
|
||||
min-width: 400px;
|
||||
}
|
||||
|
|
|
@ -3,9 +3,8 @@ import React, { Component } from 'react';
|
|||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import AlbumStudioAlbum from './AlbumStudioAlbum';
|
||||
import styles from './AlbumStudioRow.css';
|
||||
|
@ -31,37 +30,40 @@ class AlbumStudioRow extends Component {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
className={styles.selectCell}
|
||||
id={artistId}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isDisabled={false}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.status}>
|
||||
<VirtualTableRowCell className={styles.monitored}>
|
||||
<MonitorToggleButton
|
||||
monitored={monitored}
|
||||
size={14}
|
||||
isSaving={isSaving}
|
||||
onPress={onArtistMonitoredPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.status}>
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
||||
title={status === 'ended' ? 'Ended' : 'Continuing'}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<VirtualTableRowCell className={styles.title}>
|
||||
<ArtistNameLink
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<TableRowCell className={styles.monitored}>
|
||||
<MonitorToggleButton
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
onPress={onArtistMonitoredPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.albums}>
|
||||
<VirtualTableRowCell className={styles.albums}>
|
||||
{
|
||||
albums.map((album) => {
|
||||
return (
|
||||
|
@ -73,8 +75,8 @@ class AlbumStudioRow extends Component {
|
|||
);
|
||||
})
|
||||
}
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,23 @@ import { toggleArtistMonitored } from 'Store/Actions/artistActions';
|
|||
import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
|
||||
import AlbumStudioRow from './AlbumStudioRow';
|
||||
|
||||
// Use a const to share the reselect cache between instances
|
||||
const getAlbumMap = createSelector(
|
||||
(state) => state.albums.items,
|
||||
(albums) => {
|
||||
return albums.reduce((acc, curr) => {
|
||||
(acc[curr.artistId] = acc[curr.artistId] || []).push(curr);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
);
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.albums,
|
||||
createArtistSelector(),
|
||||
(albums, artist) => {
|
||||
const albumsInArtist = _.filter(albums.items, { artistId: artist.id });
|
||||
getAlbumMap,
|
||||
(artist, albumMap) => {
|
||||
const albumsInArtist = albumMap.hasOwnProperty(artist.id) ? albumMap[artist.id] : [];
|
||||
const sortedAlbums = _.orderBy(albumsInArtist, 'releaseDate', 'desc');
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
.monitored,
|
||||
.status {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
width: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sortName {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 110px;
|
||||
}
|
||||
|
||||
.albumCount {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
padding: 12px;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import styles from './AlbumStudioTableHeader.css';
|
||||
|
||||
function AlbumStudioTableHeader(props) {
|
||||
const {
|
||||
columns,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
isSortable,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={isSortable}
|
||||
{...otherProps}
|
||||
>
|
||||
{label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
AlbumStudioTableHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AlbumStudioTableHeader;
|
|
@ -42,6 +42,7 @@ class VirtualTableSelectCell extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
inputClassName,
|
||||
id,
|
||||
isSelected,
|
||||
|
@ -51,7 +52,7 @@ class VirtualTableSelectCell extends Component {
|
|||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
className={styles.cell}
|
||||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
<CheckInput
|
||||
|
@ -67,6 +68,7 @@ class VirtualTableSelectCell extends Component {
|
|||
}
|
||||
|
||||
VirtualTableSelectCell.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
inputClassName: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
|
@ -75,6 +77,7 @@ VirtualTableSelectCell.propTypes = {
|
|||
};
|
||||
|
||||
VirtualTableSelectCell.defaultProps = {
|
||||
className: styles.cell,
|
||||
inputClassName: styles.input,
|
||||
isSelected: false
|
||||
};
|
||||
|
|
|
@ -7,8 +7,6 @@ import { WindowScroller, Grid } from 'react-virtualized';
|
|||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import styles from './VirtualTable.css';
|
||||
|
||||
const ROW_HEIGHT = 38;
|
||||
|
||||
function overscanIndicesGetter(options) {
|
||||
const {
|
||||
cellCount,
|
||||
|
@ -48,7 +46,8 @@ class VirtualTable extends Component {
|
|||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
scrollIndex
|
||||
scrollIndex,
|
||||
onRecompute
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -58,6 +57,7 @@ class VirtualTable extends Component {
|
|||
if (this._grid &&
|
||||
(prevState.width !== width ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
onRecompute(width);
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ class VirtualTable extends Component {
|
|||
scroller,
|
||||
header,
|
||||
headerHeight,
|
||||
rowHeight,
|
||||
rowRenderer,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
@ -146,7 +147,7 @@ class VirtualTable extends Component {
|
|||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowHeight={rowHeight}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
|
@ -181,12 +182,16 @@ VirtualTable.propTypes = {
|
|||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired
|
||||
rowHeight: PropTypes.oneOfType([PropTypes.func, PropTypes.number]).isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired,
|
||||
onRecompute: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38
|
||||
headerHeight: 38,
|
||||
rowHeight: 38,
|
||||
onRecompute: () => {}
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
|
Loading…
Reference in New Issue