New: Newznab/Torznab categories dropdown with indexer provided category names
This commit is contained in:
parent
9dab2ba6e4
commit
b4c27f5d34
|
@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
|
import { fetchOptions, clearOptions, defaultState } from 'Store/Actions/providerOptionActions';
|
||||||
import DeviceInput from './DeviceInput';
|
import DeviceInput from './DeviceInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state) => state.providerOptions,
|
(state) => state.providerOptions.devices || defaultState,
|
||||||
(value, devices) => {
|
(value, devices) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
this.props.dispatchClearOptions();
|
this.props.dispatchClearOptions({ section: 'devices' });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -65,6 +65,7 @@ class DeviceInputConnector extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchOptions({
|
dispatchFetchOptions({
|
||||||
|
section: 'devices',
|
||||||
action: 'getDevices',
|
action: 'getDevices',
|
||||||
provider,
|
provider,
|
||||||
providerData
|
providerData
|
||||||
|
|
|
@ -66,3 +66,8 @@
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px -5px 5px 0;
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Measure from 'Components/Measure';
|
import Measure from 'Components/Measure';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
@ -250,6 +251,10 @@ class EnhancedSelectInput extends Component {
|
||||||
this._addListener();
|
this._addListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.isOpen) {
|
||||||
|
this.props.onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +300,7 @@ class EnhancedSelectInput extends Component {
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isFetching,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning,
|
hasWarning,
|
||||||
valueOptions,
|
valueOptions,
|
||||||
|
@ -355,9 +361,21 @@ class EnhancedSelectInput extends Component {
|
||||||
styles.dropdownArrowContainer
|
styles.dropdownArrowContainer
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching &&
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.CARET_DOWN}
|
name={icons.CARET_DOWN}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Measure>
|
</Measure>
|
||||||
|
@ -483,12 +501,14 @@ EnhancedSelectInput.propTypes = {
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasWarning: PropTypes.bool,
|
hasWarning: PropTypes.bool,
|
||||||
valueOptions: PropTypes.object.isRequired,
|
valueOptions: PropTypes.object.isRequired,
|
||||||
selectedValueOptions: PropTypes.object.isRequired,
|
selectedValueOptions: PropTypes.object.isRequired,
|
||||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||||
optionComponent: PropTypes.elementType,
|
optionComponent: PropTypes.elementType,
|
||||||
|
onOpen: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -496,6 +516,7 @@ EnhancedSelectInput.defaultProps = {
|
||||||
className: styles.enhancedSelect,
|
className: styles.enhancedSelect,
|
||||||
disabledClassName: styles.isDisabled,
|
disabledClassName: styles.isDisabled,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
isFetching: false,
|
||||||
valueOptions: {},
|
valueOptions: {},
|
||||||
selectedValueOptions: {},
|
selectedValueOptions: {},
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||||
|
|
|
@ -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 { fetchOptions, clearOptions, defaultState } 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);
|
|
@ -32,6 +32,7 @@ class EnhancedSelectInputOption extends Component {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
|
depth,
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isHidden,
|
isHidden,
|
||||||
|
@ -54,6 +55,11 @@ class EnhancedSelectInputOption extends Component {
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
depth !== 0 &&
|
||||||
|
<div style={{ width: `${depth * 20}px` }} />
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isMultiSelect &&
|
isMultiSelect &&
|
||||||
<CheckInput
|
<CheckInput
|
||||||
|
@ -84,6 +90,7 @@ class EnhancedSelectInputOption extends Component {
|
||||||
EnhancedSelectInputOption.propTypes = {
|
EnhancedSelectInputOption.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
depth: PropTypes.number.isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
@ -95,6 +102,7 @@ EnhancedSelectInputOption.propTypes = {
|
||||||
|
|
||||||
EnhancedSelectInputOption.defaultProps = {
|
EnhancedSelectInputOption.defaultProps = {
|
||||||
className: styles.option,
|
className: styles.option,
|
||||||
|
depth: 0,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
isMultiSelect: false
|
isMultiSelect: false
|
||||||
|
|
|
@ -18,6 +18,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import TagInputConnector from './TagInputConnector';
|
import TagInputConnector from './TagInputConnector';
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
import TextTagInputConnector from './TextTagInputConnector';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
@ -71,6 +72,9 @@ function getComponent(type) {
|
||||||
case inputTypes.SELECT:
|
case inputTypes.SELECT:
|
||||||
return EnhancedSelectInput;
|
return EnhancedSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.SERIES_TYPE_SELECT:
|
case inputTypes.SERIES_TYPE_SELECT:
|
||||||
return SeriesTypeSelectInput;
|
return SeriesTypeSelectInput;
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ function HintedSelectInputOption(props) {
|
||||||
id,
|
id,
|
||||||
value,
|
value,
|
||||||
hint,
|
hint,
|
||||||
|
depth,
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isMultiSelect,
|
isMultiSelect,
|
||||||
|
@ -19,6 +20,7 @@ function HintedSelectInputOption(props) {
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
id={id}
|
id={id}
|
||||||
|
depth={depth}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isHidden={isDisabled}
|
isHidden={isDisabled}
|
||||||
|
@ -48,6 +50,7 @@ HintedSelectInputOption.propTypes = {
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
hint: PropTypes.node,
|
hint: PropTypes.node,
|
||||||
|
depth: PropTypes.number,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isMultiSelect: PropTypes.bool.isRequired,
|
isMultiSelect: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
|
||||||
function getType(type, value) {
|
function getType({ type, selectOptionsProviderAction }) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'captcha':
|
case 'captcha':
|
||||||
return inputTypes.CAPTCHA;
|
return inputTypes.CAPTCHA;
|
||||||
|
@ -23,6 +23,9 @@ function getType(type, value) {
|
||||||
case 'filePath':
|
case 'filePath':
|
||||||
return inputTypes.PATH;
|
return inputTypes.PATH;
|
||||||
case 'select':
|
case 'select':
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
return inputTypes.DYNAMIC_SELECT;
|
||||||
|
}
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
|
@ -85,7 +88,7 @@ function ProviderFieldFormGroup(props) {
|
||||||
<FormLabel>{label}</FormLabel>
|
<FormLabel>{label}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={getType(type, value)}
|
type={getType(props)}
|
||||||
name={name}
|
name={name}
|
||||||
label={label}
|
label={label}
|
||||||
helpText={helpText}
|
helpText={helpText}
|
||||||
|
@ -105,7 +108,8 @@ function ProviderFieldFormGroup(props) {
|
||||||
|
|
||||||
const selectOptionsShape = {
|
const selectOptionsShape = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.number.isRequired
|
value: PropTypes.number.isRequired,
|
||||||
|
hint: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
ProviderFieldFormGroup.propTypes = {
|
ProviderFieldFormGroup.propTypes = {
|
||||||
|
@ -122,6 +126,7 @@ ProviderFieldFormGroup.propTypes = {
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
selectOptions: PropTypes.arrayOf(PropTypes.shape(selectOptionsShape)),
|
||||||
|
selectOptionsProviderAction: PropTypes.string,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
|
||||||
export const INDEXER_SELECT = 'indexerSelect';
|
export const INDEXER_SELECT = 'indexerSelect';
|
||||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||||
export const SELECT = 'select';
|
export const SELECT = 'select';
|
||||||
|
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||||
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
export const TEXT = 'text';
|
export const TEXT = 'text';
|
||||||
|
@ -34,6 +35,7 @@ export const all = [
|
||||||
INDEXER_SELECT,
|
INDEXER_SELECT,
|
||||||
ROOT_FOLDER_SELECT,
|
ROOT_FOLDER_SELECT,
|
||||||
SELECT,
|
SELECT,
|
||||||
|
DYNAMIC_SELECT,
|
||||||
SERIES_TYPE_SELECT,
|
SERIES_TYPE_SELECT,
|
||||||
TAG,
|
TAG,
|
||||||
TEXT,
|
TEXT,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import requestAction from 'Utilities/requestAction';
|
import requestAction from 'Utilities/requestAction';
|
||||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
@ -10,6 +11,9 @@ import { set } from './baseActions';
|
||||||
|
|
||||||
export const section = 'providerOptions';
|
export const section = 'providerOptions';
|
||||||
|
|
||||||
|
const lastActions = {};
|
||||||
|
let lastActionId = 0;
|
||||||
|
|
||||||
//
|
//
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -23,8 +27,8 @@ export const defaultState = {
|
||||||
//
|
//
|
||||||
// Actions Types
|
// Actions Types
|
||||||
|
|
||||||
export const FETCH_OPTIONS = 'devices/fetchOptions';
|
export const FETCH_OPTIONS = 'providers/fetchOptions';
|
||||||
export const CLEAR_OPTIONS = 'devices/clearOptions';
|
export const CLEAR_OPTIONS = 'providers/clearOptions';
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
@ -38,30 +42,55 @@ export const clearOptions = createAction(CLEAR_OPTIONS);
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
|
[FETCH_OPTIONS]: function(getState, payload, dispatch) {
|
||||||
|
const subsection = `${section}.${payload.section}`;
|
||||||
|
|
||||||
|
if (lastActions[payload.section] && _.isEqual(payload, lastActions[payload.section].payload)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionId = ++lastActionId;
|
||||||
|
|
||||||
|
lastActions[payload.section] = {
|
||||||
|
actionId,
|
||||||
|
payload
|
||||||
|
};
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section: subsection,
|
||||||
isFetching: true
|
isFetching: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = requestAction(payload);
|
const promise = requestAction(payload);
|
||||||
|
|
||||||
promise.done((data) => {
|
promise.done((data) => {
|
||||||
|
if (lastActions[payload.section]) {
|
||||||
|
if (lastActions[payload.section].actionId === actionId) {
|
||||||
|
lastActions[payload.section] = null;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section: subsection,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: true,
|
isPopulated: true,
|
||||||
error: null,
|
error: null,
|
||||||
items: data.options || []
|
items: data.options || []
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
promise.fail((xhr) => {
|
||||||
|
if (lastActions[payload.section]) {
|
||||||
|
if (lastActions[payload.section].actionId === actionId) {
|
||||||
|
lastActions[payload.section] = null;
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section: subsection,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: xhr
|
error: xhr
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -71,8 +100,12 @@ export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
[CLEAR_OPTIONS]: function(state) {
|
[CLEAR_OPTIONS]: function(state, { payload }) {
|
||||||
return updateSectionState(state, section, defaultState);
|
const subsection = `${section}.${payload.section}`;
|
||||||
|
|
||||||
|
lastActions[payload.section] = null;
|
||||||
|
|
||||||
|
return updateSectionState(state, subsection, defaultState);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, defaultState, section);
|
}, {}, section);
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Core.Annotations
|
||||||
public FieldType Type { get; set; }
|
public FieldType Type { get; set; }
|
||||||
public bool Advanced { get; set; }
|
public bool Advanced { get; set; }
|
||||||
public Type SelectOptions { get; set; }
|
public Type SelectOptions { get; set; }
|
||||||
|
public string SelectOptionsProviderAction { get; set; }
|
||||||
public string Section { get; set; }
|
public string Section { get; set; }
|
||||||
public HiddenType Hidden { get; set; }
|
public HiddenType Hidden { get; set; }
|
||||||
public PrivacyLevel Privacy { get; set; }
|
public PrivacyLevel Privacy { get; set; }
|
||||||
|
@ -38,6 +39,15 @@ namespace NzbDrone.Core.Annotations
|
||||||
public string Hint { get; set; }
|
public string Hint { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FieldSelectOption
|
||||||
|
{
|
||||||
|
public int Value { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Order { get; set; }
|
||||||
|
public string Hint { get; set; }
|
||||||
|
public int? ParentValue { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public enum FieldType
|
public enum FieldType
|
||||||
{
|
{
|
||||||
Textbox,
|
Textbox,
|
||||||
|
|
|
@ -332,7 +332,14 @@ namespace NzbDrone.Core.Indexers
|
||||||
{
|
{
|
||||||
var parser = GetParser();
|
var parser = GetParser();
|
||||||
var generator = GetRequestGenerator();
|
var generator = GetRequestGenerator();
|
||||||
var releases = FetchPage(generator.GetRecentRequests().GetAllTiers().First().First(), parser);
|
var firstRequest = generator.GetRecentRequests().GetAllTiers().FirstOrDefault()?.FirstOrDefault();
|
||||||
|
|
||||||
|
if (firstRequest == null)
|
||||||
|
{
|
||||||
|
return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var releases = FetchPage(firstRequest, parser);
|
||||||
|
|
||||||
if (releases.Empty())
|
if (releases.Empty())
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
@ -133,5 +134,31 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "newznabCategories")
|
||||||
|
{
|
||||||
|
List<NewznabCategory> categories = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Settings.BaseUrl.IsNotNullOrWhiteSpace() && Settings.ApiPath.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
categories = _capabilitiesProvider.GetCapabilities(Settings).Categories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Use default categories
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
options = NewznabCategoryFieldOptionsConverter.GetFieldSelectOptions(categories)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.RequestAction(action, query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ using NzbDrone.Common.Cache;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Http;
|
using NzbDrone.Common.Http;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Indexers.Newznab
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
{
|
{
|
||||||
|
@ -49,6 +50,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = new HttpRequest(url, HttpAccept.Rss);
|
var request = new HttpRequest(url, HttpAccept.Rss);
|
||||||
|
request.AllowAutoRedirect = true;
|
||||||
|
|
||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Web.UI.WebControls;
|
||||||
|
using NzbDrone.Core.Annotations;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Indexers.Newznab
|
||||||
|
{
|
||||||
|
public static class NewznabCategoryFieldOptionsConverter
|
||||||
|
{
|
||||||
|
public static List<FieldSelectOption> GetFieldSelectOptions(List<NewznabCategory> categories)
|
||||||
|
{
|
||||||
|
// Ignore categories not relevant for Sonarr
|
||||||
|
var ignoreCategories = new[] { 0, 1000, 2000, 3000, 4000, 6000, 7000 };
|
||||||
|
|
||||||
|
var result = new List<FieldSelectOption>();
|
||||||
|
|
||||||
|
if (categories == null)
|
||||||
|
{
|
||||||
|
// Fetching categories failed, use default Newznab categories
|
||||||
|
categories = new List<NewznabCategory>();
|
||||||
|
categories.Add(new NewznabCategory
|
||||||
|
{
|
||||||
|
Id = 5000,
|
||||||
|
Name = "TV",
|
||||||
|
Subcategories = new List<NewznabCategory>
|
||||||
|
{
|
||||||
|
new NewznabCategory { Id = 5070, Name = "Anime" },
|
||||||
|
new NewznabCategory { Id = 5080, Name = "Documentary" },
|
||||||
|
new NewznabCategory { Id = 5020, Name = "Foreign" },
|
||||||
|
new NewznabCategory { Id = 5040, Name = "HD" },
|
||||||
|
new NewznabCategory { Id = 5045, Name = "UHD" },
|
||||||
|
new NewznabCategory { Id = 5050, Name = "Other" },
|
||||||
|
new NewznabCategory { Id = 5030, Name = "SD" },
|
||||||
|
new NewznabCategory { Id = 5060, Name = "Sport" },
|
||||||
|
new NewznabCategory { Id = 5010, Name = "WEB-DL" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var category in categories)
|
||||||
|
{
|
||||||
|
if (ignoreCategories.Contains(category.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new FieldSelectOption
|
||||||
|
{
|
||||||
|
Value = category.Id,
|
||||||
|
Name = category.Name,
|
||||||
|
Hint = $"({category.Id})"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (category.Subcategories != null)
|
||||||
|
{
|
||||||
|
foreach (var subcat in category.Subcategories)
|
||||||
|
{
|
||||||
|
result.Add(new FieldSelectOption
|
||||||
|
{
|
||||||
|
Value = subcat.Id,
|
||||||
|
Name = subcat.Name,
|
||||||
|
Hint = $"({subcat.Id})",
|
||||||
|
ParentValue = category.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Sort((l, r) => l.Value.CompareTo(r.Value));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,10 +73,10 @@ namespace NzbDrone.Core.Indexers.Newznab
|
||||||
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey)]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(3, Label = "Categories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows")]
|
[FieldDefinition(3, Label = "Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable standard/daily shows")]
|
||||||
public IEnumerable<int> Categories { get; set; }
|
public IEnumerable<int> Categories { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(4, Label = "Anime Categories", HelpText = "Comma Separated list, leave blank to disable anime")]
|
[FieldDefinition(4, Label = "Anime Categories", Type = FieldType.Select, SelectOptionsProviderAction = "newznabCategories", HelpText = "Comma Separated list, leave blank to disable anime")]
|
||||||
public IEnumerable<int> AnimeCategories { get; set; }
|
public IEnumerable<int> AnimeCategories { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
[FieldDefinition(5, Label = "Additional Parameters", HelpText = "Additional Newznab parameters", Advanced = true)]
|
||||||
|
|
|
@ -124,5 +124,28 @@ namespace NzbDrone.Core.Indexers.Torznab
|
||||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override object RequestAction(string action, IDictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (action == "newznabCategories")
|
||||||
|
{
|
||||||
|
List<NewznabCategory> categories = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
categories = _capabilitiesProvider.GetCapabilities(Settings).Categories;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Use default categories
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
options = NewznabCategoryFieldOptionsConverter.GetFieldSelectOptions(categories)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.RequestAction(action, query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ namespace Sonarr.Api.V3
|
||||||
Get("schema", x => GetTemplates());
|
Get("schema", x => GetTemplates());
|
||||||
Post("test", x => Test(ReadResourceFromRequest(true)));
|
Post("test", x => Test(ReadResourceFromRequest(true)));
|
||||||
Post("testall", x => TestAll());
|
Post("testall", x => TestAll());
|
||||||
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true)));
|
Post("action/{action}", x => RequestAction(x.action, ReadResourceFromRequest(true, true)));
|
||||||
|
|
||||||
GetResourceAll = GetAll;
|
GetResourceAll = GetAll;
|
||||||
GetResourceById = GetProviderById;
|
GetResourceById = GetProviderById;
|
||||||
|
|
|
@ -14,6 +14,7 @@ namespace Sonarr.Http.ClientSchema
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public bool Advanced { get; set; }
|
public bool Advanced { get; set; }
|
||||||
public List<SelectOption> SelectOptions { get; set; }
|
public List<SelectOption> SelectOptions { get; set; }
|
||||||
|
public string SelectOptionsProviderAction { get; set; }
|
||||||
public string Section { get; set; }
|
public string Section { get; set; }
|
||||||
public string Hidden { get; set; }
|
public string Hidden { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -105,9 +105,16 @@ namespace Sonarr.Http.ClientSchema
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fieldAttribute.Type == FieldType.Select)
|
if (fieldAttribute.Type == FieldType.Select)
|
||||||
|
{
|
||||||
|
if (fieldAttribute.SelectOptionsProviderAction.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
field.SelectOptionsProviderAction = fieldAttribute.SelectOptionsProviderAction;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
|
field.SelectOptions = GetSelectOptions(fieldAttribute.SelectOptions);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fieldAttribute.Hidden != HiddenType.Visible)
|
if (fieldAttribute.Hidden != HiddenType.Visible)
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
|
using FluentValidation.Results;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using Nancy.Responses.Negotiation;
|
using Nancy.Responses.Negotiation;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -198,7 +199,7 @@ namespace Sonarr.Http.REST
|
||||||
return Negotiate.WithModel(model).WithStatusCode(statusCode);
|
return Negotiate.WithModel(model).WithStatusCode(statusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected TResource ReadResourceFromRequest(bool skipValidate = false)
|
protected TResource ReadResourceFromRequest(bool skipValidate = false, bool skipSharedValidate = false)
|
||||||
{
|
{
|
||||||
TResource resource;
|
TResource resource;
|
||||||
|
|
||||||
|
@ -216,7 +217,12 @@ namespace Sonarr.Http.REST
|
||||||
throw new BadRequestException("Request body can't be empty");
|
throw new BadRequestException("Request body can't be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
var errors = SharedValidator.Validate(resource).Errors.ToList();
|
var errors = new List<ValidationFailure>();
|
||||||
|
|
||||||
|
if (!skipSharedValidate)
|
||||||
|
{
|
||||||
|
errors.AddRange(SharedValidator.Validate(resource).Errors);
|
||||||
|
}
|
||||||
|
|
||||||
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
|
if (Request.Method.Equals("POST", StringComparison.InvariantCultureIgnoreCase) && !skipValidate && !Request.Url.Path.EndsWith("/test", StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue