mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-11 15:47:09 -07:00
New: UI Updates, Tag manager, More custom filters (#437)
* New: UI Updates, Tag manager, More custom filters * fixup! Fix ScanFixture Unit Tests * Fixed: Sentry Errors from UI don't have release, branch, environment * Changed: Bump Mobile Detect for New Device Detection * Fixed: Build on changes to package.json * fixup! Add MetadataProfile filter option * fixup! Tag Note, Blacklist, Manual Import * fixup: Remove connectSection * fixup: root folder comment
This commit is contained in:
parent
afa78b1d20
commit
6581b3a2c5
198 changed files with 3057 additions and 888 deletions
|
@ -1,4 +1,4 @@
|
|||
.descriptionList {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ class DescriptionList extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
children
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<dl className={styles.descriptionList}>
|
||||
<dl className={className}>
|
||||
{children}
|
||||
</dl>
|
||||
);
|
||||
|
@ -21,7 +22,12 @@ class DescriptionList extends Component {
|
|||
}
|
||||
|
||||
DescriptionList.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.node
|
||||
};
|
||||
|
||||
DescriptionList.defaultProps = {
|
||||
className: styles.descriptionList
|
||||
};
|
||||
|
||||
export default DescriptionList;
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'continuing', name: 'Continuing' },
|
||||
{ id: 'ended', name: 'Ended' }
|
||||
];
|
||||
|
||||
function ArtistStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistStatusFilterBuilderRowValue;
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: true, name: 'true' },
|
||||
{ id: false, name: 'false' }
|
||||
];
|
||||
|
||||
function BoolFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BoolFilterBuilderRowValue;
|
|
@ -0,0 +1,15 @@
|
|||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.numberInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
composes: select from 'Components/Form/SelectInput.css';
|
||||
|
||||
margin-left: 3px;
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import isString from 'Utilities/String/isString';
|
||||
import { IN_LAST, IN_NEXT } from 'Helpers/Props/filterTypes';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import { NAME } from './FilterBuilderRowValue';
|
||||
import styles from './DateFilterBuilderRowValue.css';
|
||||
|
||||
const timeOptions = [
|
||||
{ key: 'seconds', value: 'seconds' },
|
||||
{ key: 'minutes', value: 'minutes' },
|
||||
{ key: 'hours', value: 'hours' },
|
||||
{ key: 'days', value: 'days' },
|
||||
{ key: 'weeks', value: 'weeks' },
|
||||
{ key: 'months', value: 'months' }
|
||||
];
|
||||
|
||||
function isInFilter(filterType) {
|
||||
return filterType === IN_LAST || filterType === IN_NEXT;
|
||||
}
|
||||
|
||||
class DateFilterBuilderRowValue extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
filterType,
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (isInFilter(filterType) && isString(filterValue)) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: timeOptions[0].key,
|
||||
value: null
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
filterType,
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.filterType === filterType) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInFilter(filterType) && isString(filterValue)) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: timeOptions[0].key,
|
||||
value: null
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInFilter(filterType) && !isString(filterValue)) {
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onValueChange = ({ value }) => {
|
||||
const {
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
let newValue = value;
|
||||
|
||||
if (!isString(value)) {
|
||||
newValue = {
|
||||
time: filterValue.time,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
onTimeChange = ({ value }) => {
|
||||
const {
|
||||
filterValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: {
|
||||
time: value,
|
||||
value: filterValue.value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
filterType,
|
||||
filterValue
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(isInFilter(filterType) && isString(filterValue)) ||
|
||||
(!isInFilter(filterType) && !isString(filterValue))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isInFilter(filterType)) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<NumberInput
|
||||
className={styles.numberInput}
|
||||
name={NAME}
|
||||
value={filterValue.value}
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
className={styles.selectInput}
|
||||
name={NAME}
|
||||
value={filterValue.time}
|
||||
values={timeOptions}
|
||||
onChange={this.onTimeChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue}
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DateFilterBuilderRowValue.propTypes = {
|
||||
filterType: PropTypes.string,
|
||||
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DateFilterBuilderRowValue;
|
|
@ -3,10 +3,17 @@ import React, { Component } from 'react';
|
|||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import LanguageProfileFilterBuilderRowValueConnector from './LanguageProfileFilterBuilderRowValueConnector';
|
||||
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||
import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
|
||||
function getselectedFilterBuilderProp(filterBuilderProps, name) {
|
||||
|
@ -29,6 +36,14 @@ function getDefaultFilterType(selectedFilterBuilderProp) {
|
|||
return filterBuilderTypes.possibleFilterTypes[selectedFilterBuilderProp.type][0].key;
|
||||
}
|
||||
|
||||
function getDefaultFilterValue(selectedFilterBuilderProp) {
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.DATE) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
if (!selectedFilterBuilderProp) {
|
||||
return FilterBuilderRowValueConnector;
|
||||
|
@ -37,15 +52,36 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||
const valueType = selectedFilterBuilderProp.valueType;
|
||||
|
||||
switch (valueType) {
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.LANGUAGE_PROFILE:
|
||||
return LanguageProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.METADATA_PROFILE:
|
||||
return MetadataProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.PROTOCOL:
|
||||
return ProtocolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY:
|
||||
return QualityFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValueConnector;
|
||||
|
||||
case filterBuilderValueTypes.ARTIST_STATUS:
|
||||
return ArtistStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.TAG:
|
||||
return TagFilterBuilderRowValueConnector;
|
||||
|
||||
default:
|
||||
return FilterBuilderRowValueConnector;
|
||||
}
|
||||
|
@ -59,9 +95,15 @@ class FilterBuilderRow extends Component {
|
|||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
selectedFilterBuilderProp: null
|
||||
};
|
||||
const {
|
||||
filterKey,
|
||||
filterBuilderProps
|
||||
} = props;
|
||||
|
||||
if (filterKey) {
|
||||
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -74,7 +116,7 @@ class FilterBuilderRow extends Component {
|
|||
|
||||
if (filterKey) {
|
||||
const selectedFilterBuilderProp = filterBuilderProps.find((a) => a.name === filterKey);
|
||||
this.setState({ selectedFilterBuilderProp });
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -83,13 +125,12 @@ class FilterBuilderRow extends Component {
|
|||
|
||||
const filter = {
|
||||
key: selectedFilterBuilderProp.name,
|
||||
value: [],
|
||||
value: getDefaultFilterValue(selectedFilterBuilderProp),
|
||||
type: getDefaultFilterType(selectedFilterBuilderProp)
|
||||
};
|
||||
|
||||
this.setState({ selectedFilterBuilderProp }, () => {
|
||||
onFilterChange(index, filter);
|
||||
});
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
onFilterChange(index, filter);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -107,13 +148,12 @@ class FilterBuilderRow extends Component {
|
|||
|
||||
const filter = {
|
||||
key,
|
||||
value: [],
|
||||
value: getDefaultFilterValue(selectedFilterBuilderProp),
|
||||
type
|
||||
};
|
||||
|
||||
this.setState({ selectedFilterBuilderProp }, () => {
|
||||
onFilterChange(index, filter);
|
||||
});
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
onFilterChange(index, filter);
|
||||
}
|
||||
|
||||
onFilterChange = ({ name, value }) => {
|
||||
|
@ -163,12 +203,11 @@ class FilterBuilderRow extends Component {
|
|||
filterType,
|
||||
filterValue,
|
||||
filterCount,
|
||||
filterBuilderProps
|
||||
filterBuilderProps,
|
||||
sectionItems
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedFilterBuilderProp
|
||||
} = this.state;
|
||||
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
|
||||
|
||||
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
|
||||
return {
|
||||
|
@ -209,8 +248,10 @@ class FilterBuilderRow extends Component {
|
|||
{
|
||||
filterValue != null && !!selectedFilterBuilderProp &&
|
||||
<ValueComponent
|
||||
filterType={filterType}
|
||||
filterValue={filterValue}
|
||||
selectedFilterBuilderProp={selectedFilterBuilderProp}
|
||||
sectionItems={sectionItems}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
}
|
||||
|
@ -236,10 +277,11 @@ class FilterBuilderRow extends Component {
|
|||
FilterBuilderRow.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
filterKey: PropTypes.string,
|
||||
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
|
||||
filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array, PropTypes.object]),
|
||||
filterType: PropTypes.string,
|
||||
filterCount: PropTypes.number.isRequired,
|
||||
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onFilterChange: PropTypes.func.isRequired,
|
||||
onAddPress: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
|
|
|
@ -4,7 +4,7 @@ import { kinds, filterBuilderTypes } from 'Helpers/Props';
|
|||
import TagInput, { tagShape } from 'Components/Form/TagInput';
|
||||
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||
|
||||
const NAME = 'value';
|
||||
export const NAME = 'value';
|
||||
|
||||
class FilterBuilderRowValue extends Component {
|
||||
|
||||
|
@ -91,7 +91,7 @@ class FilterBuilderRowValue extends Component {
|
|||
}
|
||||
|
||||
FilterBuilderRowValue.propTypes = {
|
||||
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired,
|
||||
filterValue: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.string, PropTypes.number])).isRequired,
|
||||
selectedFilterBuilderProp: PropTypes.object.isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
return createSelector(
|
||||
(state, { sectionItems }) => _.get(state, sectionItems),
|
||||
(state, { sectionItems }) => sectionItems,
|
||||
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
||||
(sectionItems, selectedFilterBuilderProp) => {
|
||||
if (
|
||||
|
@ -19,16 +20,20 @@ function createTagListSelector() {
|
|||
let items = [];
|
||||
|
||||
if (selectedFilterBuilderProp.optionsSelector) {
|
||||
items = sectionItems.map(selectedFilterBuilderProp.optionsSelector);
|
||||
items = selectedFilterBuilderProp.optionsSelector(sectionItems);
|
||||
} else {
|
||||
items = sectionItems.map((item) => {
|
||||
items = sectionItems.reduce((acc, item) => {
|
||||
const name = item[selectedFilterBuilderProp.name];
|
||||
|
||||
return {
|
||||
id: name,
|
||||
name
|
||||
};
|
||||
});
|
||||
if (name) {
|
||||
acc.push({
|
||||
id: name,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByName);
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languageProfiles,
|
||||
(languageProfiles) => {
|
||||
const tagList = languageProfiles.items.map((languageProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = languageProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -0,0 +1,28 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.metadataProfiles,
|
||||
(metadataProfiles) => {
|
||||
const tagList = metadataProfiles.items.map((metadataProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = metadataProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -0,0 +1,28 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const tagList = qualityProfiles.items.map((qualityProfile) => {
|
||||
const {
|
||||
id,
|
||||
name
|
||||
} = qualityProfile;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -0,0 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createTagsSelector(),
|
||||
(tagList) => {
|
||||
return {
|
||||
tagList: tagList.map((tag) => {
|
||||
const {
|
||||
id,
|
||||
label: name
|
||||
} = tag;
|
||||
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
|
@ -9,9 +9,28 @@
|
|||
}
|
||||
|
||||
.inputContainer {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.inputUnit {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
margin-top: 7px;
|
||||
width: 75px;
|
||||
color: #c6c6c6;
|
||||
text-align: right;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.inputUnitNumber {
|
||||
composes: inputUnit;
|
||||
|
||||
right: 40px;
|
||||
}
|
||||
|
||||
.pendingChangesContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
@ -83,6 +83,7 @@ function FormInputGroup(props) {
|
|||
containerClassName,
|
||||
inputClassName,
|
||||
type,
|
||||
unit,
|
||||
buttons,
|
||||
helpText,
|
||||
helpTexts,
|
||||
|
@ -115,6 +116,19 @@ function FormInputGroup(props) {
|
|||
hasButton={hasButton}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
{
|
||||
unit &&
|
||||
<div
|
||||
className={
|
||||
type === inputTypes.NUMBER ?
|
||||
styles.inputUnitNumber :
|
||||
styles.inputUnit
|
||||
}
|
||||
>
|
||||
{unit}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
@ -219,6 +233,7 @@ FormInputGroup.propTypes = {
|
|||
containerClassName: PropTypes.string.isRequired,
|
||||
inputClassName: PropTypes.string,
|
||||
type: PropTypes.string.isRequired,
|
||||
unit: PropTypes.string,
|
||||
buttons: PropTypes.oneOfType([PropTypes.node, PropTypes.arrayOf(PropTypes.node)]),
|
||||
helpText: PropTypes.string,
|
||||
helpTexts: PropTypes.arrayOf(PropTypes.string),
|
||||
|
|
|
@ -48,12 +48,14 @@ class NumberInput extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
type="number"
|
||||
value={value == null ? '' : value}
|
||||
{...otherProps}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
|
|
|
@ -1,30 +1,39 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
|
||||
function OAuthInput(props) {
|
||||
const {
|
||||
label,
|
||||
authorizing,
|
||||
error,
|
||||
onPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SpinnerButton
|
||||
<SpinnerErrorButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={authorizing}
|
||||
error={error}
|
||||
onPress={onPress}
|
||||
>
|
||||
Start OAuth
|
||||
</SpinnerButton>
|
||||
{label}
|
||||
</SpinnerErrorButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
OAuthInput.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
authorizing: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
OAuthInput.defaultProps = {
|
||||
label: 'Start OAuth'
|
||||
};
|
||||
|
||||
export default OAuthInput;
|
||||
|
|
|
@ -26,18 +26,17 @@ class OAuthInputConnector extends Component {
|
|||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
accessToken,
|
||||
accessTokenSecret,
|
||||
result,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
if (accessToken &&
|
||||
accessToken !== prevProps.accessToken &&
|
||||
accessTokenSecret &&
|
||||
accessTokenSecret !== prevProps.accessTokenSecret) {
|
||||
onChange({ name: 'AccessToken', value: accessToken });
|
||||
onChange({ name: 'AccessTokenSecret', value: accessTokenSecret });
|
||||
if (!result || result === prevProps.result) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(result).forEach((key) => {
|
||||
onChange({ name: key, value: result[key] });
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
|
@ -70,8 +69,7 @@ class OAuthInputConnector extends Component {
|
|||
}
|
||||
|
||||
OAuthInputConnector.propTypes = {
|
||||
accessToken: PropTypes.string,
|
||||
accessTokenSecret: PropTypes.string,
|
||||
result: PropTypes.object,
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
|
|
@ -7,19 +7,6 @@ import FormLabel from 'Components/Form/FormLabel';
|
|||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
function getType(type) {
|
||||
// Textbox,
|
||||
// Password,
|
||||
// Checkbox,
|
||||
// Select,
|
||||
// Path,
|
||||
// FilePath,
|
||||
// Hidden,
|
||||
// Tag,
|
||||
// Action,
|
||||
// Url,
|
||||
// Captcha
|
||||
// OAuth
|
||||
|
||||
switch (type) {
|
||||
case 'captcha':
|
||||
return inputTypes.CAPTCHA;
|
||||
|
@ -27,6 +14,8 @@ function getType(type) {
|
|||
return inputTypes.CHECK;
|
||||
case 'password':
|
||||
return inputTypes.PASSWORD;
|
||||
case 'number':
|
||||
return inputTypes.NUMBER;
|
||||
case 'path':
|
||||
return inputTypes.PATH;
|
||||
case 'select':
|
||||
|
@ -83,6 +72,7 @@ function ProviderFieldFormGroup(props) {
|
|||
<FormInputGroup
|
||||
type={getType(type)}
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
helpLink={helpLink}
|
||||
value={value}
|
||||
|
|
|
@ -32,7 +32,7 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||
}
|
||||
|
||||
RootFolderSelectInputSelectedValue.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
freeSpace: PropTypes.number,
|
||||
includeFreeSpace: PropTypes.bool.isRequired
|
||||
};
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
min-width: 20%;
|
||||
max-width: 100%;
|
||||
width: 0%;
|
||||
height: 21px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -58,20 +58,20 @@ class TagInput extends Component {
|
|||
return name;
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputContainerPress = () => {
|
||||
this._autosuggestRef.input.focus();
|
||||
}
|
||||
|
||||
onTagAdd(tag) {
|
||||
addTag = _.debounce((tag) => {
|
||||
this.props.onTagAdd(tag);
|
||||
|
||||
this.setState({
|
||||
value: '',
|
||||
suggestions: []
|
||||
});
|
||||
}, 250, { leading: true, trailing: false })
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputContainerPress = () => {
|
||||
this._autosuggestRef.input.focus();
|
||||
}
|
||||
|
||||
onInputChange = (event, { newValue, method }) => {
|
||||
|
@ -116,10 +116,9 @@ class TagInput extends Component {
|
|||
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||
|
||||
if (tag) {
|
||||
this.onTagAdd(tag);
|
||||
this.addTag(tag);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,7 +146,7 @@ class TagInput extends Component {
|
|||
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||
|
||||
if (tag) {
|
||||
this.onTagAdd(tag);
|
||||
this.addTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,7 +173,7 @@ class TagInput extends Component {
|
|||
}
|
||||
|
||||
onSuggestionSelected = (event, { suggestion }) => {
|
||||
this.onTagAdd(suggestion);
|
||||
this.addTag(suggestion);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -262,7 +261,7 @@ class TagInput extends Component {
|
|||
}
|
||||
|
||||
export const tagShape = {
|
||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]).isRequired,
|
||||
name: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
color: $white;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
@ -92,6 +91,7 @@
|
|||
|
||||
.large {
|
||||
padding: 3px 7px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,10 @@ class Menu extends Component {
|
|||
this.setMaxHeight();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._removeListener();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
.extraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1440px;
|
||||
width: 1280px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
|
|
|
@ -123,6 +123,10 @@ const links = [
|
|||
title: 'Metadata',
|
||||
to: '/settings/metadata'
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
to: '/settings/tags'
|
||||
},
|
||||
{
|
||||
title: 'General',
|
||||
to: '/settings/general'
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { repopulatePage } from 'Utilities/pagePopulator';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import { updateCommand, finishCommand } from 'Store/Actions/commandActions';
|
||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
|
||||
|
@ -34,6 +35,13 @@ function isAppDisconnected(disconnectedTime) {
|
|||
return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180;
|
||||
}
|
||||
|
||||
function getHandlerName(name) {
|
||||
name = titleCase(name);
|
||||
name = name.replace('/', '');
|
||||
|
||||
return `handle${name}`;
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app.isReconnecting,
|
||||
|
@ -91,6 +99,10 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.retryTimeoutId) {
|
||||
this.retryTimeoutId = clearTimeout(this.retryTimeoutId);
|
||||
}
|
||||
|
||||
this.signalRconnection.stop();
|
||||
this.signalRconnection = null;
|
||||
}
|
||||
|
@ -106,6 +118,11 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
|
||||
this.retryTimeoutId = setTimeout(() => {
|
||||
if (!this.signalRconnection) {
|
||||
console.error('signalR: Connection was disposed');
|
||||
return;
|
||||
}
|
||||
|
||||
this.signalRconnection.start(this.signalRconnectionOptions);
|
||||
this.retryInterval = Math.min(this.retryInterval + 1, 10);
|
||||
}, this.retryInterval * 1000);
|
||||
|
@ -117,70 +134,14 @@ class SignalRConnector extends Component {
|
|||
body
|
||||
} = message;
|
||||
|
||||
if (name === 'calendar') {
|
||||
this.handleCalendar(body);
|
||||
const handler = this[getHandlerName(name)];
|
||||
|
||||
if (handler) {
|
||||
handler(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'command') {
|
||||
this.handleCommand(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'album') {
|
||||
this.handleAlbum(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'track') {
|
||||
this.handleTrack(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'trackfile') {
|
||||
this.handleTrackFile(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'health') {
|
||||
this.handleHealth(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'artist') {
|
||||
this.handleArtist(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'queue') {
|
||||
this.handleQueue(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'queue/details') {
|
||||
this.handleQueueDetails(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'queue/status') {
|
||||
this.handleQueueStatus(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'version') {
|
||||
this.handleVersion(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'wanted/cutoff') {
|
||||
this.handleWantedCutoff(body);
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === 'wanted/missing') {
|
||||
this.handleWantedMissing(body);
|
||||
return;
|
||||
}
|
||||
console.error(`signalR: Unable to find handler for ${name}`);
|
||||
}
|
||||
|
||||
handleCalendar = (body) => {
|
||||
|
@ -237,7 +198,7 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleHealth = (body) => {
|
||||
handleHealth = () => {
|
||||
this.props.fetchHealth();
|
||||
}
|
||||
|
||||
|
@ -252,13 +213,13 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleQueue = (body) => {
|
||||
handleQueue = () => {
|
||||
if (this.props.isQueuePopulated) {
|
||||
this.props.fetchQueue();
|
||||
}
|
||||
}
|
||||
|
||||
handleQueueDetails = (body) => {
|
||||
handleQueueDetails = () => {
|
||||
this.props.fetchQueueDetails();
|
||||
}
|
||||
|
||||
|
@ -292,12 +253,16 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleSystemTask = () => {
|
||||
// No-op for now, we may want this later
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onStateChanged = (change) => {
|
||||
const state = getState(change.newState);
|
||||
console.log(`SignalR: ${state}`);
|
||||
console.log(`signalR: ${state}`);
|
||||
|
||||
if (state === 'connected') {
|
||||
// Clear disconnected time
|
||||
|
@ -326,7 +291,7 @@ class SignalRConnector extends Component {
|
|||
}
|
||||
|
||||
onReceived = (message) => {
|
||||
console.debug('SignalR: received', message.name, message.body);
|
||||
console.debug('signalR: received', message.name, message.body);
|
||||
|
||||
this.handleMessage(message);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ function TableRow(props) {
|
|||
const {
|
||||
className,
|
||||
children,
|
||||
overlayContent,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
|
@ -21,7 +22,8 @@ function TableRow(props) {
|
|||
|
||||
TableRow.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
overlayContent: PropTypes.bool
|
||||
};
|
||||
|
||||
TableRow.defaultProps = {
|
||||
|
|
|
@ -100,5 +100,5 @@
|
|||
}
|
||||
|
||||
.body {
|
||||
padding: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
|
|
@ -89,6 +89,7 @@ class Popover extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
anchor,
|
||||
title,
|
||||
body,
|
||||
|
@ -103,6 +104,7 @@ class Popover extends Component {
|
|||
{...tetherOptions[position]}
|
||||
>
|
||||
<span
|
||||
className={className}
|
||||
// onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
|
@ -141,6 +143,7 @@ class Popover extends Component {
|
|||
}
|
||||
|
||||
Popover.propTypes = {
|
||||
className: PropTypes.string,
|
||||
anchor: PropTypes.node.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue