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

View File

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

View File

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

View File

@ -1,23 +1,43 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) {
const {
value,
values,
hint,
isMultiSelect,
includeHint,
...otherProps
} = props;
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<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>
{
@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
}
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,
isMultiSelect: PropTypes.bool.isRequired,
includeHint: PropTypes.bool.isRequired
};
HintedSelectInputSelectedValue.defaultProps = {
isMultiSelect: false,
includeHint: true
};

View File

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

View File

@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
namespace NzbDrone.Core.Annotations
{
@ -23,6 +24,20 @@ namespace NzbDrone.Core.Annotations
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
{
Textbox,

View File

@ -4,5 +4,7 @@ namespace NzbDrone.Core.Annotations
{
public int Value { get; set; }
public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
}
}

View File

@ -28,7 +28,13 @@ namespace NzbDrone.Core.Indexers.FileList
BaseUrl = "https://filelist.io";
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>();
RequiredFlags = new List<int>();
}
@ -45,7 +51,7 @@ namespace NzbDrone.Core.Indexers.FileList
[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; }
[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; }
[FieldDefinition(5, Type = FieldType.Number, Label = "Minimum Seeders", HelpText = "Minimum number of seeders required.", Advanced = true)]
@ -65,15 +71,25 @@ namespace NzbDrone.Core.Indexers.FileList
public enum FileListCategories
{
[FieldOption]
Movie_SD = 1,
[FieldOption]
Movie_DVD = 2,
[FieldOption]
Movie_DVDRO = 3,
[FieldOption]
Movie_HD = 4,
[FieldOption]
Movie_HDRO = 19,
[FieldOption]
Movie_BluRay = 20,
[FieldOption]
Movie_BluRay4K = 26,
[FieldOption]
Movie_3D = 25,
[FieldOption]
Movie_4K = 6,
[FieldOption]
Xxx = 7
}
}

View File

@ -146,10 +146,34 @@ namespace Radarr.Http.ClientSchema
{
if (selectOptions.IsEnum)
{
var options = from Enum e in Enum.GetValues(selectOptions)
select new SelectOption { Value = Convert.ToInt32(e), Name = e.ToString() };
var options = selectOptions.GetFields().Where(v => v.IsStatic).Select(v =>
{
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))