diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js
deleted file mode 100644
index 3e73d74f3..000000000
--- a/frontend/src/Components/Form/KeyValueListInput.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import KeyValueListInputItem from './KeyValueListInputItem';
-import styles from './KeyValueListInput.css';
-
-class KeyValueListInput extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- isFocused: false
- };
- }
-
- //
- // Listeners
-
- onItemChange = (index, itemValue) => {
- const {
- name,
- value,
- onChange
- } = this.props;
-
- const newValue = [...value];
-
- if (index == null) {
- newValue.push(itemValue);
- } else {
- newValue.splice(index, 1, itemValue);
- }
-
- onChange({
- name,
- value: newValue
- });
- };
-
- onRemoveItem = (index) => {
- const {
- name,
- value,
- onChange
- } = this.props;
-
- const newValue = [...value];
- newValue.splice(index, 1);
-
- onChange({
- name,
- value: newValue
- });
- };
-
- onFocus = () => {
- this.setState({
- isFocused: true
- });
- };
-
- onBlur = () => {
- this.setState({
- isFocused: false
- });
-
- const {
- name,
- value,
- onChange
- } = this.props;
-
- const newValue = value.reduce((acc, v) => {
- if (v.key || v.value) {
- acc.push(v);
- }
-
- return acc;
- }, []);
-
- if (newValue.length !== value.length) {
- onChange({
- name,
- value: newValue
- });
- }
- };
-
- //
- // Render
-
- render() {
- const {
- className,
- value,
- keyPlaceholder,
- valuePlaceholder,
- hasError,
- hasWarning
- } = this.props;
-
- const { isFocused } = this.state;
-
- return (
-
- {
- [...value, { key: '', value: '' }].map((v, index) => {
- return (
-
- );
- })
- }
-
- );
- }
-}
-
-KeyValueListInput.propTypes = {
- className: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- value: PropTypes.arrayOf(PropTypes.object).isRequired,
- hasError: PropTypes.bool,
- hasWarning: PropTypes.bool,
- keyPlaceholder: PropTypes.string,
- valuePlaceholder: PropTypes.string,
- onChange: PropTypes.func.isRequired
-};
-
-KeyValueListInput.defaultProps = {
- className: styles.inputContainer,
- value: []
-};
-
-export default KeyValueListInput;
diff --git a/frontend/src/Components/Form/KeyValueListInput.tsx b/frontend/src/Components/Form/KeyValueListInput.tsx
new file mode 100644
index 000000000..f5c6ac19b
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInput.tsx
@@ -0,0 +1,104 @@
+import classNames from 'classnames';
+import React, { useCallback, useState } from 'react';
+import { InputOnChange } from 'typings/inputs';
+import KeyValueListInputItem from './KeyValueListInputItem';
+import styles from './KeyValueListInput.css';
+
+interface KeyValue {
+ key: string;
+ value: string;
+}
+
+export interface KeyValueListInputProps {
+ className?: string;
+ name: string;
+ value: KeyValue[];
+ hasError?: boolean;
+ hasWarning?: boolean;
+ keyPlaceholder?: string;
+ valuePlaceholder?: string;
+ onChange: InputOnChange;
+}
+
+function KeyValueListInput({
+ className = styles.inputContainer,
+ name,
+ value = [],
+ hasError = false,
+ hasWarning = false,
+ keyPlaceholder,
+ valuePlaceholder,
+ onChange,
+}: KeyValueListInputProps): JSX.Element {
+ const [isFocused, setIsFocused] = useState(false);
+
+ const handleItemChange = useCallback(
+ (index: number | null, itemValue: KeyValue) => {
+ const newValue = [...value];
+
+ if (index === null) {
+ newValue.push(itemValue);
+ } else {
+ newValue.splice(index, 1, itemValue);
+ }
+
+ onChange({ name, value: newValue });
+ },
+ [value, name, onChange]
+ );
+
+ const handleRemoveItem = useCallback(
+ (index: number) => {
+ const newValue = [...value];
+ newValue.splice(index, 1);
+ onChange({ name, value: newValue });
+ },
+ [value, name, onChange]
+ );
+
+ const onFocus = useCallback(() => setIsFocused(true), []);
+
+ const onBlur = useCallback(() => {
+ setIsFocused(false);
+
+ const newValue = value.reduce((acc: KeyValue[], v) => {
+ if (v.key || v.value) {
+ acc.push(v);
+ }
+ return acc;
+ }, []);
+
+ if (newValue.length !== value.length) {
+ onChange({ name, value: newValue });
+ }
+ }, [value, name, onChange]);
+
+ return (
+
+ {[...value, { key: '', value: '' }].map((v, index) => (
+
+ ))}
+
+ );
+}
+
+export default KeyValueListInput;
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css
index 88c5847ee..ed82db459 100644
--- a/frontend/src/Components/Form/KeyValueListInputItem.css
+++ b/frontend/src/Components/Form/KeyValueListInputItem.css
@@ -5,13 +5,19 @@
&:last-child {
margin-bottom: 0;
+ border-bottom: 0;
}
}
-.inputWrapper {
+.keyInputWrapper {
flex: 1 0 0;
}
+.valueInputWrapper {
+ flex: 1 0 0;
+ min-width: 40px;
+}
+
.buttonWrapper {
flex: 0 0 22px;
}
@@ -20,4 +26,10 @@
.valueInput {
width: 100%;
border: none;
+ background-color: transparent;
+ color: var(--textColor);
+
+ &::placeholder {
+ color: var(--helpTextColor);
+ }
}
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts
index 35baf55cd..aa0c1be13 100644
--- a/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts
+++ b/frontend/src/Components/Form/KeyValueListInputItem.css.d.ts
@@ -2,10 +2,11 @@
// Please do not change this file!
interface CssExports {
'buttonWrapper': string;
- 'inputWrapper': string;
'itemContainer': string;
'keyInput': string;
+ 'keyInputWrapper': string;
'valueInput': string;
+ 'valueInputWrapper': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.js b/frontend/src/Components/Form/KeyValueListInputItem.js
deleted file mode 100644
index 5379c2129..000000000
--- a/frontend/src/Components/Form/KeyValueListInputItem.js
+++ /dev/null
@@ -1,124 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import IconButton from 'Components/Link/IconButton';
-import { icons } from 'Helpers/Props';
-import TextInput from './TextInput';
-import styles from './KeyValueListInputItem.css';
-
-class KeyValueListInputItem extends Component {
-
- //
- // Listeners
-
- onKeyChange = ({ value: keyValue }) => {
- const {
- index,
- value,
- onChange
- } = this.props;
-
- onChange(index, { key: keyValue, value });
- };
-
- onValueChange = ({ value }) => {
- // TODO: Validate here or validate at a lower level component
-
- const {
- index,
- keyValue,
- onChange
- } = this.props;
-
- onChange(index, { key: keyValue, value });
- };
-
- onRemovePress = () => {
- const {
- index,
- onRemove
- } = this.props;
-
- onRemove(index);
- };
-
- onFocus = () => {
- this.props.onFocus();
- };
-
- onBlur = () => {
- this.props.onBlur();
- };
-
- //
- // Render
-
- render() {
- const {
- keyValue,
- value,
- keyPlaceholder,
- valuePlaceholder,
- isNew
- } = this.props;
-
- return (
-
-
-
-
-
-
-
-
-
-
- {
- isNew ?
- null :
-
- }
-
-
- );
- }
-}
-
-KeyValueListInputItem.propTypes = {
- index: PropTypes.number,
- keyValue: PropTypes.string.isRequired,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
- keyPlaceholder: PropTypes.string.isRequired,
- valuePlaceholder: PropTypes.string.isRequired,
- isNew: PropTypes.bool.isRequired,
- onChange: PropTypes.func.isRequired,
- onRemove: PropTypes.func.isRequired,
- onFocus: PropTypes.func.isRequired,
- onBlur: PropTypes.func.isRequired
-};
-
-KeyValueListInputItem.defaultProps = {
- keyPlaceholder: 'Key',
- valuePlaceholder: 'Value'
-};
-
-export default KeyValueListInputItem;
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.tsx b/frontend/src/Components/Form/KeyValueListInputItem.tsx
new file mode 100644
index 000000000..c63ad50a9
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInputItem.tsx
@@ -0,0 +1,89 @@
+import React, { useCallback } from 'react';
+import IconButton from 'Components/Link/IconButton';
+import { icons } from 'Helpers/Props';
+import TextInput from './TextInput';
+import styles from './KeyValueListInputItem.css';
+
+interface KeyValueListInputItemProps {
+ index: number;
+ keyValue: string;
+ value: string;
+ keyPlaceholder?: string;
+ valuePlaceholder?: string;
+ isNew: boolean;
+ onChange: (index: number, itemValue: { key: string; value: string }) => void;
+ onRemove: (index: number) => void;
+ onFocus: () => void;
+ onBlur: () => void;
+}
+
+function KeyValueListInputItem({
+ index,
+ keyValue,
+ value,
+ keyPlaceholder = 'Key',
+ valuePlaceholder = 'Value',
+ isNew,
+ onChange,
+ onRemove,
+ onFocus,
+ onBlur,
+}: KeyValueListInputItemProps): JSX.Element {
+ const handleKeyChange = useCallback(
+ ({ value: keyValue }: { value: string }) => {
+ onChange(index, { key: keyValue, value });
+ },
+ [index, value, onChange]
+ );
+
+ const handleValueChange = useCallback(
+ ({ value }: { value: string }) => {
+ onChange(index, { key: keyValue, value });
+ },
+ [index, keyValue, onChange]
+ );
+
+ const handleRemovePress = useCallback(() => {
+ onRemove(index);
+ }, [index, onRemove]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {isNew ? null : (
+
+ )}
+
+
+ );
+}
+
+export default KeyValueListInputItem;
diff --git a/frontend/src/Components/Form/ProviderFieldFormGroup.js b/frontend/src/Components/Form/ProviderFieldFormGroup.js
index 3f3349026..0cd23298e 100644
--- a/frontend/src/Components/Form/ProviderFieldFormGroup.js
+++ b/frontend/src/Components/Form/ProviderFieldFormGroup.js
@@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.CHECK;
case 'device':
return inputTypes.DEVICE;
+ case 'keyValueList':
+ return inputTypes.KEY_VALUE_LIST;
case 'password':
return inputTypes.PASSWORD;
case 'number':
diff --git a/frontend/src/typings/inputs.ts b/frontend/src/typings/inputs.ts
index cf91149b6..eb42e316d 100644
--- a/frontend/src/typings/inputs.ts
+++ b/frontend/src/typings/inputs.ts
@@ -3,4 +3,6 @@ export type InputChanged = {
value: T;
};
+export type InputOnChange = (change: InputChanged) => void;
+
export type CheckInputChanged = InputChanged;
diff --git a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs
index 12749df9e..44c7d8cc5 100644
--- a/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs
+++ b/src/NzbDrone.Common/Reflection/ReflectionExtensions.cs
@@ -34,7 +34,8 @@ namespace NzbDrone.Common.Reflection
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Version)
- || type == typeof(decimal);
+ || type == typeof(decimal)
+ || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>));
}
public static bool IsReadable(this PropertyInfo propertyInfo)
diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs
index 413bbdb25..77cc67793 100644
--- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs
+++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs
@@ -100,7 +100,8 @@ namespace NzbDrone.Core.Annotations
TagSelect,
RootFolder,
QualityProfile,
- MovieTag
+ MovieTag,
+ KeyValueList,
}
public enum HiddenType
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 5160649fe..bc9a859f1 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -1206,6 +1206,7 @@
"NotificationsSettingsWebhookMethod": "Method",
"NotificationsSettingsWebhookMethodHelpText": "Which HTTP method to use submit to the Webservice",
"NotificationsSettingsWebhookUrl": "Webhook URL",
+ "NotificationsSettingsWebhookHeaders": "Headers",
"NotificationsSignalSettingsGroupIdPhoneNumber": "Group ID / Phone Number",
"NotificationsSignalSettingsGroupIdPhoneNumberHelpText": "Group ID / Phone Number of the receiver",
"NotificationsSignalSettingsPasswordHelpText": "Password used to authenticate requests toward signal-api",
diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs
index 23a7fbdc8..a7a7025e7 100755
--- a/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs
+++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookProxy.cs
@@ -43,6 +43,11 @@ namespace NzbDrone.Core.Notifications.Webhook
request.Credentials = new BasicNetworkCredential(settings.Username, settings.Password);
}
+ foreach (var header in settings.Headers)
+ {
+ request.Headers.Add(header.Key, header.Value);
+ }
+
_httpClient.Execute(request);
}
catch (HttpException ex)
diff --git a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs
index 565f454e2..51d91f7db 100644
--- a/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs
+++ b/src/NzbDrone.Core/Notifications/Webhook/WebhookSettings.cs
@@ -1,4 +1,5 @@
-using System;
+using System;
+using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Validation;
@@ -20,6 +21,7 @@ namespace NzbDrone.Core.Notifications.Webhook
public WebhookSettings()
{
Method = Convert.ToInt32(WebhookMethod.POST);
+ Headers = new List>();
}
[FieldDefinition(0, Label = "NotificationsSettingsWebhookUrl", Type = FieldType.Url)]
@@ -34,6 +36,9 @@ namespace NzbDrone.Core.Notifications.Webhook
[FieldDefinition(3, Label = "Password", Type = FieldType.Password, Privacy = PrivacyLevel.Password)]
public string Password { get; set; }
+ [FieldDefinition(4, Label = "NotificationsSettingsWebhookHeaders", Type = FieldType.KeyValueList, Advanced = true)]
+ public IEnumerable> Headers { get; set; }
+
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));