mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-07 21:42:16 -07:00
New: Newznab/Torznab categories dropdown with indexer provided category names
This commit is contained in:
parent
2d8657a77f
commit
9b1bbaef02
22 changed files with 429 additions and 36 deletions
|
@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearOptions, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||
import DeviceInput from './DeviceInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.providerOptions,
|
||||
(state) => state.providerOptions.devices || defaultState,
|
||||
(value, devices) => {
|
||||
|
||||
return {
|
||||
|
@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
|||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this.props.dispatchClearOptions();
|
||||
this.props.dispatchClearOptions({ section: 'devices' });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -65,6 +65,7 @@ class DeviceInputConnector extends Component {
|
|||
} = this.props;
|
||||
|
||||
dispatchFetchOptions({
|
||||
section: 'devices',
|
||||
action: 'getDevices',
|
||||
provider,
|
||||
providerData
|
||||
|
|
|
@ -66,3 +66,8 @@
|
|||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
margin: 5px -5px 5px 0;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import React, { Component } from 'react';
|
|||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Measure from 'Components/Measure';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
|
@ -250,6 +251,10 @@ class EnhancedSelectInput extends Component {
|
|||
this._addListener();
|
||||
}
|
||||
|
||||
if (!this.state.isOpen && this.props.onOpen) {
|
||||
this.props.onOpen();
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
|
@ -295,6 +300,7 @@ class EnhancedSelectInput extends Component {
|
|||
value,
|
||||
values,
|
||||
isDisabled,
|
||||
isFetching,
|
||||
hasError,
|
||||
hasWarning,
|
||||
valueOptions,
|
||||
|
@ -355,9 +361,21 @@ class EnhancedSelectInput extends Component {
|
|||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching &&
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
</Measure>
|
||||
|
@ -483,12 +501,14 @@ EnhancedSelectInput.propTypes = {
|
|||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
valueOptions: PropTypes.object.isRequired,
|
||||
selectedValueOptions: PropTypes.object.isRequired,
|
||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||
optionComponent: PropTypes.elementType,
|
||||
onOpen: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
@ -496,6 +516,7 @@ EnhancedSelectInput.defaultProps = {
|
|||
className: styles.enhancedSelect,
|
||||
disabledClassName: styles.isDisabled,
|
||||
isDisabled: false,
|
||||
isFetching: false,
|
||||
valueOptions: {},
|
||||
selectedValueOptions: {},
|
||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||
|
|
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
|
@ -0,0 +1,159 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearOptions, defaultState, fetchOptions } from 'Store/Actions/providerOptionActions';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const importantFieldNames = [
|
||||
'baseUrl',
|
||||
'apiPath',
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
function getProviderDataKey(providerData) {
|
||||
if (!providerData || !providerData.fields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fields = providerData.fields
|
||||
.filter((f) => importantFieldNames.includes(f.name))
|
||||
.map((f) => f.value);
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function getSelectOptions(items) {
|
||||
if (!items) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return items.map((option) => {
|
||||
return {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
||||
(options) => {
|
||||
if (options) {
|
||||
return {
|
||||
isFetching: options.isFetching,
|
||||
values: getSelectOptions(options.items)
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchOptions: fetchOptions,
|
||||
dispatchClearOptions: clearOptions
|
||||
};
|
||||
|
||||
class EnhancedSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
refetchRequired: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this._populate();
|
||||
}
|
||||
|
||||
componentDidUpdate = (prevProps) => {
|
||||
const prevKey = getProviderDataKey(prevProps.providerData);
|
||||
const nextKey = getProviderDataKey(this.props.providerData);
|
||||
|
||||
if (!_.isEqual(prevKey, nextKey)) {
|
||||
this.setState({ refetchRequired: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOpen = () => {
|
||||
if (this.state.refetchRequired) {
|
||||
this._populate();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_populate() {
|
||||
const {
|
||||
provider,
|
||||
providerData,
|
||||
selectOptionsProviderAction,
|
||||
dispatchFetchOptions
|
||||
} = this.props;
|
||||
|
||||
if (selectOptionsProviderAction) {
|
||||
this.setState({ refetchRequired: false });
|
||||
dispatchFetchOptions({
|
||||
section: selectOptionsProviderAction,
|
||||
action: selectOptionsProviderAction,
|
||||
provider,
|
||||
providerData
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
const {
|
||||
selectOptionsProviderAction,
|
||||
dispatchClearOptions
|
||||
} = this.props;
|
||||
|
||||
if (selectOptionsProviderAction) {
|
||||
dispatchClearOptions({ section: selectOptionsProviderAction });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onOpen={this.onOpen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||
dispatchClearOptions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
|
@ -13,12 +13,14 @@
|
|||
|
||||
.optionCheck {
|
||||
composes: container from '~./CheckInput.css';
|
||||
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.optionCheckInput {
|
||||
composes: input from '~./CheckInput.css';
|
||||
margin-top: 0px;
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.isSelected {
|
||||
|
|
|
@ -32,6 +32,7 @@ class EnhancedSelectInputOption extends Component {
|
|||
const {
|
||||
className,
|
||||
id,
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isHidden,
|
||||
|
@ -54,6 +55,11 @@ class EnhancedSelectInputOption extends Component {
|
|||
onPress={this.onPress}
|
||||
>
|
||||
|
||||
{
|
||||
depth !== 0 &&
|
||||
<div style={{ width: `${depth * 20}px` }} />
|
||||
}
|
||||
|
||||
{
|
||||
isMultiSelect &&
|
||||
<CheckInput
|
||||
|
@ -84,6 +90,7 @@ class EnhancedSelectInputOption extends Component {
|
|||
EnhancedSelectInputOption.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
|
@ -95,6 +102,7 @@ EnhancedSelectInputOption.propTypes = {
|
|||
|
||||
EnhancedSelectInputOption.defaultProps = {
|
||||
className: styles.option,
|
||||
depth: 0,
|
||||
isDisabled: false,
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
|
|
|
@ -8,6 +8,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
|
|||
import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
|
@ -79,6 +80,9 @@ function getComponent(type) {
|
|||
case inputTypes.SELECT:
|
||||
return EnhancedSelectInput;
|
||||
|
||||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ function HintedSelectInputOption(props) {
|
|||
id,
|
||||
value,
|
||||
hint,
|
||||
depth,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isMultiSelect,
|
||||
|
@ -19,6 +20,7 @@ function HintedSelectInputOption(props) {
|
|||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
depth={depth}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isHidden={isDisabled}
|
||||
|
@ -48,6 +50,7 @@ HintedSelectInputOption.propTypes = {
|
|||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hint: PropTypes.node,
|
||||
depth: PropTypes.number,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
|
||||
function getType(type, value) {
|
||||
function getType({ type, selectOptionsProviderAction }) {
|
||||
switch (type) {
|
||||
case 'captcha':
|
||||
return inputTypes.CAPTCHA;
|
||||
|
@ -25,6 +25,9 @@ function getType(type, value) {
|
|||
case 'filePath':
|
||||
return inputTypes.PATH;
|
||||
case 'select':
|
||||
if (selectOptionsProviderAction) {
|
||||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
|
@ -87,7 +90,7 @@ function ProviderFieldFormGroup(props) {
|
|||
<FormLabel>{label}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={getType(type, value)}
|
||||
type={getType(props)}
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
|
@ -107,7 +110,8 @@ function ProviderFieldFormGroup(props) {
|
|||
|
||||
const selectOptionsShape = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
value: PropTypes.number.isRequired,
|
||||
hint: PropTypes.string
|
||||
};
|
||||
|
||||
ProviderFieldFormGroup.propTypes = {
|
||||
|
@ -124,6 +128,7 @@ ProviderFieldFormGroup.propTypes = {
|
|||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue