Remove Album Studio

(cherry picked from commit 6fce6c2bbcb1395a48f89d41d209b0ebe96042ee)
This commit is contained in:
Mark McDowall 2023-01-30 19:15:40 -08:00 committed by Bogdan
parent 093d6c3c26
commit f2b513c081
22 changed files with 9 additions and 1480 deletions

View File

@ -1,36 +0,0 @@
.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;
}
}

View File

@ -1,10 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'contentBody': string;
'contentBodyContainer': string;
'pageContentBodyWrapper': string;
'tableInnerContentBody': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -1,440 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { CellMeasurer, CellMeasurerCache } from 'react-virtualized';
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 PageJumpBar from 'Components/Page/PageJumpBar';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import { align, sortDirections } from 'Helpers/Props';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
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 AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector';
import AlbumStudioFooter from './AlbumStudioFooter';
import AlbumStudioRowConnector from './AlbumStudioRowConnector';
import AlbumStudioTableHeader from './AlbumStudioTableHeader';
import styles from './AlbumStudio.css';
const columns = [
{
name: 'status',
isVisible: true
},
{
name: 'sortName',
label: () => translate('Name'),
isSortable: true,
isVisible: true
},
{
name: 'albumCount',
label: () => translate('Albums'),
isSortable: false,
isVisible: true
}
];
class AlbumStudio extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.scrollerRef = React.createRef();
this.state = {
estimatedRowSize: 100,
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) {
const {
isSaving,
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 && scrollIndex > 0) {
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
});
}
}
}
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
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);
});
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onUpdateSelectedPress = (changes) => {
this.props.onUpdateSelectedPress({
artistIds: this.getSelectedIds(),
...changes
});
};
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
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
isSaving,
saveError,
isSmallScreen,
onSortPress,
onFilterSelect
} = this.props;
const {
allSelected,
allUnselected,
estimatedRowSize,
jumpBarItems,
scrollIndex
} = this.state;
return (
<PageContent title={translate('AlbumStudio')}>
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={AlbumStudioFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<div className={styles.pageContentBodyWrapper}>
<PageContentBody
ref={this.scrollerRef}
className={styles.contentBody}
innerClassName={styles.innerContentBody}
>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>{getErrorMessage(error, 'Failed to load artist from API')}</div>
}
{
!error &&
isPopulated &&
!!items.length &&
this.scrollerRef.current ?
<div className={styles.contentBodyContainer}>
<VirtualTable
items={items}
scrollIndex={scrollIndex}
columns={columns}
scroller={this.scrollerRef.current}
isSmallScreen={isSmallScreen}
overscanRowCount={5}
rowRenderer={this.rowRenderer}
header={
<AlbumStudioTableHeader
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={this.onSelectAllChange}
/>
}
sortKey={sortKey}
sortDirection={sortDirection}
deferredMeasurementCache={this.cache}
rowHeight={this.cache.rowHeight}
estimatedRowSize={estimatedRowSize}
onRecompute={this.onGridRecompute}
/>
</div> :
null
}
{
!error && isPopulated && !items.length &&
<NoArtist totalItems={totalItems} />
}
</PageContentBody>
{
isPopulated && !!jumpBarItems.order.length &&
<PageJumpBar
items={jumpBarItems}
onItemPress={this.onJumpBarItemPress}
/>
}
</div>
<AlbumStudioFooter
selectedCount={this.getSelectedIds().length}
isSaving={isSaving}
saveError={saveError}
onUpdateSelectedPress={this.onUpdateSelectedPress}
/>
</PageContent>
);
}
}
AlbumStudio.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
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,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
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
};
export default AlbumStudio;

View File

@ -1,39 +0,0 @@
.album {
display: flex;
align-items: stretch;
overflow: hidden;
margin: 2px 4px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--albumBackgroundColor);
cursor: default;
}
.info {
padding: 0 4px;
}
.albumType {
padding: 0 4px;
border-width: 0 1px;
border-style: solid;
border-color: var(--borderColor);
background-color: var(--albumBackgroundColor);
color: var(--defaultColor);
}
.tracks {
padding: 0 4px;
background-color: var(--trackBackgroundColor);
color: var(--defaultColor);
}
.allTracks {
background-color: color(#27c24c saturation(-25%));
color: var(--white);
}
.missingWanted {
background-color: color(#f05050 saturation(-20%));
color: var(--white);
}

View File

@ -1,12 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'album': string;
'albumType': string;
'allTracks': string;
'info': string;
'missingWanted': string;
'tracks': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -1,102 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import translate from 'Utilities/String/translate';
import styles from './AlbumStudioAlbum.css';
class AlbumStudioAlbum extends Component {
//
// Listeners
onAlbumMonitoredPress = () => {
const {
id,
monitored
} = this.props;
this.props.onAlbumMonitoredPress(id, !monitored);
};
//
// Render
render() {
const {
title,
disambiguation,
albumType,
monitored,
statistics,
isSaving
} = this.props;
const {
trackFileCount,
totalTrackCount,
percentOfTracks
} = statistics;
return (
<div className={styles.album}>
<div className={styles.info}>
<MonitorToggleButton
monitored={monitored}
isSaving={isSaving}
onPress={this.onAlbumMonitoredPress}
/>
<span>
{
disambiguation ? `${title} (${disambiguation})` : `${title}`
}
</span>
</div>
<div className={styles.albumType}>
<span>
{
`${albumType}`
}
</span>
</div>
<div
className={classNames(
styles.tracks,
percentOfTracks < 100 && monitored && styles.missingWanted,
percentOfTracks === 100 && styles.allTracks
)}
title={translate('TrackFileCounttotalTrackCountTracksDownloadedInterp', [trackFileCount, totalTrackCount])}
>
{
totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}`
}
</div>
</div>
);
}
}
AlbumStudioAlbum.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string,
albumType: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
statistics: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
onAlbumMonitoredPress: PropTypes.func.isRequired
};
AlbumStudioAlbum.defaultProps = {
isSaving: false,
statistics: {
trackFileCount: 0,
totalTrackCount: 0,
percentOfTracks: 0
}
};
export default AlbumStudioAlbum;

View File

@ -1,116 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
import { saveAlbumStudio, setAlbumStudioFilter, setAlbumStudioSort } from 'Store/Actions/albumStudioActions';
import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
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(
createAlbumFetchStateSelector(),
createArtistClientSideCollectionItemsSelector('albumStudio'),
createDimensionsSelector(),
(albums, artist, dimensionsState) => {
const isPopulated = albums.isPopulated && artist.isPopulated;
const isFetching = artist.isFetching || albums.isFetching;
return {
...artist,
isPopulated,
isFetching,
albumCount: albums.albumCount,
isSmallScreen: dimensionsState.isSmallScreen
};
}
);
}
const mapDispatchToProps = {
fetchAlbums,
clearAlbums,
setAlbumStudioSort,
setAlbumStudioFilter,
saveAlbumStudio
};
class AlbumStudioConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.populate();
}
componentWillUnmount() {
this.unpopulate();
}
//
// Control
populate = () => {
this.props.fetchAlbums();
};
unpopulate = () => {
this.props.clearAlbums();
};
//
// Listeners
onSortPress = (sortKey) => {
this.props.setAlbumStudioSort({ sortKey });
};
onFilterSelect = (selectedFilterKey) => {
this.props.setAlbumStudioFilter({ selectedFilterKey });
};
onUpdateSelectedPress = (payload) => {
this.props.saveAlbumStudio(payload);
};
//
// Render
render() {
return (
<AlbumStudio
{...this.props}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onUpdateSelectedPress={this.onUpdateSelectedPress}
/>
);
}
}
AlbumStudioConnector.propTypes = {
setAlbumStudioSort: PropTypes.func.isRequired,
setAlbumStudioFilter: PropTypes.func.isRequired,
fetchAlbums: PropTypes.func.isRequired,
clearAlbums: PropTypes.func.isRequired,
saveAlbumStudio: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioConnector);

View File

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

View File

@ -1,14 +0,0 @@
.inputContainer {
margin-right: 20px;
}
.label {
margin-bottom: 3px;
font-weight: bold;
}
.updateSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
height: 35px;
}

View File

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

View File

@ -1,174 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup';
import MonitorAlbumsSelectInput from 'Components/Form/MonitorAlbumsSelectInput';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
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 styles from './AlbumStudioFooter.css';
const NO_CHANGE = 'noChange';
class AlbumStudioFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
monitor: NO_CHANGE,
monitorNewItems: NO_CHANGE
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
monitor: NO_CHANGE,
monitorNewItems: NO_CHANGE
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
};
onUpdateSelectedPress = () => {
const {
monitor,
monitored,
monitorNewItems
} = this.state;
const changes = {};
if (monitored !== NO_CHANGE) {
changes.monitored = monitored === 'monitored';
}
if (monitor !== NO_CHANGE) {
changes.monitor = monitor;
}
if (monitorNewItems !== NO_CHANGE) {
changes.monitorNewItems = monitorNewItems;
}
this.props.onUpdateSelectedPress(changes);
};
//
// Render
render() {
const {
selectedCount,
isSaving
} = this.props;
const {
monitored,
monitor,
monitorNewItems
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];
const noChanges = monitored === NO_CHANGE &&
monitor === NO_CHANGE &&
monitorNewItems === NO_CHANGE;
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('MonitorArtist')}
</div>
<FormInputGroup
type={inputTypes.SELECT}
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('MonitorExistingAlbums')}
</div>
<MonitorAlbumsSelectInput
name="monitor"
value={monitor}
includeNoChange={true}
includeNoChangeDisabled={false}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('MonitorNewAlbums')}
</div>
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
includeNoChangeDisabled={false}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div>
<div className={styles.label}>
{translate('CountArtistsSelected', { count: selectedCount })}
</div>
<SpinnerButton
className={styles.updateSelectedButton}
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!selectedCount || noChanges}
onPress={this.onUpdateSelectedPress}
>
{translate('UpdateSelected')}
</SpinnerButton>
</div>
</PageContentFooter>
);
}
}
AlbumStudioFooter.propTypes = {
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
onUpdateSelectedPress: PropTypes.func.isRequired
};
export default AlbumStudioFooter;

View File

@ -1,41 +0,0 @@
.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 {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
display: flex;
align-items: center;
padding: 0;
min-width: 60px;
}
.title {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
display: flex;
align-items: center;
flex-shrink: 0;
min-width: 110px;
}
.albums {
composes: cell;
display: flex;
flex-grow: 4;
flex-wrap: wrap;
min-width: 400px;
}

View File

@ -1,11 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'albums': string;
'cell': string;
'selectCell': string;
'status': string;
'title': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -1,95 +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 VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import AlbumStudioAlbum from './AlbumStudioAlbum';
import styles from './AlbumStudioRow.css';
class AlbumStudioRow extends Component {
//
// Render
render() {
const {
artistId,
status,
foreignArtistId,
artistName,
artistType,
monitored,
albums,
isSaving,
isSelected,
onSelectedChange,
onArtistMonitoredPress,
onAlbumMonitoredPress
} = this.props;
return (
<>
<VirtualTableSelectCell
className={styles.selectCell}
id={artistId}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
isDisabled={false}
/>
<ArtistStatusCell
className={styles.status}
artistType={artistType}
monitored={monitored}
status={status}
isSaving={isSaving}
onMonitoredPress={onArtistMonitoredPress}
component={VirtualTableRowCell}
/>
<VirtualTableRowCell className={styles.title}>
<ArtistNameLink
foreignArtistId={foreignArtistId}
artistName={artistName}
/>
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.albums}>
{
albums.map((album) => {
return (
<AlbumStudioAlbum
key={album.id}
{...album}
onAlbumMonitoredPress={onAlbumMonitoredPress}
/>
);
})
}
</VirtualTableRowCell>
</>
);
}
}
AlbumStudioRow.propTypes = {
artistId: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
artistType: PropTypes.string,
monitored: PropTypes.bool.isRequired,
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onArtistMonitoredPress: PropTypes.func.isRequired,
onAlbumMonitoredPress: PropTypes.func.isRequired
};
AlbumStudioRow.defaultProps = {
isSaving: false
};
export default AlbumStudioRow;

View File

@ -1,94 +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 { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
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(
createArtistSelector(),
getAlbumMap,
(artist, albumMap) => {
const albumsInArtist = albumMap.hasOwnProperty(artist.id) ? albumMap[artist.id] : [];
const sortedAlbums = _.orderBy(albumsInArtist, 'releaseDate', 'desc');
return {
...artist,
artistId: artist.id,
artistName: artist.artistName,
monitored: artist.monitored,
status: artist.status,
isSaving: artist.isSaving,
albums: sortedAlbums
};
}
);
}
const mapDispatchToProps = {
toggleArtistMonitored,
toggleAlbumsMonitored
};
class AlbumStudioRowConnector extends Component {
//
// Listeners
onArtistMonitoredPress = () => {
const {
artistId,
monitored
} = this.props;
this.props.toggleArtistMonitored({
artistId,
monitored: !monitored
});
};
onAlbumMonitoredPress = (albumId, monitored) => {
const albumIds = [albumId];
this.props.toggleAlbumsMonitored({
albumIds,
monitored
});
};
//
// Render
render() {
return (
<AlbumStudioRow
{...this.props}
onArtistMonitoredPress={this.onArtistMonitoredPress}
onAlbumMonitoredPress={this.onAlbumMonitoredPress}
/>
);
}
}
AlbumStudioRowConnector.propTypes = {
artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleArtistMonitored: PropTypes.func.isRequired,
toggleAlbumsMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);

View File

@ -1,18 +0,0 @@
.status {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 60px;
padding: 0;
}
.sortName {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 110px;
}
.albumCount {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
padding: 12px;
}

View File

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

View File

@ -1,61 +0,0 @@
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}
>
{typeof label === 'function' ? label() : 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;

View File

@ -5,7 +5,6 @@ import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
import ArtistIndex from 'Artist/Index/ArtistIndex';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
@ -90,7 +89,15 @@ function AppRoutes(props) {
<Route
path="/albumstudio"
component={AlbumStudioConnector}
exact={true}
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/')}
component={app}
/>
);
}}
/>
<Route

View File

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

View File

@ -1,167 +0,0 @@
import { createAction } from 'redux-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 { fetchAlbums } from './albumActions';
import { filterPredicates, filters } from './artistActions';
import { set } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
//
// Variables
export const section = 'albumStudio';
//
// State
export const defaultState = {
isSaving: false,
saveError: null,
sortKey: 'sortName',
sortDirection: sortDirections.ASCENDING,
secondarySortKey: 'sortName',
secondarySortDirection: sortDirections.ASCENDING,
selectedFilterKey: 'all',
filters,
filterPredicates,
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: 'artistType',
label: () => translate('ArtistType'),
type: filterBuilderTypes.EXACT
},
{
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: 'rootFolderPath',
label: () => translate('RootFolderPath'),
type: filterBuilderTypes.EXACT
},
{
name: 'tags',
label: () => translate('Tags'),
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG
}
]
};
export const persistState = [
'albumStudio.sortKey',
'albumStudio.sortDirection',
'albumStudio.selectedFilterKey',
'albumStudio.customFilters'
];
//
// Actions Types
export const SET_ALBUM_STUDIO_SORT = 'albumStudio/setAlbumStudioSort';
export const SET_ALBUM_STUDIO_FILTER = 'albumStudio/setAlbumStudioFilter';
export const SAVE_ALBUM_STUDIO = 'albumStudio/saveAlbumStudio';
//
// Action Creators
export const setAlbumStudioSort = createAction(SET_ALBUM_STUDIO_SORT);
export const setAlbumStudioFilter = createAction(SET_ALBUM_STUDIO_FILTER);
export const saveAlbumStudio = createThunk(SAVE_ALBUM_STUDIO);
//
// Action Handlers
export const actionHandlers = handleThunks({
[SAVE_ALBUM_STUDIO]: function(getState, payload, dispatch) {
const {
artistIds,
monitor,
monitored,
monitorNewItems
} = payload;
const artists = [];
artistIds.forEach((id) => {
const artistsToUpdate = { id };
if (payload.hasOwnProperty('monitored')) {
artistsToUpdate.monitored = monitored;
}
artists.push(artistsToUpdate);
});
dispatch(set({
section,
isSaving: true
}));
const promise = createAjaxRequest({
url: '/albumStudio',
method: 'POST',
data: JSON.stringify({
artist: artists,
monitoringOptions: { monitor },
monitorNewItems
}),
dataType: 'json'
}).request;
promise.done((data) => {
dispatch(fetchAlbums());
dispatch(set({
section,
isSaving: false,
saveError: null
}));
});
promise.fail((xhr) => {
dispatch(set({
section,
isSaving: false,
saveError: xhr
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[SET_ALBUM_STUDIO_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_ALBUM_STUDIO_FILTER]: createSetClientSideCollectionFilterReducer(section)
}, defaultState, section);

View File

@ -1,6 +1,5 @@
import * as albums from './albumActions';
import * as albumHistory from './albumHistoryActions';
import * as albumStudio from './albumStudioActions';
import * as app from './appActions';
import * as artist from './artistActions';
import * as artistHistory from './artistHistoryActions';
@ -46,7 +45,6 @@ export default [
providerOptions,
queue,
releases,
albumStudio,
artist,
artistHistory,
artistIndex,