Various UI Fixes and Updates

Closes #188
Closes #185
Closes #187
This commit is contained in:
Qstick 2018-01-25 22:01:53 -05:00
commit 54e9f88648
89 changed files with 2354 additions and 995 deletions

View file

@ -0,0 +1,31 @@
.button {
composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css';
position: relative;
}
.labelContainer {
composes: labelContainer from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.label {
composes: label from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.indicatorContainer {
position: absolute;
top: 10px;
right: 12px;
}
.indicatorBackground {
color: $themeDarkColor;
}
.enabled {
color: $successColor;
}
.disabled {
color: $dangerColor;
}

View file

@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import styles from './AdvancedSettingsButton.css';
function AdvancedSettingsButton(props) {
const {
advancedSettings,
onAdvancedSettingsPress
} = props;
return (
<Link
className={styles.button}
title={advancedSettings ? 'Shown, click to hide' : 'Hidden, click to show'}
onPress={onAdvancedSettingsPress}
>
<Icon
name={icons.ADVANCED_SETTINGS}
size={21}
/>
<span
className={classNames(
styles.indicatorContainer,
'fa-layers fa-fw'
)}
>
<Icon
className={styles.indicatorBackground}
name={icons.CIRCLE}
size={16}
/>
<Icon
className={advancedSettings ? styles.enabled : styles.disabled}
name={advancedSettings ? icons.CHECK : icons.CLOSE}
size={10}
/>
</span>
<div className={styles.labelContainer}>
<div className={styles.label}>
{advancedSettings ? 'Hide Advanced' : 'Show Advanced'}
</div>
</div>
</Link>
);
}
AdvancedSettingsButton.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
onAdvancedSettingsPress: PropTypes.func.isRequired
};
export default AdvancedSettingsButton;

View file

@ -14,7 +14,10 @@ class DownloadClientSettings extends Component {
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
@ -22,28 +25,34 @@ class DownloadClientSettings extends Component {
//
// Listeners
setDownloadClientOptionsRef = (ref) => {
this._downloadClientOptions = ref;
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onHasPendingChange = (hasPendingChanges) => {
this.setState({
hasPendingChanges
});
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
this._downloadClientOptions.getWrappedInstance().save();
if (this._saveCallback) {
this._saveCallback();
}
}
//
// Render
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Download Client Settings">
<SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges}
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
@ -51,8 +60,8 @@ class DownloadClientSettings extends Component {
<DownloadClientsConnector />
<DownloadClientOptionsConnector
ref={this.setDownloadClientOptionsRef}
onHasPendingChange={this.onHasPendingChange}
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<RemotePathMappingsConnector />

View file

@ -60,6 +60,7 @@ class DownloadClient extends Component {
return (
<Card
className={styles.downloadClient}
overlayContent={true}
onPress={this.onEditDownloadClientPress}
>
<div className={styles.name}>

View file

@ -21,10 +21,10 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchDownloadClientOptions,
setDownloadClientOptionsValue,
saveDownloadClientOptions,
clearPendingChanges
dispatchFetchDownloadClientOptions: fetchDownloadClientOptions,
dispatchSetDownloadClientOptionsValue: setDownloadClientOptionsValue,
dispatchSaveDownloadClientOptions: saveDownloadClientOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class DownloadClientOptionsConnector extends Component {
@ -33,31 +33,43 @@ class DownloadClientOptionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchDownloadClientOptions();
const {
dispatchFetchDownloadClientOptions,
dispatchSaveDownloadClientOptions,
onChildMounted
} = this.props;
dispatchFetchDownloadClientOptions();
onChildMounted(dispatchSaveDownloadClientOptions);
}
componentDidUpdate(prevProps) {
if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(this.props.hasPendingChanges);
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section });
}
//
// Control
save = () => {
this.props.saveDownloadClientOptions();
this.props.dispatchClearPendingChanges({ section: this.props.section });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setDownloadClientOptionsValue({ name, value });
this.props.dispatchSetDownloadClientOptionsValue({ name, value });
}
//
@ -75,18 +87,20 @@ class DownloadClientOptionsConnector extends Component {
DownloadClientOptionsConnector.propTypes = {
section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
fetchDownloadClientOptions: PropTypes.func.isRequired,
setDownloadClientOptionsValue: PropTypes.func.isRequired,
saveDownloadClientOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired
dispatchFetchDownloadClientOptions: PropTypes.func.isRequired,
dispatchSetDownloadClientOptionsValue: PropTypes.func.isRequired,
dispatchSaveDownloadClientOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
{ withRef: true },
undefined,
{ section: 'settings.downloadClientOptions' }
)(DownloadClientOptionsConnector);

View file

@ -49,6 +49,8 @@ function HostSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="port"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...port}
@ -95,6 +97,8 @@ function HostSettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="sslPort"
min={1}
max={65535}
helpTextWarning="Requires restart to take effect"
onChange={onInputChange}
{...sslPort}

View file

@ -74,6 +74,8 @@ function ProxySettings(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="proxyPort"
min={1}
max={65535}
onChange={onInputChange}
{...proxyPort}
/>

View file

@ -14,7 +14,10 @@ class IndexerSettings extends Component {
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
@ -22,28 +25,34 @@ class IndexerSettings extends Component {
//
// Listeners
setIndexerOptionsRef = (ref) => {
this._indexerOptions = ref;
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onHasPendingChange = (hasPendingChanges) => {
this.setState({
hasPendingChanges
});
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
this._indexerOptions.getWrappedInstance().save();
if (this._saveCallback) {
this._saveCallback();
}
}
//
// Render
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Indexer Settings">
<SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges}
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
@ -51,8 +60,8 @@ class IndexerSettings extends Component {
<IndexersConnector />
<IndexerOptionsConnector
ref={this.setIndexerOptionsRef}
onHasPendingChange={this.onHasPendingChange}
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<RestrictionsConnector />

View file

@ -76,6 +76,7 @@ class Indexer extends Component {
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.name}>

View file

@ -41,6 +41,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
helpText="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
onChange={onInputChange}
{...settings.minimumAge}
@ -53,6 +54,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited."
onChange={onInputChange}
{...settings.maximumSize}
@ -65,6 +67,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
helpText="Usenet only: Set to zero to set for unlimited retention"
onChange={onInputChange}
{...settings.retention}
@ -80,6 +83,7 @@ function IndexerOptions(props) {
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
helpTextWarning="This will apply to all indexers, please follow the rules set forth by them"
helpLink="https://github.com/Sonarr/Sonarr/wiki/RSS-Sync"

View file

@ -21,10 +21,10 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchIndexerOptions,
setIndexerOptionsValue,
saveIndexerOptions,
clearPendingChanges
dispatchFetchIndexerOptions: fetchIndexerOptions,
dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
dispatchSaveIndexerOptions: saveIndexerOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class IndexerOptionsConnector extends Component {
@ -33,31 +33,43 @@ class IndexerOptionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchIndexerOptions();
const {
dispatchFetchIndexerOptions,
dispatchSaveIndexerOptions,
onChildMounted
} = this.props;
dispatchFetchIndexerOptions();
onChildMounted(dispatchSaveIndexerOptions);
}
componentDidUpdate(prevProps) {
if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(this.props.hasPendingChanges);
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: this.props.section });
}
//
// Control
save = () => {
this.props.saveIndexerOptions();
this.props.dispatchClearPendingChanges({ section: this.props.section });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerOptionsValue({ name, value });
this.props.dispatchSetIndexerOptionsValue({ name, value });
}
//
@ -75,18 +87,20 @@ class IndexerOptionsConnector extends Component {
IndexerOptionsConnector.propTypes = {
section: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
fetchIndexerOptions: PropTypes.func.isRequired,
setIndexerOptionsValue: PropTypes.func.isRequired,
saveIndexerOptions: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connectSection(
createMapStateToProps,
mapDispatchToProps,
undefined,
{ withRef: true },
undefined,
{ section: 'settings.indexerOptions' }
)(IndexerOptionsConnector);

View file

@ -64,6 +64,7 @@ class Restriction extends Component {
return (
<Card
className={styles.restriction}
overlayContent={true}
onPress={this.onEditRestrictionPress}
>
<div>

View file

@ -52,6 +52,7 @@ class Metadata extends Component {
return (
<Card
className={styles.metadata}
overlayContent={true}
onPress={this.onEditMetadataPress}
>
<div className={styles.name}>

View file

@ -79,6 +79,7 @@ class Notification extends Component {
return (
<Card
className={styles.notification}
overlayContent={true}
onPress={this.onEditNotificationPress}
>
<div className={styles.name}>

View file

@ -101,7 +101,7 @@ function EditLanguageProfileModalContent(props) {
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a language profile that is attached to a artist'}
title={isInUse ? 'Can\'t delete a language profile that is attached to a artist' : undefined}
>
<Button
kind={kinds.DANGER}

View file

@ -97,7 +97,7 @@ function EditMetadataProfileModalContent(props) {
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a metadata profile that is attached to a artist'}
title={isInUse ? 'Can\'t delete a metadata profile that is attached to a artist' : undefined}
>
<Button
kind={kinds.DANGER}

View file

@ -200,7 +200,7 @@ class EditQualityProfileModalContent extends Component {
id &&
<div
className={styles.deleteButtonContainer}
title={isInUse && 'Can\'t delete a quality profile that is attached to a artist'}
title={isInUse ? 'Can\'t delete a quality profile that is attached to a artist' : undefined}
>
<Button
kind={kinds.DANGER}

View file

@ -28,6 +28,29 @@ function getValue(value) {
class QualityDefinition extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._forceUpdateTimeout = null;
}
componentDidMount() {
// A hack to deal with a bug in the slider component until a fix for it
// lands and an updated version is available.
// See: https://github.com/mpowaga/react-slider/issues/115
this._forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 1);
}
componentWillUnmount() {
if (this._forceUpdateTimeout) {
clearTimeout(this._forceUpdateTimeout);
}
}
//
// Listeners
@ -131,6 +154,8 @@ class QualityDefinition extends Component {
<NumberInput
className={styles.sizeInput}
name={`${id}.min`}
min={slider.min}
max={maxSize ? maxSize - 10 : slider.max - 10}
value={minSize || slider.min}
isFloat={true}
onChange={this.onMinSizeChange}
@ -143,6 +168,7 @@ class QualityDefinition extends Component {
<NumberInput
className={styles.sizeInput}
name={`${id}.max`}
min={minSize + 10}
value={maxSize || slider.max}
isFloat={true}
onChange={this.onMaxSizeChange}

View file

@ -40,7 +40,7 @@ class QualityDefinitionConnector extends Component {
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
}
if (minSize !== currentMaxSize) {
if (maxSize !== currentMaxSize) {
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
}
}

View file

@ -26,8 +26,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchQualityDefinitions,
saveQualityDefinitions
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
dispatchSaveQualityDefinitions: saveQualityDefinitions
};
class QualityDefinitionsConnector extends Component {
@ -36,26 +36,36 @@ class QualityDefinitionsConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchQualityDefinitions();
this.props.dispatchFetchQualityDefinitions();
const {
dispatchFetchQualityDefinitions,
dispatchSaveQualityDefinitions,
onChildMounted
} = this.props;
dispatchFetchQualityDefinitions();
onChildMounted(dispatchSaveQualityDefinitions);
}
componentDidUpdate(prevProps) {
const {
hasPendingChanges
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (hasPendingChanges !== prevProps.hasPendingChanges) {
this.props.onHasPendingChange(hasPendingChanges);
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
//
// Control
save = () => {
this.props.saveQualityDefinitions();
}
//
// Render
@ -69,10 +79,12 @@ class QualityDefinitionsConnector extends Component {
}
QualityDefinitionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
fetchQualityDefinitions: PropTypes.func.isRequired,
saveQualityDefinitions: PropTypes.func.isRequired,
onHasPendingChange: PropTypes.func.isRequired
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
dispatchSaveQualityDefinitions: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector);

View file

@ -12,7 +12,10 @@ class Quality extends Component {
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
@ -20,35 +23,41 @@ class Quality extends Component {
//
// Listeners
setQualityDefinitionsRef = (ref) => {
this._qualityDefinitions = ref;
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onHasPendingChange = (hasPendingChanges) => {
this.setState({
hasPendingChanges
});
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
this._qualityDefinitions.getWrappedInstance().save();
if (this._saveCallback) {
this._saveCallback();
}
}
//
// Render
render() {
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Quality Settings">
<SettingsToolbarConnector
hasPendingChanges={this.state.hasPendingChanges}
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<QualityDefinitionsConnector
ref={this.setQualityDefinitionsRef}
onHasPendingChange={this.onHasPendingChange}
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
</PageContentBodyConnector>
</PageContent>

View file

@ -1,7 +0,0 @@
.advancedSettings {
composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css';
}
.advancedSettingsEnabled {
color: $toobarButtonHoverColor;
}

View file

@ -1,13 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PendingChangesModal from './PendingChangesModal';
import styles from './SettingsToolbar.css';
import AdvancedSettingsButton from './AdvancedSettingsButton';
class SettingsToolbar extends Component {
@ -53,14 +52,9 @@ class SettingsToolbar extends Component {
return (
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={advancedSettings ? 'Hide Advanced' : 'Show Advanced'}
className={classNames(
styles.advancedSettings,
advancedSettings && styles.advancedSettingsEnabled
)}
iconName={icons.ADVANCED_SETTINGS}
onPress={onAdvancedSettingsPress}
<AdvancedSettingsButton
advancedSettings={advancedSettings}
onAdvancedSettingsPress={onAdvancedSettingsPress}
/>
{