1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-08-18 00:09:37 +02:00

New: MultiSelect input control for provider settings

Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
This commit is contained in:
Qstick 2020-10-06 23:03:29 -04:00
parent 00022fd206
commit a826c1dc25
10 changed files with 210 additions and 25 deletions

View File

@ -58,11 +58,30 @@ function getSelectedIndex(props) {
values values
} = props; } = props;
if (Array.isArray(value)) {
return values.findIndex((v) => {
return value.size && v.key === value[0];
});
}
return values.findIndex((v) => { return values.findIndex((v) => {
return v.key === value; return v.key === value;
}); });
} }
function isSelectedItem(index, props) {
const {
value,
values
} = props;
if (Array.isArray(value)) {
return value.includes(values[index].key);
}
return values[index].key === value;
}
function getKey(selectedIndex, values) { function getKey(selectedIndex, values) {
return values[selectedIndex].key; return values[selectedIndex].key;
} }
@ -92,7 +111,7 @@ class EnhancedSelectInput extends Component {
this._scheduleUpdate(); this._scheduleUpdate();
} }
if (prevProps.value !== this.props.value) { if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
this.setState({ this.setState({
selectedIndex: getSelectedIndex(this.props) selectedIndex: getSelectedIndex(this.props)
}); });
@ -134,7 +153,7 @@ class EnhancedSelectInput extends Component {
const button = document.getElementById(this._buttonId); const button = document.getElementById(this._buttonId);
const options = document.getElementById(this._optionsId); const options = document.getElementById(this._optionsId);
if (!button || this.state.isMobile) { if (!button || !event.target.isConnected || this.state.isMobile) {
return; return;
} }
@ -177,7 +196,7 @@ class EnhancedSelectInput extends Component {
} }
if ( if (
selectedIndex == null || selectedIndex == null || selectedIndex === -1 ||
getSelectedOption(selectedIndex, values).isDisabled getSelectedOption(selectedIndex, values).isDisabled
) { ) {
if (keyCode === keyCodes.UP_ARROW) { if (keyCode === keyCodes.UP_ARROW) {
@ -235,12 +254,27 @@ class EnhancedSelectInput extends Component {
} }
onSelect = (value) => { onSelect = (value) => {
this.setState({ isOpen: false }); if (Array.isArray(this.props.value)) {
let newValue = null;
const index = this.props.value.indexOf(value);
if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
} else {
newValue = [...this.props.value];
newValue.splice(index, 1);
}
this.props.onChange({
name: this.props.name,
value: newValue
});
} else {
this.setState({ isOpen: false });
this.props.onChange({ this.props.onChange({
name: this.props.name, name: this.props.name,
value value
}); });
}
} }
onMeasure = ({ width }) => { onMeasure = ({ width }) => {
@ -258,6 +292,7 @@ class EnhancedSelectInput extends Component {
const { const {
className, className,
disabledClassName, disabledClassName,
value,
values, values,
isDisabled, isDisabled,
hasError, hasError,
@ -275,6 +310,7 @@ class EnhancedSelectInput extends Component {
isMobile isMobile
} = this.state; } = this.state;
const isMultiSelect = Array.isArray(value);
const selectedOption = getSelectedOption(selectedIndex, values); const selectedOption = getSelectedOption(selectedIndex, values);
return ( return (
@ -303,9 +339,12 @@ class EnhancedSelectInput extends Component {
onPress={this.onPress} onPress={this.onPress}
> >
<SelectedValueComponent <SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions} {...selectedValueOptions}
{...selectedOption} {...selectedOption}
isDisabled={isDisabled} isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
> >
{selectedOption ? selectedOption.value : null} {selectedOption ? selectedOption.value : null}
</SelectedValueComponent> </SelectedValueComponent>
@ -359,11 +398,17 @@ class EnhancedSelectInput extends Component {
> >
{ {
values.map((v, index) => { values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
return ( return (
<OptionComponent <OptionComponent
key={v.key} key={v.key}
id={v.key} id={v.key}
isSelected={index === selectedIndex} depth={depth}
isSelected={isSelectedItem(index, this.props)}
isDisabled={parentSelected}
isMultiSelect={isMultiSelect}
{...valueOptions} {...valueOptions}
{...v} {...v}
isMobile={false} isMobile={false}
@ -401,11 +446,17 @@ class EnhancedSelectInput extends Component {
<Scroller className={styles.optionsModalScroller}> <Scroller className={styles.optionsModalScroller}>
{ {
values.map((v, index) => { values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
return ( return (
<OptionComponent <OptionComponent
key={v.key} key={v.key}
id={v.key} id={v.key}
isSelected={index === selectedIndex} depth={depth}
isSelected={isSelectedItem(index, this.props)}
isMultiSelect={isMultiSelect}
isDisabled={parentSelected}
{...valueOptions} {...valueOptions}
{...v} {...v}
isMobile={true} isMobile={true}
@ -429,9 +480,9 @@ EnhancedSelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, 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, isDisabled: PropTypes.bool.isRequired,
hasError: PropTypes.bool, hasError: PropTypes.bool,
hasWarning: PropTypes.bool, hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired, valueOptions: PropTypes.object.isRequired,

View File

@ -11,6 +11,18 @@
} }
} }
.optionCheck {
composes: container from '~./CheckInput.css';
flex: 0 0 0;
}
.optionCheckInput {
composes: input from '~./CheckInput.css';
margin-top: 0;
}
.isSelected { .isSelected {
background-color: #e2e2e2; background-color: #e2e2e2;

View File

@ -4,6 +4,7 @@ import React, { Component } from 'react';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import CheckInput from './CheckInput';
import styles from './EnhancedSelectInputOption.css'; import styles from './EnhancedSelectInputOption.css';
class EnhancedSelectInputOption extends Component { class EnhancedSelectInputOption extends Component {
@ -20,15 +21,21 @@ class EnhancedSelectInputOption extends Component {
onSelect(id); onSelect(id);
} }
onCheckPress = () => {
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
}
// //
// Render // Render
render() { render() {
const { const {
className, className,
id,
isSelected, isSelected,
isDisabled, isDisabled,
isHidden, isHidden,
isMultiSelect,
isMobile, isMobile,
children children
} = this.props; } = this.props;
@ -37,8 +44,8 @@ class EnhancedSelectInputOption extends Component {
<Link <Link
className={classNames( className={classNames(
className, className,
isSelected && styles.isSelected, isSelected && !isMultiSelect && styles.isSelected,
isDisabled && styles.isDisabled, isDisabled && !isMultiSelect && styles.isDisabled,
isHidden && styles.isHidden, isHidden && styles.isHidden,
isMobile && styles.isMobile isMobile && styles.isMobile
)} )}
@ -46,6 +53,19 @@ class EnhancedSelectInputOption extends Component {
isDisabled={isDisabled} isDisabled={isDisabled}
onPress={this.onPress} onPress={this.onPress}
> >
{
isMultiSelect &&
<CheckInput
className={styles.optionCheckInput}
containerClassName={styles.optionCheck}
name={`select-${id}`}
value={isSelected}
isDisabled={isDisabled}
onChange={this.onCheckPress}
/>
}
{children} {children}
{ {
@ -67,6 +87,7 @@ EnhancedSelectInputOption.propTypes = {
isSelected: PropTypes.bool.isRequired, isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
isHidden: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired,
isMultiSelect: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired, isMobile: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired, children: PropTypes.node.isRequired,
onSelect: PropTypes.func.isRequired onSelect: PropTypes.func.isRequired
@ -75,7 +96,8 @@ EnhancedSelectInputOption.propTypes = {
EnhancedSelectInputOption.defaultProps = { EnhancedSelectInputOption.defaultProps = {
className: styles.option, className: styles.option,
isDisabled: false, isDisabled: false,
isHidden: false isHidden: false,
isMultiSelect: false
}; };
export default EnhancedSelectInputOption; export default EnhancedSelectInputOption;

View File

@ -6,14 +6,23 @@ import styles from './HintedSelectInputOption.css';
function HintedSelectInputOption(props) { function HintedSelectInputOption(props) {
const { const {
id,
value, value,
hint, hint,
isSelected,
isDisabled,
isMultiSelect,
isMobile, isMobile,
...otherProps ...otherProps
} = props; } = props;
return ( return (
<EnhancedSelectInputOption <EnhancedSelectInputOption
id={id}
isSelected={isSelected}
isDisabled={isDisabled}
isHidden={isDisabled}
isMultiSelect={isMultiSelect}
isMobile={isMobile} isMobile={isMobile}
{...otherProps} {...otherProps}
> >
@ -36,9 +45,19 @@ function HintedSelectInputOption(props) {
} }
HintedSelectInputOption.propTypes = { HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired, value: PropTypes.string.isRequired,
hint: PropTypes.node, hint: PropTypes.node,
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isMultiSelect: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired isMobile: PropTypes.bool.isRequired
}; };
HintedSelectInputOption.defaultProps = {
isDisabled: false,
isHidden: false,
isMultiSelect: false
};
export default HintedSelectInputOption; export default HintedSelectInputOption;

View File

@ -1,23 +1,43 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css'; import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) { function HintedSelectInputSelectedValue(props) {
const { const {
value, value,
values,
hint, hint,
isMultiSelect,
includeHint, includeHint,
...otherProps ...otherProps
} = props; } = props;
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
return ( return (
<EnhancedSelectInputSelectedValue <EnhancedSelectInputSelectedValue
className={styles.selectedValue} className={styles.selectedValue}
{...otherProps} {...otherProps}
> >
<div className={styles.valueText}> <div className={styles.valueText}>
{value} {
isMultiSelect &&
value.map((key, index) => {
const v = valuesMap[key];
return (
<Label key={key}>
{v ? v.value : key}
</Label>
);
})
}
{
!isMultiSelect && value
}
</div> </div>
{ {
@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
} }
HintedSelectInputSelectedValue.propTypes = { HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.string, value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
hint: PropTypes.string, hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,
includeHint: PropTypes.bool.isRequired includeHint: PropTypes.bool.isRequired
}; };
HintedSelectInputSelectedValue.defaultProps = { HintedSelectInputSelectedValue.defaultProps = {
isMultiSelect: false,
includeHint: true includeHint: true
}; };

View File

@ -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) { function getType(type, value) {
switch (type) { switch (type) {
case 'captcha': case 'captcha':
return inputTypes.CAPTCHA; return inputTypes.CAPTCHA;
@ -45,7 +45,8 @@ function getSelectValues(selectOptions) {
return _.reduce(selectOptions, (result, option) => { return _.reduce(selectOptions, (result, option) => {
result.push({ result.push({
key: option.value, key: option.value,
value: option.name value: option.name,
hint: option.hint
}); });
return result; return result;
@ -87,7 +88,7 @@ function ProviderFieldFormGroup(props) {
<FormLabel>{label}</FormLabel> <FormLabel>{label}</FormLabel>
<FormInputGroup <FormInputGroup
type={getType(type)} type={getType(type, value)}
name={name} name={name}
label={label} label={label}
helpText={helpText} helpText={helpText}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Runtime.CompilerServices;
namespace NzbDrone.Core.Annotations namespace NzbDrone.Core.Annotations
{ {
@ -23,6 +24,20 @@ public FieldDefinitionAttribute(int order)
public string RequestAction { get; set; } public string RequestAction { get; set; }
} }
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class FieldOptionAttribute : Attribute
{
public FieldOptionAttribute(string label = null, [CallerLineNumber] int order = 0)
{
Order = order;
Label = label;
}
public int Order { get; private set; }
public string Label { get; set; }
public string Hint { get; set; }
}
public enum FieldType public enum FieldType
{ {
Textbox, Textbox,

View File

@ -4,5 +4,7 @@ public class SelectOption
{ {
public int Value { get; set; } public int Value { get; set; }
public string Name { get; set; } public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
} }
} }

View File

@ -28,7 +28,13 @@ public FileListSettings()
BaseUrl = "https://filelist.io"; BaseUrl = "https://filelist.io";
MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS; MinimumSeeders = IndexerDefaults.MINIMUM_SEEDERS;
Categories = new int[] { (int)FileListCategories.Movie_HD, (int)FileListCategories.Movie_SD, (int)FileListCategories.Movie_4K }; Categories = new int[]
{
(int)FileListCategories.Movie_HD,
(int)FileListCategories.Movie_SD,
(int)FileListCategories.Movie_4K
};
MultiLanguages = new List<int>(); MultiLanguages = new List<int>();
RequiredFlags = new List<int>(); RequiredFlags = new List<int>();
} }
@ -45,7 +51,7 @@ public FileListSettings()
[FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")] [FieldDefinition(3, Label = "API URL", Advanced = true, HelpText = "Do not change this unless you know what you're doing. Since your API key will be sent to that host.")]
public string BaseUrl { get; set; } public string BaseUrl { get; set; }
[FieldDefinition(4, Label = "Categories", Type = FieldType.TagSelect, SelectOptions = typeof(FileListCategories), Advanced = true, HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")] [FieldDefinition(4, Label = "Categories", Type = FieldType.Select, SelectOptions = typeof(FileListCategories), Advanced = true, HelpText = "Categories for use in search and feeds. If unspecified, all options are used.")]
public IEnumerable<int> Categories { get; set; } public IEnumerable<int> Categories { get; set; }
[FieldDefinition(5, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)] [FieldDefinition(5, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
@ -65,15 +71,25 @@ public NzbDroneValidationResult Validate()
public enum FileListCategories public enum FileListCategories
{ {
[FieldOption]
Movie_SD = 1, Movie_SD = 1,
[FieldOption]
Movie_DVD = 2, Movie_DVD = 2,
[FieldOption]
Movie_DVDRO = 3, Movie_DVDRO = 3,
[FieldOption]
Movie_HD = 4, Movie_HD = 4,
[FieldOption]
Movie_HDRO = 19, Movie_HDRO = 19,
[FieldOption]
Movie_BluRay = 20, Movie_BluRay = 20,
[FieldOption]
Movie_BluRay4K = 26, Movie_BluRay4K = 26,
[FieldOption]
Movie_3D = 25, Movie_3D = 25,
[FieldOption]
Movie_4K = 6, Movie_4K = 6,
[FieldOption]
Xxx = 7 Xxx = 7
} }
} }

View File

@ -146,10 +146,34 @@ private static List<SelectOption> GetSelectOptions(Type selectOptions)
{ {
if (selectOptions.IsEnum) if (selectOptions.IsEnum)
{ {
var options = from Enum e in Enum.GetValues(selectOptions) var options = selectOptions.GetFields().Where(v => v.IsStatic).Select(v =>
select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() }; {
var name = v.Name.Replace('_', ' ');
var value = Convert.ToInt32(v.GetRawConstantValue());
var attrib = v.GetCustomAttribute<FieldOptionAttribute>();
if (attrib != null)
{
return new SelectOption
{
Value = value,
Name = attrib.Label ?? name,
Order = attrib.Order,
Hint = attrib.Hint ?? $"({value})"
};
}
else
{
return new SelectOption
{
Value = value,
Name = name,
Order = value,
Hint = $"({value})"
};
}
});
return options.OrderBy(o => o.Value).ToList(); return options.OrderBy(o => o.Order).ToList();
} }
if (typeof(ISelectOptionsConverter).IsAssignableFrom(selectOptions)) if (typeof(ISelectOptionsConverter).IsAssignableFrom(selectOptions))