mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
Category Selection on Search Page
This commit is contained in:
@@ -2,34 +2,22 @@ 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 { clearOptions, fetchOptions } from 'Store/Actions/providerOptionActions';
|
import { clearOptions, defaultState, fetchOptions } 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, { name }) => name,
|
(state) => state.providerOptions.devices || defaultState,
|
||||||
(state) => state.providerOptions,
|
(value, devices) => {
|
||||||
(value, name, devices) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = devices;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isFetching,
|
...devices,
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items: items[name] || [],
|
|
||||||
selectedDevices: value.map((valueDevice) => {
|
selectedDevices: value.map((valueDevice) => {
|
||||||
const sectionItems = items[name] || [];
|
|
||||||
|
|
||||||
// Disable equality ESLint rule so we don't need to worry about
|
// Disable equality ESLint rule so we don't need to worry about
|
||||||
// a type mismatch between the value items and the device ID.
|
// a type mismatch between the value items and the device ID.
|
||||||
// eslint-disable-next-line eqeqeq
|
// eslint-disable-next-line eqeqeq
|
||||||
const device = sectionItems.find((d) => d.id == valueDevice);
|
const device = devices.items.find((d) => d.id == valueDevice);
|
||||||
|
|
||||||
if (device) {
|
if (device) {
|
||||||
return {
|
return {
|
||||||
@@ -63,7 +51,7 @@ class DeviceInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
this.props.dispatchClearOptions();
|
this.props.dispatchClearOptions({ section: 'devices' });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -73,14 +61,12 @@ class DeviceInputConnector extends Component {
|
|||||||
const {
|
const {
|
||||||
provider,
|
provider,
|
||||||
providerData,
|
providerData,
|
||||||
dispatchFetchOptions,
|
dispatchFetchOptions
|
||||||
requestAction,
|
|
||||||
name
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchOptions({
|
dispatchFetchOptions({
|
||||||
action: requestAction,
|
section: 'devices',
|
||||||
itemSection: name,
|
action: 'getDevices',
|
||||||
provider,
|
provider,
|
||||||
providerData
|
providerData
|
||||||
});
|
});
|
||||||
@@ -109,7 +95,6 @@ class DeviceInputConnector extends Component {
|
|||||||
DeviceInputConnector.propTypes = {
|
DeviceInputConnector.propTypes = {
|
||||||
provider: PropTypes.string.isRequired,
|
provider: PropTypes.string.isRequired,
|
||||||
providerData: PropTypes.object.isRequired,
|
providerData: PropTypes.object.isRequired,
|
||||||
requestAction: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
dispatchFetchOptions: PropTypes.func.isRequired,
|
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||||
|
@@ -5,6 +5,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editableContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
composes: hasError from '~Components/Form/Input.css';
|
composes: hasError from '~Components/Form/Input.css';
|
||||||
}
|
}
|
||||||
@@ -22,6 +26,16 @@
|
|||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdownArrowContainerEditable {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-right: 17px;
|
||||||
|
width: 30%;
|
||||||
|
height: 35px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdownArrowContainerDisabled {
|
.dropdownArrowContainerDisabled {
|
||||||
composes: dropdownArrowContainer;
|
composes: dropdownArrowContainer;
|
||||||
|
|
||||||
@@ -66,3 +80,8 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $white;
|
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 { Manager, Popper, Reference } from 'react-popper';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
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';
|
||||||
@@ -16,6 +17,7 @@ import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
|||||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
|
import TextInput from './TextInput';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
|
|
||||||
function isArrowKey(keyCode) {
|
function isArrowKey(keyCode) {
|
||||||
@@ -168,13 +170,23 @@ class EnhancedSelectInput extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
if (this.state.isOpen) {
|
||||||
|
this._removeListener();
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
|
if (!this.props.isEditable) {
|
||||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||||
const origIndex = getSelectedIndex(this.props);
|
const origIndex = getSelectedIndex(this.props);
|
||||||
|
|
||||||
if (origIndex !== this.state.selectedIndex) {
|
if (origIndex !== this.state.selectedIndex) {
|
||||||
this.setState({ selectedIndex: origIndex });
|
this.setState({ selectedIndex: origIndex });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onKeyDown = (event) => {
|
onKeyDown = (event) => {
|
||||||
const {
|
const {
|
||||||
@@ -250,6 +262,10 @@ class EnhancedSelectInput extends Component {
|
|||||||
this._addListener();
|
this._addListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.isOpen && this.props.onOpen) {
|
||||||
|
this.props.onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,15 +308,19 @@ class EnhancedSelectInput extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
disabledClassName,
|
disabledClassName,
|
||||||
|
name,
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isEditable,
|
||||||
|
isFetching,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning,
|
hasWarning,
|
||||||
valueOptions,
|
valueOptions,
|
||||||
selectedValueOptions,
|
selectedValueOptions,
|
||||||
selectedValueComponent: SelectedValueComponent,
|
selectedValueComponent: SelectedValueComponent,
|
||||||
optionComponent: OptionComponent
|
optionComponent: OptionComponent,
|
||||||
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -326,6 +346,47 @@ class EnhancedSelectInput extends Component {
|
|||||||
whitelist={['width']}
|
whitelist={['width']}
|
||||||
onMeasure={this.onMeasure}
|
onMeasure={this.onMeasure}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
isEditable ?
|
||||||
|
<div
|
||||||
|
className={styles.editableContainer}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
className={className}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
readOnly={isDisabled}
|
||||||
|
hasError={hasError}
|
||||||
|
hasWarning={hasWarning}
|
||||||
|
onFocus={this.onFocus}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
className={classNames(
|
||||||
|
styles.dropdownArrowContainerEditable,
|
||||||
|
isDisabled ?
|
||||||
|
styles.dropdownArrowContainerDisabled :
|
||||||
|
styles.dropdownArrowContainer)
|
||||||
|
}
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching &&
|
||||||
|
<Icon
|
||||||
|
name={icons.CARET_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Link>
|
||||||
|
</div> :
|
||||||
<Link
|
<Link
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
@@ -355,11 +416,24 @@ 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>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -483,12 +557,15 @@ 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,
|
||||||
|
isEditable: 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 +573,8 @@ EnhancedSelectInput.defaultProps = {
|
|||||||
className: styles.enhancedSelect,
|
className: styles.enhancedSelect,
|
||||||
disabledClassName: styles.isDisabled,
|
disabledClassName: styles.isDisabled,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
isFetching: false,
|
||||||
|
isEditable: false,
|
||||||
valueOptions: {},
|
valueOptions: {},
|
||||||
selectedValueOptions: {},
|
selectedValueOptions: {},
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
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);
|
@@ -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
|
||||||
|
@@ -9,6 +9,7 @@ import CaptchaInputConnector from './CaptchaInputConnector';
|
|||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
import DeviceInputConnector from './DeviceInputConnector';
|
import DeviceInputConnector from './DeviceInputConnector';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
@@ -65,6 +66,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.SELECT:
|
case inputTypes.SELECT:
|
||||||
return EnhancedSelectInput;
|
return EnhancedSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.TAG:
|
case inputTypes.TAG:
|
||||||
return TagInputConnector;
|
return TagInputConnector;
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -0,0 +1,71 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { value }) => value,
|
||||||
|
(state) => state.settings.indexerCategories,
|
||||||
|
(value, categories) => {
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
categories.items.forEach((element) => {
|
||||||
|
values.push({
|
||||||
|
key: element.id,
|
||||||
|
value: element.name,
|
||||||
|
hint: `(${element.id})`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (element.subCategories && element.subCategories.length > 0) {
|
||||||
|
element.subCategories.forEach((subCat) => {
|
||||||
|
values.push({
|
||||||
|
key: subCat.id,
|
||||||
|
value: subCat.name,
|
||||||
|
hint: `(${subCat.id})`,
|
||||||
|
parentKey: element.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(values);
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IndexersSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
onChange = ({ name, value }) => {
|
||||||
|
this.props.onChange({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...this.props}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexersSelectInputConnector.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
indexerIds: PropTypes.number,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(IndexersSelectInputConnector);
|
@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
|
||||||
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;
|
||||||
@@ -63,7 +66,6 @@ function ProviderFieldFormGroup(props) {
|
|||||||
value,
|
value,
|
||||||
type,
|
type,
|
||||||
advanced,
|
advanced,
|
||||||
requestAction,
|
|
||||||
hidden,
|
hidden,
|
||||||
pending,
|
pending,
|
||||||
errors,
|
errors,
|
||||||
@@ -88,7 +90,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}
|
||||||
@@ -100,7 +102,6 @@ function ProviderFieldFormGroup(props) {
|
|||||||
pending={pending}
|
pending={pending}
|
||||||
includeFiles={type === 'filePath' ? true : undefined}
|
includeFiles={type === 'filePath' ? true : undefined}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
requestAction={requestAction}
|
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -109,7 +110,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 = {
|
||||||
@@ -121,12 +123,12 @@ ProviderFieldFormGroup.propTypes = {
|
|||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
advanced: PropTypes.bool.isRequired,
|
advanced: PropTypes.bool.isRequired,
|
||||||
requestAction: PropTypes.string,
|
|
||||||
hidden: PropTypes.string,
|
hidden: PropTypes.string,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ import { createSelector } from 'reselect';
|
|||||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
import { fetchIndexers } from 'Store/Actions/indexerActions';
|
||||||
import { fetchIndexerFlags, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
import { fetchIndexerCategories, fetchIndexerFlags, fetchLanguages, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
@@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
|
|||||||
(state) => state.settings.ui.isPopulated,
|
(state) => state.settings.ui.isPopulated,
|
||||||
(state) => state.settings.languages.isPopulated,
|
(state) => state.settings.languages.isPopulated,
|
||||||
(state) => state.indexers.isPopulated,
|
(state) => state.indexers.isPopulated,
|
||||||
|
(state) => state.settings.indexerCategories.isPopulated,
|
||||||
(state) => state.settings.indexerFlags.isPopulated,
|
(state) => state.settings.indexerFlags.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
(
|
(
|
||||||
@@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
|
|||||||
uiSettingsIsPopulated,
|
uiSettingsIsPopulated,
|
||||||
languagesIsPopulated,
|
languagesIsPopulated,
|
||||||
indexersIsPopulated,
|
indexersIsPopulated,
|
||||||
|
indexerCategoriesIsPopulated,
|
||||||
indexerFlagsIsPopulated,
|
indexerFlagsIsPopulated,
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated
|
||||||
) => {
|
) => {
|
||||||
@@ -65,6 +67,7 @@ const selectIsPopulated = createSelector(
|
|||||||
uiSettingsIsPopulated &&
|
uiSettingsIsPopulated &&
|
||||||
languagesIsPopulated &&
|
languagesIsPopulated &&
|
||||||
indexersIsPopulated &&
|
indexersIsPopulated &&
|
||||||
|
indexerCategoriesIsPopulated &&
|
||||||
indexerFlagsIsPopulated &&
|
indexerFlagsIsPopulated &&
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated
|
||||||
);
|
);
|
||||||
@@ -77,6 +80,7 @@ const selectErrors = createSelector(
|
|||||||
(state) => state.settings.ui.error,
|
(state) => state.settings.ui.error,
|
||||||
(state) => state.settings.languages.error,
|
(state) => state.settings.languages.error,
|
||||||
(state) => state.indexers.error,
|
(state) => state.indexers.error,
|
||||||
|
(state) => state.settings.indexerCategories.error,
|
||||||
(state) => state.settings.indexerFlags.error,
|
(state) => state.settings.indexerFlags.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
(
|
(
|
||||||
@@ -85,6 +89,7 @@ const selectErrors = createSelector(
|
|||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
languagesError,
|
languagesError,
|
||||||
indexersError,
|
indexersError,
|
||||||
|
indexerCategoriesError,
|
||||||
indexerFlagsError,
|
indexerFlagsError,
|
||||||
systemStatusError
|
systemStatusError
|
||||||
) => {
|
) => {
|
||||||
@@ -94,6 +99,7 @@ const selectErrors = createSelector(
|
|||||||
uiSettingsError ||
|
uiSettingsError ||
|
||||||
languagesError ||
|
languagesError ||
|
||||||
indexersError ||
|
indexersError ||
|
||||||
|
indexerCategoriesError ||
|
||||||
indexerFlagsError ||
|
indexerFlagsError ||
|
||||||
systemStatusError
|
systemStatusError
|
||||||
);
|
);
|
||||||
@@ -105,6 +111,7 @@ const selectErrors = createSelector(
|
|||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
languagesError,
|
languagesError,
|
||||||
indexersError,
|
indexersError,
|
||||||
|
indexerCategoriesError,
|
||||||
indexerFlagsError,
|
indexerFlagsError,
|
||||||
systemStatusError
|
systemStatusError
|
||||||
};
|
};
|
||||||
@@ -150,6 +157,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
dispatchFetchIndexers() {
|
dispatchFetchIndexers() {
|
||||||
dispatch(fetchIndexers());
|
dispatch(fetchIndexers());
|
||||||
},
|
},
|
||||||
|
dispatchFetchIndexerCategories() {
|
||||||
|
dispatch(fetchIndexerCategories());
|
||||||
|
},
|
||||||
dispatchFetchIndexerFlags() {
|
dispatchFetchIndexerFlags() {
|
||||||
dispatch(fetchIndexerFlags());
|
dispatch(fetchIndexerFlags());
|
||||||
},
|
},
|
||||||
@@ -187,6 +197,7 @@ class PageConnector extends Component {
|
|||||||
this.props.dispatchFetchTags();
|
this.props.dispatchFetchTags();
|
||||||
this.props.dispatchFetchLanguages();
|
this.props.dispatchFetchLanguages();
|
||||||
this.props.dispatchFetchIndexers();
|
this.props.dispatchFetchIndexers();
|
||||||
|
this.props.dispatchFetchIndexerCategories();
|
||||||
this.props.dispatchFetchIndexerFlags();
|
this.props.dispatchFetchIndexerFlags();
|
||||||
this.props.dispatchFetchUISettings();
|
this.props.dispatchFetchUISettings();
|
||||||
this.props.dispatchFetchStatus();
|
this.props.dispatchFetchStatus();
|
||||||
@@ -210,6 +221,7 @@ class PageConnector extends Component {
|
|||||||
dispatchFetchTags,
|
dispatchFetchTags,
|
||||||
dispatchFetchLanguages,
|
dispatchFetchLanguages,
|
||||||
dispatchFetchIndexers,
|
dispatchFetchIndexers,
|
||||||
|
dispatchFetchIndexerCategories,
|
||||||
dispatchFetchIndexerFlags,
|
dispatchFetchIndexerFlags,
|
||||||
dispatchFetchUISettings,
|
dispatchFetchUISettings,
|
||||||
dispatchFetchStatus,
|
dispatchFetchStatus,
|
||||||
@@ -248,6 +260,7 @@ PageConnector.propTypes = {
|
|||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
dispatchFetchIndexers: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchIndexerCategories: PropTypes.func.isRequired,
|
||||||
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
|
import IndexersSelectInputConnector from 'Components/Form/IndexersSelectInputConnector';
|
||||||
|
import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
@@ -18,7 +19,8 @@ class SearchFooter extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
searchingReleases: false,
|
searchingReleases: false,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
indexerIds: []
|
indexerIds: [],
|
||||||
|
categories: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ class SearchFooter extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSearchPress = () => {
|
onSearchPress = () => {
|
||||||
this.props.onSearchPress(this.state.searchQuery, this.state.indexerIds);
|
this.props.onSearchPress(this.state.searchQuery, this.state.indexerIds, this.state.categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -57,7 +59,8 @@ class SearchFooter extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
indexerIds
|
indexerIds,
|
||||||
|
categories
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,13 +87,26 @@ class SearchFooter extends Component {
|
|||||||
|
|
||||||
<IndexersSelectInputConnector
|
<IndexersSelectInputConnector
|
||||||
name='indexerIds'
|
name='indexerIds'
|
||||||
placeholder='Indexers'
|
|
||||||
value={indexerIds}
|
value={indexerIds}
|
||||||
isDisabled={isFetching}
|
isDisabled={isFetching}
|
||||||
onChange={this.onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.indexerContainer}>
|
||||||
|
<SearchFooterLabel
|
||||||
|
label={'Categories'}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NewznabCategorySelectInputConnector
|
||||||
|
name='categories'
|
||||||
|
value={categories}
|
||||||
|
isDisabled={isFetching}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<div className={styles.buttonContainerContent}>
|
<div className={styles.buttonContainerContent}>
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
|
@@ -146,8 +146,8 @@ class SearchIndex extends Component {
|
|||||||
this.setState({ jumpToCharacter });
|
this.setState({ jumpToCharacter });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearchPress = (query, indexerIds) => {
|
onSearchPress = (query, indexerIds, categories) => {
|
||||||
this.props.onSearchPress({ query, indexerIds });
|
this.props.onSearchPress({ query, indexerIds, categories });
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyUp = (event) => {
|
onKeyUp = (event) => {
|
||||||
|
48
frontend/src/Store/Actions/Settings/indexerCategories.js
Normal file
48
frontend/src/Store/Actions/Settings/indexerCategories.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.indexerCategories';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_INDEXER_CATEGORIES = 'settings/indexerFlags/fetchIndexerCategories';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchIndexerCategories = createThunk(FETCH_INDEXER_CATEGORIES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: []
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_INDEXER_CATEGORIES]: createFetchHandler(section, '/indexer/categories')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@@ -1,3 +1,4 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import requestAction from 'Utilities/requestAction';
|
import requestAction from 'Utilities/requestAction';
|
||||||
@@ -10,11 +11,14 @@ import createHandleActions from './Creators/createHandleActions';
|
|||||||
|
|
||||||
export const section = 'providerOptions';
|
export const section = 'providerOptions';
|
||||||
|
|
||||||
|
const lastActions = {};
|
||||||
|
let lastActionId = 0;
|
||||||
|
|
||||||
//
|
//
|
||||||
// State
|
// State
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
items: {},
|
items: [],
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: false
|
error: false
|
||||||
@@ -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,35 +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 oldItems = getState().providerOptions.items;
|
|
||||||
const itemSection = payload.itemSection;
|
|
||||||
|
|
||||||
const promise = requestAction(payload);
|
const promise = requestAction(payload);
|
||||||
|
|
||||||
promise.done((data) => {
|
promise.done((data) => {
|
||||||
oldItems[itemSection] = data.options || [];
|
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: oldItems
|
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
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -76,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);
|
||||||
|
@@ -3,6 +3,7 @@ import { handleThunks } from 'Store/thunks';
|
|||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
import applications from './Settings/applications';
|
import applications from './Settings/applications';
|
||||||
import general from './Settings/general';
|
import general from './Settings/general';
|
||||||
|
import indexerCategories from './Settings/indexerCategories';
|
||||||
import indexerFlags from './Settings/indexerFlags';
|
import indexerFlags from './Settings/indexerFlags';
|
||||||
import indexerOptions from './Settings/indexerOptions';
|
import indexerOptions from './Settings/indexerOptions';
|
||||||
import languages from './Settings/languages';
|
import languages from './Settings/languages';
|
||||||
@@ -10,6 +11,7 @@ import notifications from './Settings/notifications';
|
|||||||
import ui from './Settings/ui';
|
import ui from './Settings/ui';
|
||||||
|
|
||||||
export * from './Settings/general';
|
export * from './Settings/general';
|
||||||
|
export * from './Settings/indexerCategories';
|
||||||
export * from './Settings/indexerFlags';
|
export * from './Settings/indexerFlags';
|
||||||
export * from './Settings/indexerOptions';
|
export * from './Settings/indexerOptions';
|
||||||
export * from './Settings/languages';
|
export * from './Settings/languages';
|
||||||
@@ -29,6 +31,7 @@ export const defaultState = {
|
|||||||
advancedSettings: false,
|
advancedSettings: false,
|
||||||
|
|
||||||
general: general.defaultState,
|
general: general.defaultState,
|
||||||
|
indexerCategories: indexerCategories.defaultState,
|
||||||
indexerFlags: indexerFlags.defaultState,
|
indexerFlags: indexerFlags.defaultState,
|
||||||
indexerOptions: indexerOptions.defaultState,
|
indexerOptions: indexerOptions.defaultState,
|
||||||
languages: languages.defaultState,
|
languages: languages.defaultState,
|
||||||
@@ -56,6 +59,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
|||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
export const actionHandlers = handleThunks({
|
||||||
...general.actionHandlers,
|
...general.actionHandlers,
|
||||||
|
...indexerCategories.actionHandlers,
|
||||||
...indexerFlags.actionHandlers,
|
...indexerFlags.actionHandlers,
|
||||||
...indexerOptions.actionHandlers,
|
...indexerOptions.actionHandlers,
|
||||||
...languages.actionHandlers,
|
...languages.actionHandlers,
|
||||||
@@ -74,6 +78,7 @@ export const reducers = createHandleActions({
|
|||||||
},
|
},
|
||||||
|
|
||||||
...general.reducers,
|
...general.reducers,
|
||||||
|
...indexerCategories.reducers,
|
||||||
...indexerFlags.reducers,
|
...indexerFlags.reducers,
|
||||||
...indexerOptions.reducers,
|
...indexerOptions.reducers,
|
||||||
...languages.reducers,
|
...languages.reducers,
|
||||||
|
@@ -19,10 +19,10 @@ 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; }
|
||||||
public string RequestAction { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
|
||||||
@@ -39,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,
|
||||||
|
@@ -48,6 +48,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;
|
||||||
|
|
||||||
|
@@ -133,7 +133,7 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||||||
if (categories != null && categories.Any())
|
if (categories != null && categories.Any())
|
||||||
{
|
{
|
||||||
var categoriesQuery = string.Join(",", categories.Distinct());
|
var categoriesQuery = string.Join(",", categories.Distinct());
|
||||||
baseUrl += string.Format("&cats={0}", categoriesQuery);
|
baseUrl += string.Format("&cat={0}", categoriesQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Settings.ApiKey.IsNotNullOrWhiteSpace())
|
if (Settings.ApiKey.IsNotNullOrWhiteSpace())
|
||||||
@@ -151,15 +151,8 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||||||
parameters += string.Format("&offset={0}", searchCriteria.Offset);
|
parameters += string.Format("&offset={0}", searchCriteria.Offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PageSize == 0)
|
|
||||||
{
|
|
||||||
yield return new IndexerRequest(string.Format("{0}{1}", baseUrl, parameters), HttpAccept.Rss);
|
yield return new IndexerRequest(string.Format("{0}{1}", baseUrl, parameters), HttpAccept.Rss);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
yield return new IndexerRequest(string.Format("{0}&offset={1}&limit={2}{3}", baseUrl, searchCriteria.Offset, searchCriteria.Limit, parameters), HttpAccept.Rss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NewsnabifyTitle(string title)
|
private static string NewsnabifyTitle(string title)
|
||||||
{
|
{
|
||||||
|
@@ -358,7 +358,14 @@ namespace NzbDrone.Core.Indexers
|
|||||||
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
|
_indexerStatusService.UpdateCookies(Definition.Id, cookies, expiration);
|
||||||
};
|
};
|
||||||
var generator = GetRequestGenerator();
|
var generator = GetRequestGenerator();
|
||||||
var releases = FetchPage(generator.GetSearchRequests(new MovieSearchCriteria()).GetAllTiers().First().First(), parser);
|
var firstRequest = generator.GetSearchRequests(new BasicSearchCriteria()).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())
|
||||||
{
|
{
|
||||||
|
@@ -28,7 +28,7 @@ namespace NzbDrone.Core.Notifications.PushBullet
|
|||||||
[FieldDefinition(0, Label = "Access Token", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.pushbullet.com/#settings/account")]
|
[FieldDefinition(0, Label = "Access Token", Privacy = PrivacyLevel.ApiKey, HelpLink = "https://www.pushbullet.com/#settings/account")]
|
||||||
public string ApiKey { get; set; }
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device, RequestAction = "getDevices")]
|
[FieldDefinition(1, Label = "Device IDs", HelpText = "List of device IDs (leave blank to send to all devices)", Type = FieldType.Device)]
|
||||||
public IEnumerable<string> DeviceIds { get; set; }
|
public IEnumerable<string> DeviceIds { get; set; }
|
||||||
|
|
||||||
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
|
[FieldDefinition(2, Label = "Channel Tags", HelpText = "List of Channel Tags to send notifications to", Type = FieldType.Tag)]
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using Prowlarr.Api.V1;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.V1.Indexers
|
||||||
|
{
|
||||||
|
public class IndexerDefaultCategoriesModule : ProwlarrV1Module
|
||||||
|
{
|
||||||
|
public IndexerDefaultCategoriesModule()
|
||||||
|
: base("/indexer/categories")
|
||||||
|
{
|
||||||
|
Get("/", movie => GetAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndexerCategory[] GetAll()
|
||||||
|
{
|
||||||
|
return NewznabStandardCategory.ParentCats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -28,7 +28,7 @@ namespace Prowlarr.Api.V1
|
|||||||
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;
|
||||||
|
@@ -35,22 +35,22 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
|
|
||||||
if (indexerIds.Count > 0)
|
if (indexerIds.Count > 0)
|
||||||
{
|
{
|
||||||
return GetSearchReleases(request.Query, indexerIds);
|
return GetSearchReleases(request.Query, indexerIds, request.Categories);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return GetSearchReleases(request.Query, null);
|
return GetSearchReleases(request.Query, null, request.Categories);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new List<SearchResource>();
|
return new List<SearchResource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SearchResource> GetSearchReleases(string query, List<int> indexerIds)
|
private List<SearchResource> GetSearchReleases(string query, List<int> indexerIds, int[] categories)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var decisions = _nzbSearhService.Search(new NewznabRequest { q = query, t = "search" }, indexerIds, true).Releases;
|
var decisions = _nzbSearhService.Search(new NewznabRequest { q = query, t = "search", cat = string.Join(",", categories) }, indexerIds, true).Releases;
|
||||||
|
|
||||||
return MapDecisions(decisions);
|
return MapDecisions(decisions);
|
||||||
}
|
}
|
||||||
|
@@ -15,9 +15,10 @@ namespace Prowlarr.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; }
|
||||||
public string RequestAction { get; set; }
|
|
||||||
|
|
||||||
public Field Clone()
|
public Field Clone()
|
||||||
{
|
{
|
||||||
|
@@ -100,14 +100,20 @@ namespace Prowlarr.Http.ClientSchema
|
|||||||
Order = fieldAttribute.Order,
|
Order = fieldAttribute.Order,
|
||||||
Advanced = fieldAttribute.Advanced,
|
Advanced = fieldAttribute.Advanced,
|
||||||
Type = fieldAttribute.Type.ToString().FirstCharToLower(),
|
Type = fieldAttribute.Type.ToString().FirstCharToLower(),
|
||||||
Section = fieldAttribute.Section,
|
Section = fieldAttribute.Section
|
||||||
RequestAction = fieldAttribute.RequestAction
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.TagSelect)
|
if (fieldAttribute.Type == FieldType.Select || fieldAttribute.Type == FieldType.TagSelect)
|
||||||
|
{
|
||||||
|
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;
|
||||||
@@ -224,7 +225,7 @@ namespace Prowlarr.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;
|
||||||
|
|
||||||
@@ -242,7 +243,12 @@ namespace Prowlarr.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))
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user