New: Server Side UI Filtering, Error Boundaries (#501)

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
Qstick 2018-09-22 23:10:50 -04:00 committed by GitHub
parent a95191dc3b
commit 64a8d02f77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 1564 additions and 431 deletions

View file

@ -0,0 +1,62 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as sentry from '@sentry/browser';
class ErrorBoundary extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
error: null,
info: null
};
}
componentDidCatch(error, info) {
this.setState({
error,
info
});
sentry.captureException(error);
}
//
// Render
render() {
const {
children,
errorComponent: ErrorComponent,
...otherProps
} = this.props;
const {
error,
info
} = this.state;
if (error) {
return (
<ErrorComponent
error={error}
info={info}
{...otherProps}
/>
);
}
return children;
}
}
ErrorBoundary.propTypes = {
children: PropTypes.node.isRequired,
errorComponent: PropTypes.func.isRequired
};
export default ErrorBoundary;

View file

@ -0,0 +1,38 @@
.container {
text-align: center;
}
.message {
margin: 50px 0;
text-align: center;
font-weight: 300;
font-size: 36px;
}
.imageContainer {
display: flex;
justify-content: center;
flex: 0 0 auto;
}
.image {
height: 350px;
}
.details {
margin: 20px;
text-align: left;
white-space: pre-wrap;
}
@media only screen and (max-width: $breakpointMedium) {
.image {
height: 250px;
}
}
@media only screen and (max-width: $breakpointSmall) {
.image {
height: 150px;
}
}

View file

@ -0,0 +1,60 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './ErrorBoundaryError.css';
function ErrorBoundaryError(props) {
const {
className,
messageClassName,
detailsClassName,
message,
error,
info
} = props;
return (
<div className={className}>
<div className={messageClassName}>
{message}
</div>
<div className={styles.imageContainer}>
<img
className={styles.image}
src={`${window.Lidarr.urlBase}/Content/Images/error.png`}
/>
</div>
<details className={detailsClassName}>
{
error &&
<div>
{error.toString()}
</div>
}
<div className={styles.info}>
{info.componentStack}
</div>
</details>
</div>
);
}
ErrorBoundaryError.propTypes = {
className: PropTypes.string.isRequired,
messageClassName: PropTypes.string.isRequired,
detailsClassName: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
error: PropTypes.object.isRequired,
info: PropTypes.object.isRequired
};
ErrorBoundaryError.defaultProps = {
className: styles.container,
messageClassName: styles.message,
detailsClassName: styles.details,
message: 'There was an error loading this content'
};
export default ErrorBoundaryError;

View file

@ -35,7 +35,7 @@ function createMapStateToProps() {
directories,
files,
paths: filteredPaths,
isWindowsService: true || systemStatus.isWindows && systemStatus.mode === 'service'
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service'
};
}
);

View file

@ -3,6 +3,7 @@ import React, { Component } from 'react';
import { inputTypes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
@ -34,6 +35,28 @@ class FilterBuilderModalContent extends Component {
};
}
componentDidUpdate(prevProps) {
const {
id,
customFilters,
isSaving,
saveError,
dispatchSetFilter,
onModalClose
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
if (id) {
dispatchSetFilter({ selectedFilterKey: id });
} else {
const last = customFilters[customFilters.length -1];
dispatchSetFilter({ selectedFilterKey: last.id });
}
onModalClose();
}
}
//
// Listeners
@ -70,9 +93,9 @@ class FilterBuilderModalContent extends Component {
onSaveFilterPress = () => {
const {
customFilterKey: key,
onSaveCustomFilterPress,
onModalClose
id,
customFilterType,
onSaveCustomFilterPress
} = this.props;
const {
@ -92,8 +115,12 @@ class FilterBuilderModalContent extends Component {
return;
}
onSaveCustomFilterPress({ key, label, filters });
onModalClose();
onSaveCustomFilterPress({
id,
type: customFilterType,
label,
filters
});
}
//
@ -103,6 +130,8 @@ class FilterBuilderModalContent extends Component {
const {
sectionItems,
filterBuilderProps,
isSaving,
saveError,
onModalClose
} = this.props;
@ -161,17 +190,17 @@ class FilterBuilderModalContent extends Component {
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={this.onSaveFilterPress}
>
Apply
</Button>
Save
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
@ -179,13 +208,18 @@ class FilterBuilderModalContent extends Component {
}
FilterBuilderModalContent.propTypes = {
customFilterKey: PropTypes.string,
id: PropTypes.number,
label: PropTypes.string.isRequired,
customFilterType: PropTypes.string.isRequired,
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
onRemoveCustomFilterPress: PropTypes.func.isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
onSaveCustomFilterPress: PropTypes.func.isRequired,
dispatchSetFilter: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View file

@ -1,28 +1,42 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveCustomFilter, deleteCustomFilter } from 'Store/Actions/customFilterActions';
import FilterBuilderModalContent from './FilterBuilderModalContent';
function createMapStateToProps() {
return createSelector(
(state, { customFilters }) => customFilters,
(state, { customFilterKey }) => customFilterKey,
(customFilters, customFilterKey) => {
if (customFilterKey) {
const customFilter = customFilters.find((c) => c.key === customFilterKey);
(state, { id }) => id,
(state) => state.customFilters.isSaving,
(state) => state.customFilters.saveError,
(customFilters, id, isSaving, saveError) => {
if (id) {
const customFilter = customFilters.find((c) => c.id === id);
return {
customFilterKey: customFilter.key,
id: customFilter.id,
label: customFilter.label,
filters: customFilter.filters
filters: customFilter.filters,
customFilters,
isSaving,
saveError
};
}
return {
label: '',
filters: []
filters: [],
customFilters,
isSaving,
saveError
};
}
);
}
export default connect(createMapStateToProps)(FilterBuilderModalContent);
const mapDispatchToProps = {
onSaveCustomFilterPress: saveCustomFilter,
dispatchDeleteCustomFilter: deleteCustomFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);

View file

@ -1,11 +1,64 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds, filterBuilderTypes } from 'Helpers/Props';
import convertToBytes from 'Utilities/Number/convertToBytes';
import formatBytes from 'Utilities/Number/formatBytes';
import { kinds, filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
import TagInput, { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
export const NAME = 'value';
function getTagDisplayValue(value, selectedFilterBuilderProp) {
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
return formatBytes(value);
}
return value;
}
function getValue(input, selectedFilterBuilderProp) {
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
if (match && match.length > 1) {
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
switch (unit.toLowerCase()) {
case 'k':
return convertToBytes(value, 1, true);
case 'm':
return convertToBytes(value, 2, true);
case 'g':
return convertToBytes(value, 3, true);
case 't':
return convertToBytes(value, 4, true);
case 'kb':
return convertToBytes(value, 1, true);
case 'mb':
return convertToBytes(value, 2, true);
case 'gb':
return convertToBytes(value, 3, true);
case 'tb':
return convertToBytes(value, 4, true);
case 'kib':
return convertToBytes(value, 1, true);
case 'mib':
return convertToBytes(value, 2, true);
case 'gib':
return convertToBytes(value, 3, true);
case 'tib':
return convertToBytes(value, 4, true);
default:
return parseInt(value);
}
}
}
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
return parseInt(input);
}
return input;
}
class FilterBuilderRowValue extends Component {
//
@ -18,17 +71,15 @@ class FilterBuilderRowValue extends Component {
onChange
} = this.props;
let id = tag.id;
let value = tag.id;
if (id == null) {
id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ?
parseInt(tag.name) :
tag.name;
if (value == null) {
value = getValue(tag.name, selectedFilterBuilderProp);
}
onChange({
name: NAME,
value: [...filterValue, id]
value: [...filterValue, value]
});
}
@ -52,6 +103,7 @@ class FilterBuilderRowValue extends Component {
render() {
const {
filterValue,
selectedFilterBuilderProp,
tagList
} = this.props;
@ -68,7 +120,7 @@ class FilterBuilderRowValue extends Component {
}
return {
id,
name: id
name: getTagDisplayValue(id, selectedFilterBuilderProp)
};
});

View file

@ -2,29 +2,70 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import styles from './CustomFilter.css';
class CustomFilter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDeleting: false
};
}
componentDidUpdate(prevProps) {
const {
isDeleting,
deleteError
} = this.props;
if (prevProps.isDeleting && !isDeleting && this.state.isDeleting && deleteError) {
this.setState({ isDeleting: false });
}
}
componentWillUnmount() {
const {
id,
selectedFilterKey,
dispatchSetFilter
} = this.props;
// Assume that delete and then unmounting means the delete was successful.
// Moving this check to a ancestor would be more accurate, but would have
// more boilerplate.
if (this.state.isDeleting && id === selectedFilterKey) {
dispatchSetFilter({ selectedFilterKey: 'all' });
}
}
//
// Listeners
onEditPress = () => {
const {
customFilterKey,
id,
onEditPress
} = this.props;
onEditPress(customFilterKey);
onEditPress(id);
}
onRemovePress = () => {
const {
customFilterKey,
onRemovePress
id,
dispatchDeleteCustomFilter
} = this.props;
onRemovePress({ key: customFilterKey });
this.setState({ isDeleting: true }, () => {
dispatchDeleteCustomFilter({ id });
});
}
//
@ -47,8 +88,9 @@ class CustomFilter extends Component {
onPress={this.onEditPress}
/>
<IconButton
<SpinnerIconButton
name={icons.REMOVE}
isSpinning={this.state.isDeleting}
onPress={this.onRemovePress}
/>
</div>
@ -58,10 +100,14 @@ class CustomFilter extends Component {
}
CustomFilter.propTypes = {
customFilterKey: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
dispatchSetFilter: PropTypes.func.isRequired,
onEditPress: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
dispatchDeleteCustomFilter: PropTypes.func.isRequired
};
export default CustomFilter;

View file

@ -10,9 +10,13 @@ import styles from './CustomFiltersModalContent.css';
function CustomFiltersModalContent(props) {
const {
selectedFilterKey,
customFilters,
isDeleting,
deleteError,
dispatchDeleteCustomFilter,
dispatchSetFilter,
onAddCustomFilter,
onRemoveCustomFilterPress,
onEditCustomFilter,
onModalClose
} = props;
@ -29,10 +33,14 @@ function CustomFiltersModalContent(props) {
return (
<CustomFilter
key={index}
customFilterKey={customFilter.key}
id={customFilter.id}
label={customFilter.label}
filters={customFilter.filters}
onRemovePress={onRemoveCustomFilterPress}
selectedFilterKey={selectedFilterKey}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
onEditPress={onEditCustomFilter}
/>
);
@ -58,9 +66,13 @@ function CustomFiltersModalContent(props) {
}
CustomFiltersModalContent.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
dispatchSetFilter: PropTypes.func.isRequired,
onAddCustomFilter: PropTypes.func.isRequired,
onRemoveCustomFilterPress: PropTypes.func.isRequired,
onEditCustomFilter: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
import CustomFiltersModalContent from './CustomFiltersModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.customFilters.isDeleting,
(state) => state.customFilters.deleteError,
(isDeleting, deleteError) => {
return {
isDeleting,
deleteError
};
}
);
}
const mapDispatchToProps = {
dispatchDeleteCustomFilter: deleteCustomFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFiltersModalContent);

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
import CustomFiltersModalContentConnector from './CustomFilters/CustomFiltersModalContentConnector';
class FilterModal extends Component {
@ -14,7 +14,7 @@ class FilterModal extends Component {
this.state = {
filterBuilder: !props.customFilters.length,
customFilterKey: null
id: null
};
}
@ -27,17 +27,17 @@ class FilterModal extends Component {
});
}
onEditCustomFilter = (customFilterKey) => {
onEditCustomFilter = (id) => {
this.setState({
filterBuilder: true,
customFilterKey
id
});
}
onModalClose = () => {
this.setState({
filterBuilder: false,
customFilterKey: null
id: null
}, () => {
this.props.onModalClose();
});
@ -54,7 +54,7 @@ class FilterModal extends Component {
const {
filterBuilder,
customFilterKey
id
} = this.state;
return (
@ -66,10 +66,10 @@ class FilterModal extends Component {
filterBuilder ?
<FilterBuilderModalContentConnector
{...otherProps}
customFilterKey={customFilterKey}
id={id}
onModalClose={this.onModalClose}
/> :
<CustomFiltersModalContent
<CustomFiltersModalContentConnector
{...otherProps}
onAddCustomFilter={this.onAddCustomFilter}
onEditCustomFilter={this.onEditCustomFilter}

View file

@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import TetherComponent from 'react-tether';
import classNames from 'classnames';
import isMobileUtil from 'Utilities/isMobile';
@ -10,6 +9,7 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller';

View file

@ -62,13 +62,15 @@ class PathInput extends Component {
event.preventDefault();
const path = this.props.paths[0];
this.props.onChange({
name: this.props.name,
value: path.path
});
if (path) {
this.props.onChange({
name: this.props.name,
value: path.path
});
if (path.type !== 'file') {
this.props.onFetchPaths(path.path);
if (path.type !== 'file') {
this.props.onFetchPaths(path.path);
}
}
}
}

View file

@ -39,6 +39,10 @@ class TagInput extends Component {
this._autosuggestRef = null;
}
componentWillUnmount() {
this.addTag.cancel();
}
//
// Control

View file

@ -0,0 +1,38 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactMeasure from 'react-measure';
class Measure extends Component {
//
// Lifecycle
componentWillUnmount() {
this.onMeasure.cancel();
}
//
// Listeners
onMeasure = _.debounce((payload) => {
this.props.onMeasure(payload);
}, 250, { leading: true, trailing: false })
//
// Render
render() {
return (
<ReactMeasure
{...this.props}
/>
);
}
}
Measure.propTypes = {
onMeasure: PropTypes.func.isRequired
};
export default Measure;

View file

@ -42,6 +42,7 @@ class FilterMenu extends Component {
customFilters,
buttonComponent: ButtonComponent,
filterModalConnectorComponent: FilterModalConnectorComponent,
filterModalConnectorComponentProps,
onFilterSelect,
...otherProps
} = this.props;
@ -74,6 +75,7 @@ class FilterMenu extends Component {
{
showCustomFilters &&
<FilterModalConnectorComponent
{...filterModalConnectorComponentProps}
isOpen={this.state.isFilterModalOpen}
selectedFilterKey={selectedFilterKey}
filters={filters}
@ -90,11 +92,12 @@ class FilterMenu extends Component {
FilterMenu.propTypes = {
className: PropTypes.string,
isDisabled: PropTypes.bool.isRequired,
selectedFilterKey: PropTypes.string.isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
buttonComponent: PropTypes.func.isRequired,
filterModalConnectorComponent: PropTypes.func,
filterModalConnectorComponentProps: PropTypes.object,
onFilterSelect: PropTypes.func.isRequired
};

View file

@ -42,8 +42,8 @@ class FilterMenuContent extends Component {
customFilters.map((filter) => {
return (
<FilterMenuItem
key={filter.key}
filterKey={filter.key}
key={filter.id}
filterKey={filter.id}
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
@ -70,7 +70,7 @@ class FilterMenuContent extends Component {
}
FilterMenuContent.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
showCustomFilters: PropTypes.bool.isRequired,

View file

@ -37,8 +37,8 @@ class FilterMenuItem extends Component {
}
FilterMenuItem.propTypes = {
filterKey: PropTypes.string.isRequired,
selectedFilterKey: PropTypes.string.isRequired,
filterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onPress: PropTypes.func.isRequired
};

View file

@ -6,6 +6,8 @@ import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { sizes } from 'Helpers/Props';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import ModalError from './ModalError';
import styles from './Modal.css';
const openModals = [];
@ -153,7 +155,8 @@ class Modal extends Component {
backdropClassName,
size,
children,
isOpen
isOpen,
onModalClose
} = this.props;
if (!isOpen) {
@ -177,7 +180,12 @@ class Modal extends Component {
)}
style={style}
>
{children}
<ErrorBoundary
errorComponent={ModalError}
onModalClose={onModalClose}
>
{children}
</ErrorBoundary>
</div>
</div>
</div>,

View file

@ -0,0 +1,15 @@
.message {
composes: message from 'Components/Error/ErrorBoundaryError.css';
margin: 0;
margin-bottom: 30px;
font-weight: normal;
font-size: 26px;
}
.details {
composes: details from 'Components/Error/ErrorBoundaryError.css';
margin: 0;
margin-top: 20px;
}

View file

@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import React from 'react';
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
import Button from 'Components/Link/Button';
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 styles from './ModalError.css';
function ModalError(props) {
const {
onModalClose,
...otherProps
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Error
</ModalHeader>
<ModalBody>
<ErrorBoundaryError
messageClassName={styles.message}
detailsClassName={styles.details}
{...otherProps}
message='There was an error loading this item'
/>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
</ModalContent>);
}
ModalError.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default ModalError;

View file

@ -8,8 +8,11 @@ function ErrorPage(props) {
version,
isLocalStorageSupported,
artistError,
customFiltersError,
tagsError,
qualityProfilesError,
languageProfilesError,
metadataProfilesError,
uiSettingsError
} = props;
@ -19,10 +22,16 @@ function ErrorPage(props) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (artistError) {
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
} else if (customFiltersError) {
errorMessage = getErrorMessage(customFiltersError, 'Failed to load custom filters from API');
} else if (tagsError) {
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API');
} else if (qualityProfilesError) {
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
} else if (languageProfilesError) {
errorMessage = getErrorMessage(languageProfilesError, 'Failed to load language profiles from API');
} else if (metadataProfilesError) {
errorMessage = getErrorMessage(metadataProfilesError, 'Failed to load metadata profiles from API');
} else if (uiSettingsError) {
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
}
@ -44,8 +53,11 @@ ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
artistError: PropTypes.object,
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,
qualityProfilesError: PropTypes.object,
languageProfilesError: PropTypes.object,
metadataProfilesError: PropTypes.object,
uiSettingsError: PropTypes.object
};

View file

@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchArtist } from 'Store/Actions/artistActions';
import { fetchTags } from 'Store/Actions/tagActions';
import { fetchQualityProfiles, fetchLanguageProfiles, fetchMetadataProfiles, fetchUISettings, fetchImportLists } from 'Store/Actions/settingsActions';
@ -30,13 +31,15 @@ function testLocalStorage() {
function createMapStateToProps() {
return createSelector(
(state) => state.artist,
(state) => state.customFilters,
(state) => state.tags,
(state) => state.settings,
(state) => state.app,
createDimensionsSelector(),
(artist, tags, settings, app, dimensions) => {
(artist, customFilters, tags, settings, app, dimensions) => {
const isPopulated = (
artist.isPopulated &&
customFilters.isPopulated &&
tags.isPopulated &&
settings.qualityProfiles.isPopulated &&
settings.languageProfiles.isPopulated &&
@ -47,6 +50,7 @@ function createMapStateToProps() {
const hasError = !!(
artist.error ||
customFilters.error ||
tags.error ||
settings.qualityProfiles.error ||
settings.languageProfiles.error ||
@ -59,6 +63,7 @@ function createMapStateToProps() {
isPopulated,
hasError,
artistError: artist.error,
customFiltersError: tags.error,
tagsError: tags.error,
qualityProfilesError: settings.qualityProfiles.error,
languageProfilesError: settings.languageProfiles.error,
@ -80,6 +85,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchArtist() {
dispatch(fetchArtist());
},
dispatchFetchCustomFilters() {
dispatch(fetchCustomFilters());
},
dispatchFetchTags() {
dispatch(fetchTags());
},
@ -126,6 +134,7 @@ class PageConnector extends Component {
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchArtist();
this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguageProfiles();
@ -190,6 +199,7 @@ PageConnector.propTypes = {
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchArtist: PropTypes.func.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import DocumentTitle from 'react-document-title';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import PageContentError from './PageContentError';
import styles from './PageContent.css';
function PageContent(props) {
@ -11,11 +13,13 @@ function PageContent(props) {
} = props;
return (
<DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}>
<div className={className}>
{children}
</div>
</DocumentTitle>
<ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}>
<div className={className}>
{children}
</div>
</DocumentTitle>
</ErrorBoundary>
);
}

View file

@ -0,0 +1,3 @@
.content {
composes: content from './PageContent.css';
}

View file

@ -0,0 +1,19 @@
import React from 'react';
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
import PageContentBodyConnector from './PageContentBodyConnector';
import styles from './PageContentError.css';
function PageContentError(props) {
return (
<div className={styles.content}>
<PageContentBodyConnector>
<ErrorBoundaryError
{...props}
message='There was an error loading this page'
/>
</PageContentBodyConnector>
</div>
);
}
export default PageContentError;

View file

@ -1,8 +1,8 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Measure from 'react-measure';
import dimensions from 'Styles/Variables/dimensions';
import Measure from 'Components/Measure';
import PageJumpBarItem from './PageJumpBarItem';
import styles from './PageJumpBar.css';

View file

@ -1,11 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Measure from 'react-measure';
import classNames from 'classnames';
import { forEach } from 'Helpers/elementChildren';
import { align, icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import SpinnerIcon from 'Components/SpinnerIcon';
import Measure from 'Components/Measure';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem';

View file

@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { WindowScroller } from 'react-virtualized';
import { scrollDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import Scroller from 'Components/Scroller/Scroller';
import VirtualTableBody from './VirtualTableBody';
import styles from './VirtualTable.css';