New: Release Profiles, Frontend updates (#580)

* New: Release Profiles - UI Updates

* New: Release Profiles - API Changes

* New: Release Profiles - Test Updates

* New: Release Profiles - Backend Updates

* New: Interactive Artist Search

* New: Change Montiored on Album Details Page

* New: Show Duration on Album Details Page

* Fixed: Manual Import not working if no albums are Missing

* Fixed: Sort search input by sortTitle

* Fixed: Queue columnLabel throwing JS error
This commit is contained in:
Qstick
2019-02-23 17:39:11 -05:00
committed by GitHub
parent f126eafd26
commit 3f064c94b9
409 changed files with 6882 additions and 3176 deletions
+15 -11
View File
@@ -3,15 +3,18 @@
overflow-x: hidden;
padding: 5px;
border-bottom: 1px solid $borderColor;
font-size: 14px;
font-size: $defaultFontSize;
&:hover {
background-color: $tableRowHoverBackgroundColor;
}
}
.status {
width: 10px;
.eventWrapper {
display: flex;
flex: 1 0 1px;
overflow-x: hidden;
padding-left: 6px;
border-left-width: 4px;
border-left-style: solid;
}
@@ -24,6 +27,7 @@
.time {
flex: 0 0 120px;
margin-right: 10px;
border: none !important;
}
.artistName,
@@ -80,16 +84,16 @@
@media only screen and (max-width: $breakpointSmall) {
.event {
position: relative;
flex-wrap: wrap;
padding-left: 10px;
flex-direction: column;
}
.status {
position: absolute;
top: 7%;
left: 0;
height: 86%;
.eventWrapper {
display: block;
flex: 0 0 auto;
}
.date {
margin-left: 10px;
}
.date,
+5 -3
View File
@@ -49,7 +49,8 @@ class AgendaEvent extends Component {
queueItem,
showDate,
timeFormat,
longDateFormat
longDateFormat,
colorImpairedMode
} = this.props;
const startTime = moment(releaseDate);
@@ -74,8 +75,9 @@ class AgendaEvent extends Component {
<div
className={classNames(
styles.status,
styles[statusStyle]
styles.eventWrapper,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
)}
/>
@@ -15,7 +15,8 @@ function createMapStateToProps() {
artist,
queueItem,
timeFormat: uiSettings.timeFormat,
longDateFormat: uiSettings.longDateFormat
longDateFormat: uiSettings.longDateFormat,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
+13 -1
View File
@@ -41,8 +41,20 @@ class CalendarConnector extends Component {
}
componentDidMount() {
const {
useCurrentPage,
fetchCalendar,
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate);
this.props.gotoCalendarToday();
if (useCurrentPage) {
fetchCalendar();
} else {
gotoCalendarToday();
}
this.scheduleUpdate();
}
+62 -11
View File
@@ -10,7 +10,8 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import NoArtist from 'Artist/NoArtist';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import Legend from './Legend/Legend';
import CalendarOptionsModal from './Options/CalendarOptionsModal';
import LegendConnector from './Legend/LegendConnector';
import CalendarConnector from './CalendarConnector';
import styles from './CalendarPage.css';
@@ -26,6 +27,7 @@ class CalendarPage extends Component {
this.state = {
isCalendarLinkModalOpen: false,
isOptionsModalOpen: false,
width: 0
};
}
@@ -48,6 +50,23 @@ class CalendarPage extends Component {
this.setState({ isCalendarLinkModalOpen: false });
}
onOptionsPress = () => {
this.setState({ isOptionsModalOpen: true });
}
onOptionsModalClose = () => {
this.setState({ isOptionsModalOpen: false });
}
onSearchMissingPress = () => {
const {
missingAlbumIds,
onSearchMissingPress
} = this.props;
onSearchMissingPress(missingAlbumIds);
}
//
// Render
@@ -56,17 +75,20 @@ class CalendarPage extends Component {
selectedFilterKey,
filters,
hasArtist,
colorImpairedMode,
missingAlbumIds,
isSearchingForMissing,
useCurrentPage,
onFilterSelect
} = this.props;
const {
isCalendarLinkModalOpen,
isOptionsModalOpen
} = this.state;
const isMeasured = this.state.width > 0;
let PageComponent = 'div';
if (isMeasured) {
PageComponent = hasArtist ? CalendarConnector : NoArtist;
}
const PageComponent = hasArtist ? CalendarConnector : NoArtist;
return (
<PageContent title="Calendar">
@@ -77,9 +99,23 @@ class CalendarPage extends Component {
iconName={icons.CALENDAR}
onPress={this.onGetCalendarLinkPress}
/>
<PageToolbarButton
label="Search for Missing"
iconName={icons.SEARCH}
isDisabled={!missingAlbumIds.length}
isSpinning={isSearchingForMissing}
onPress={this.onSearchMissingPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<PageToolbarButton
label="Options"
iconName={icons.POSTER}
onPress={this.onOptionsPress}
/>
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={!hasArtist}
@@ -99,19 +135,31 @@ class CalendarPage extends Component {
whitelist={['width']}
onMeasure={this.onMeasure}
>
<PageComponent />
{
isMeasured ?
<PageComponent
useCurrentPage={useCurrentPage}
/> :
<div />
}
</Measure>
{
hasArtist &&
<Legend colorImpairedMode={colorImpairedMode} />
<LegendConnector />
}
</PageContentBodyConnector>
<CalendarLinkModal
isOpen={this.state.isCalendarLinkModalOpen}
isOpen={isCalendarLinkModalOpen}
onModalClose={this.onGetCalendarLinkModalClose}
/>
<CalendarOptionsModal
isOpen={isOptionsModalOpen}
onModalClose={this.onOptionsModalClose}
/>
</PageContent>
);
}
@@ -121,7 +169,10 @@ CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
missingAlbumIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
+71 -8
View File
@@ -1,22 +1,80 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import moment from 'moment';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import withCurrentPage from 'Components/withCurrentPage';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import CalendarPage from './CalendarPage';
function createMissingAlbumIdsSelector() {
return createSelector(
(state) => state.calendar.start,
(state) => state.calendar.end,
(state) => state.calendar.items,
(state) => state.queue.details.items,
(start, end, albums, queueDetails) => {
return albums.reduce((acc, album) => {
const releaseDate = album.releaseDate;
if (
album.percentOfTracks < 100 &&
moment(releaseDate).isAfter(start) &&
moment(releaseDate).isBefore(end) &&
isBefore(album.releaseDate) &&
!queueDetails.some((details) => !!details.album && details.album.id === album.id)
) {
acc.push(album.id);
}
return acc;
}, []);
}
);
}
function createIsSearchingSelector() {
return createSelector(
(state) => state.calendar.searchMissingCommandId,
createCommandsSelector(),
(searchMissingCommandId, commands) => {
if (searchMissingCommandId == null) {
return false;
}
return isCommandExecuting(commands.find((command) => {
return command.id === searchMissingCommandId;
}));
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
(state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createArtistCountSelector(),
createUISettingsSelector(),
(calendar, artistCount, uiSettings) => {
createMissingAlbumIdsSelector(),
createIsSearchingSelector(),
(
selectedFilterKey,
filters,
artistCount,
uiSettings,
missingAlbumIds,
isSearchingForMissing
) => {
return {
selectedFilterKey: calendar.selectedFilterKey,
filters: calendar.filters,
showUpcoming: calendar.showUpcoming,
selectedFilterKey,
filters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasArtist: !!artistCount
hasArtist: !!artistCount,
missingAlbumIds,
isSearchingForMissing
};
}
);
@@ -24,6 +82,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onSearchMissingPress(albumIds) {
dispatch(searchMissing({ albumIds }));
},
onDaysCountChange(dayCount) {
dispatch(setCalendarDaysCount({ dayCount }));
},
@@ -34,4 +95,6 @@ function createMapDispatchToProps(dispatch, props) {
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage);
export default withCurrentPage(
connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
);
@@ -22,7 +22,7 @@
.artistName {
color: #3a3f51;
font-size: 14px;
font-size: $defaultFontSize;
}
.absoluteEpisodeNumber {
@@ -53,7 +53,7 @@
border-left-color: $gray;
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, transparent, transparent 5px, #eee 5px, #eee 10px);
background: repeating-linear-gradient(45deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -61,7 +61,7 @@
border-left-color: $dangerColor;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px);
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -69,6 +69,6 @@
border-left-color: $blue;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px);
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import getStatusStyle from 'Calendar/getStatusStyle';
import albumEntities from 'Album/albumEntities';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
+34 -1
View File
@@ -1,9 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';
import { icons, kinds } from 'Helpers/Props';
import LegendItem from './LegendItem';
import LegendIconItem from './LegendIconItem';
import styles from './Legend.css';
function Legend({ colorImpairedMode }) {
function Legend(props) {
const {
showCutoffUnmetIcon,
colorImpairedMode
} = props;
const iconsToShow = [];
if (showCutoffUnmetIcon) {
iconsToShow.push(
<LegendIconItem
name="Cutoff Not Met"
icon={icons.TRACK_FILE}
kind={kinds.WARNING}
tooltip="Quality or language cutoff has not been met"
/>
);
}
return (
<div className={styles.legend}>
<div>
@@ -47,11 +67,24 @@ function Legend({ colorImpairedMode }) {
colorImpairedMode={colorImpairedMode}
/>
</div>
<div>
{iconsToShow[0]}
</div>
{
iconsToShow.length > 1 &&
<div>
{iconsToShow[1]}
{iconsToShow[2]}
</div>
}
</div>
);
}
Legend.propTypes = {
showCutoffUnmetIcon: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
createUISettingsSelector(),
(calendarOptions, uiSettings) => {
return {
...calendarOptions,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
export default connect(createMapStateToProps)(Legend);
@@ -0,0 +1,10 @@
.legendIconItem {
margin: 3px 0;
margin-right: 6px;
width: 150px;
cursor: default;
}
.icon {
margin-right: 5px;
}
@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './LegendIconItem.css';
function LegendIconItem(props) {
const {
name,
icon,
kind,
tooltip
} = props;
return (
<div
className={styles.legendIconItem}
title={tooltip}
>
<Icon
className={styles.icon}
name={icon}
kind={kind}
/>
{name}
</div>
);
}
LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired
};
export default LegendIconItem;
@@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector';
function CalendarOptionsModal(props) {
const {
isOpen,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<CalendarOptionsModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
CalendarOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarOptionsModal;
@@ -0,0 +1,216 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
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 { firstDayOfWeekOptions, weekColumnOptions, timeFormatOptions } from 'Settings/UI/UISettings';
class CalendarOptionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
};
}
componentDidUpdate(prevProps) {
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = this.props;
if (
prevProps.firstDayOfWeek !== firstDayOfWeek ||
prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader ||
prevProps.timeFormat !== timeFormat ||
prevProps.enableColorImpairedMode !== enableColorImpairedMode
) {
this.setState({
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
});
}
}
//
// Listeners
onOptionInputChange = ({ name, value }) => {
const {
dispatchSetCalendarOption
} = this.props;
dispatchSetCalendarOption({ [name]: value });
}
onGlobalInputChange = ({ name, value }) => {
const {
dispatchSaveUISettings
} = this.props;
const setting = { [name]: value };
this.setState(setting, () => {
dispatchSaveUISettings(setting);
});
}
onLinkFocus = (event) => {
event.target.select();
}
//
// Render
render() {
const {
collapseMultipleAlbums,
showCutoffUnmetIcon,
onModalClose
} = this.props;
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Calendar Options
</ModalHeader>
<ModalBody>
<FieldSet legend="Local">
<Form>
<FormGroup>
<FormLabel>Collapse Multiple Albums</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="collapseMultipleAlbums"
value={collapseMultipleAlbums}
helpText="Collapse multiple albums releasing on the same day"
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Icon for Cutoff Unmet</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showCutoffUnmetIcon"
value={showCutoffUnmetIcon}
helpText="Show icon for files when the cutoff hasn't been met"
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
<FieldSet legend="Global">
<Form>
<FormGroup>
<FormLabel>First Day of Week</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="firstDayOfWeek"
values={firstDayOfWeekOptions}
value={firstDayOfWeek}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Week Column Header</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="calendarWeekColumnHeader"
values={weekColumnOptions}
value={calendarWeekColumnHeader}
onChange={this.onGlobalInputChange}
helpText="Shown above each column when week is the active view"
/>
</FormGroup>
<FormGroup>
<FormLabel>Time Format</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="timeFormat"
values={timeFormatOptions}
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup><FormGroup>
<FormLabel>Enable Color-Impaired Mode</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableColorImpairedMode"
value={enableColorImpairedMode}
helpText="Altered style to allow color-impaired users to better distinguish color coded information"
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
}
CalendarOptionsModalContent.propTypes = {
collapseMultipleAlbums: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
firstDayOfWeek: PropTypes.number.isRequired,
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarOptionsModalContent;
@@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setCalendarOption } from 'Store/Actions/calendarActions';
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
import { saveUISettings } from 'Store/Actions/settingsActions';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.settings.ui.item,
(options, uiSettings) => {
return {
...options,
...uiSettings
};
}
);
}
const mapDispatchToProps = {
dispatchSetCalendarOption: setCalendarOption,
dispatchSaveUISettings: saveUISettings
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent);