diff --git a/frontend/src/Components/Form/AppProfileSelectInputConnector.js b/frontend/src/Components/Form/AppProfileSelectInputConnector.js
index 0ab181e2f..1aef10c30 100644
--- a/frontend/src/Components/Form/AppProfileSelectInputConnector.js
+++ b/frontend/src/Components/Form/AppProfileSelectInputConnector.js
@@ -4,13 +4,13 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
-import sortByProp from 'Utilities/Array/sortByProp';
+import sortByName from 'Utilities/Array/sortByName';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
- createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
+ createSortedSectionSelector('settings.appProfiles', sortByName),
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(appProfiles, includeNoChange, includeMixed) => {
@@ -24,20 +24,16 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
- get value() {
- return translate('NoChange');
- },
- isDisabled: true
+ value: translate('NoChange'),
+ disabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
- get value() {
- return `(${translate('Mixed')})`;
- },
- isDisabled: true
+ value: '(Mixed)',
+ disabled: true
});
}
diff --git a/frontend/src/Components/Form/AvailabilitySelectInput.js b/frontend/src/Components/Form/AvailabilitySelectInput.js
new file mode 100644
index 000000000..af9bdb2d6
--- /dev/null
+++ b/frontend/src/Components/Form/AvailabilitySelectInput.js
@@ -0,0 +1,54 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import SelectInput from './SelectInput';
+
+const availabilityOptions = [
+ { key: 'announced', value: 'Announced' },
+ { key: 'inCinemas', value: 'In Cinemas' },
+ { key: 'released', value: 'Released' },
+ { key: 'preDB', value: 'PreDB' }
+];
+
+function AvailabilitySelectInput(props) {
+ const values = [...availabilityOptions];
+
+ const {
+ includeNoChange,
+ includeMixed
+ } = props;
+
+ if (includeNoChange) {
+ values.unshift({
+ key: 'noChange',
+ value: 'No Change',
+ disabled: true
+ });
+ }
+
+ if (includeMixed) {
+ values.unshift({
+ key: 'mixed',
+ value: '(Mixed)',
+ disabled: true
+ });
+ }
+
+ return (
+
+ );
+}
+
+AvailabilitySelectInput.propTypes = {
+ includeNoChange: PropTypes.bool.isRequired,
+ includeMixed: PropTypes.bool.isRequired
+};
+
+AvailabilitySelectInput.defaultProps = {
+ includeNoChange: false,
+ includeMixed: false
+};
+
+export default AvailabilitySelectInput;
diff --git a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
index 9cf7a429a..162c79885 100644
--- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
+++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
@@ -3,8 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
-import sortByProp from 'Utilities/Array/sortByProp';
-import translate from 'Utilities/String/translate';
+import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
@@ -22,17 +21,16 @@ function createMapStateToProps() {
const values = items
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
- .sort(sortByProp('name'))
+ .sort(sortByName)
.map((downloadClient) => ({
key: downloadClient.id,
- value: downloadClient.name,
- hint: `(${downloadClient.id})`
+ value: downloadClient.name
}));
if (includeAny) {
values.unshift({
key: 0,
- value: `(${translate('Any')})`
+ value: '(Any)'
});
}
diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js
index 79b1c999c..cc4215025 100644
--- a/frontend/src/Components/Form/EnhancedSelectInput.js
+++ b/frontend/src/Components/Form/EnhancedSelectInput.js
@@ -20,8 +20,6 @@ import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput';
import styles from './EnhancedSelectInput.css';
-const MINIMUM_DISTANCE_FROM_EDGE = 10;
-
function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
}
@@ -139,9 +137,18 @@ class EnhancedSelectInput extends Component {
// Listeners
onComputeMaxHeight = (data) => {
+ const {
+ top,
+ bottom
+ } = data.offsets.reference;
+
const windowHeight = window.innerHeight;
- data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
+ if ((/^botton/).test(data.placement)) {
+ data.styles.maxHeight = windowHeight - bottom;
+ } else {
+ data.styles.maxHeight = top;
+ }
return data;
};
@@ -264,29 +271,26 @@ class EnhancedSelectInput extends Component {
this.setState({ isOpen: !this.state.isOpen });
};
- onSelect = (newValue) => {
- const { name, value, values, onChange } = this.props;
-
- if (Array.isArray(value)) {
- let arrayValue = null;
- const index = value.indexOf(newValue);
-
+ onSelect = (value) => {
+ if (Array.isArray(this.props.value)) {
+ let newValue = null;
+ const index = this.props.value.indexOf(value);
if (index === -1) {
- arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
+ newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
} else {
- arrayValue = [...value];
- arrayValue.splice(index, 1);
+ newValue = [...this.props.value];
+ newValue.splice(index, 1);
}
- onChange({
- name,
- value: arrayValue
+ this.props.onChange({
+ name: this.props.name,
+ value: newValue
});
} else {
this.setState({ isOpen: false });
- onChange({
- name,
- value: newValue
+ this.props.onChange({
+ name: this.props.name,
+ value
});
}
};
@@ -453,10 +457,6 @@ class EnhancedSelectInput extends Component {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
- },
- preventOverflow: {
- enabled: true,
- boundariesElement: 'viewport'
}
}}
>
@@ -485,7 +485,7 @@ class EnhancedSelectInput extends Component {
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
- const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
+ const parentSelected = hasParent && value.includes(v.parentKey);
return (
{error.errorMessage}
-
- {
- error.detailedDescription ?
- }
- tooltip={error.detailedDescription}
- kind={kinds.INVERSE}
- position={tooltipPositions.TOP}
- /> :
- null
- }
);
})
@@ -53,18 +39,6 @@ function Form(props) {
kind={kinds.WARNING}
>
{warning.errorMessage}
-
- {
- warning.detailedDescription ?
- }
- tooltip={warning.detailedDescription}
- kind={kinds.INVERSE}
- position={tooltipPositions.TOP}
- /> :
- null
- }
);
})
diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js
new file mode 100644
index 000000000..a7145363a
--- /dev/null
+++ b/frontend/src/Components/Form/FormInputButton.js
@@ -0,0 +1,54 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import Button from 'Components/Link/Button';
+import SpinnerButton from 'Components/Link/SpinnerButton';
+import { kinds } from 'Helpers/Props';
+import styles from './FormInputButton.css';
+
+function FormInputButton(props) {
+ const {
+ className,
+ canSpin,
+ isLastButton,
+ ...otherProps
+ } = props;
+
+ if (canSpin) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+FormInputButton.propTypes = {
+ className: PropTypes.string.isRequired,
+ isLastButton: PropTypes.bool.isRequired,
+ canSpin: PropTypes.bool.isRequired
+};
+
+FormInputButton.defaultProps = {
+ className: styles.button,
+ isLastButton: true,
+ canSpin: false
+};
+
+export default FormInputButton;
diff --git a/frontend/src/Components/Form/FormInputButton.tsx b/frontend/src/Components/Form/FormInputButton.tsx
deleted file mode 100644
index f61779122..000000000
--- a/frontend/src/Components/Form/FormInputButton.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import classNames from 'classnames';
-import React from 'react';
-import Button, { ButtonProps } from 'Components/Link/Button';
-import SpinnerButton from 'Components/Link/SpinnerButton';
-import { kinds } from 'Helpers/Props';
-import styles from './FormInputButton.css';
-
-export interface FormInputButtonProps extends ButtonProps {
- canSpin?: boolean;
- isLastButton?: boolean;
-}
-
-function FormInputButton({
- className = styles.button,
- canSpin = false,
- isLastButton = true,
- ...otherProps
-}: FormInputButtonProps) {
- if (canSpin) {
- return (
-
- );
- }
-
- return (
-
- );
-}
-
-export default FormInputButton;
diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js
index 5b3b42de2..a00f6f8de 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -5,6 +5,7 @@ import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
import AutoCompleteInput from './AutoCompleteInput';
+import AvailabilitySelectInput from './AvailabilitySelectInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
import CheckInput from './CheckInput';
@@ -36,6 +37,9 @@ function getComponent(type) {
case inputTypes.AUTO_COMPLETE:
return AutoCompleteInput;
+ case inputTypes.AVAILABILITY_SELECT:
+ return AvailabilitySelectInput;
+
case inputTypes.CAPTCHA:
return CaptchaInputConnector;
@@ -256,7 +260,6 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any),
- isFloat: PropTypes.bool,
type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number,
diff --git a/frontend/src/Components/Form/FormInputHelpText.js b/frontend/src/Components/Form/FormInputHelpText.js
index 00024684e..39a0a8e74 100644
--- a/frontend/src/Components/Form/FormInputHelpText.js
+++ b/frontend/src/Components/Form/FormInputHelpText.js
@@ -25,7 +25,7 @@ function FormInputHelpText(props) {
isCheckInput && styles.isCheckInput
)}
>
- {text}
+
{
link ?
diff --git a/frontend/src/Components/Form/IndexersSelectInputConnector.js b/frontend/src/Components/Form/IndexersSelectInputConnector.js
index fade1e758..a36650726 100644
--- a/frontend/src/Components/Form/IndexersSelectInputConnector.js
+++ b/frontend/src/Components/Form/IndexersSelectInputConnector.js
@@ -4,14 +4,14 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
-import sortByProp from 'Utilities/Array/sortByProp';
+import sortByName from 'Utilities/Array/sortByName';
import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
- createSortedSectionSelector('indexers', sortByProp('name')),
+ createSortedSectionSelector('indexers', sortByName),
(value, indexers) => {
const values = [];
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
diff --git a/frontend/src/Components/Form/InfoInput.css b/frontend/src/Components/Form/InfoInput.css
deleted file mode 100644
index 6a37f8ddf..000000000
--- a/frontend/src/Components/Form/InfoInput.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.message {
- composes: alert from '~Components/Alert.css';
-
- a {
- color: var(--linkColor);
- text-decoration: none;
-
- &:hover {
- color: var(--linkHoverColor);
- text-decoration: underline;
- }
- }
-}
diff --git a/frontend/src/Components/Form/InfoInput.js b/frontend/src/Components/Form/InfoInput.js
index 77b833c4b..d26a519a4 100644
--- a/frontend/src/Components/Form/InfoInput.js
+++ b/frontend/src/Components/Form/InfoInput.js
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import { kinds } from 'Helpers/Props';
-import styles from './InfoInput.css';
class InfoInput extends Component {
@@ -13,10 +12,7 @@ class InfoInput extends Component {
const { value } = this.props;
return (
-
+
);
diff --git a/frontend/src/Components/Form/PasswordInput.css b/frontend/src/Components/Form/PasswordInput.css
new file mode 100644
index 000000000..6cb162784
--- /dev/null
+++ b/frontend/src/Components/Form/PasswordInput.css
@@ -0,0 +1,5 @@
+.input {
+ composes: input from '~Components/Form/TextInput.css';
+
+ font-family: $passwordFamily;
+}
diff --git a/frontend/src/Components/Form/InfoInput.css.d.ts b/frontend/src/Components/Form/PasswordInput.css.d.ts
similarity index 88%
rename from frontend/src/Components/Form/InfoInput.css.d.ts
rename to frontend/src/Components/Form/PasswordInput.css.d.ts
index 65c237dff..774807ef4 100644
--- a/frontend/src/Components/Form/InfoInput.css.d.ts
+++ b/frontend/src/Components/Form/PasswordInput.css.d.ts
@@ -1,7 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
- 'message': string;
+ 'input': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/Components/Form/PasswordInput.js b/frontend/src/Components/Form/PasswordInput.js
index dbc4cfdb4..fef54fd5a 100644
--- a/frontend/src/Components/Form/PasswordInput.js
+++ b/frontend/src/Components/Form/PasswordInput.js
@@ -1,5 +1,7 @@
+import PropTypes from 'prop-types';
import React from 'react';
import TextInput from './TextInput';
+import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
@@ -11,14 +13,17 @@ function PasswordInput(props) {
return (
);
}
PasswordInput.propTypes = {
- ...TextInput.props
+ className: PropTypes.string.isRequired
+};
+
+PasswordInput.defaultProps = {
+ className: styles.input
};
export default PasswordInput;
diff --git a/frontend/src/Components/Form/SelectInput.css b/frontend/src/Components/Form/SelectInput.css
index f6806b065..aa1dfc79b 100644
--- a/frontend/src/Components/Form/SelectInput.css
+++ b/frontend/src/Components/Form/SelectInput.css
@@ -1,14 +1,7 @@
.select {
- @add-mixin truncate;
-
composes: input from '~Components/Form/Input.css';
- padding: 0 30px 0 11px;
- background-image: none, linear-gradient(-135deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(-225deg, transparent 50%, var(--inputBackgroundColor) 50%), linear-gradient(var(--inputBackgroundColor) 42%, var(--textColor) 42%);
- background-position: right 30px center, right bottom, right bottom, right bottom;
- background-size: 1px 100%, 35px 27px, 30px 35px, 30px 100%;
- background-repeat: no-repeat;
- appearance: none;
+ padding: 0 11px;
}
.hasError {
diff --git a/frontend/src/Components/Label.js b/frontend/src/Components/Label.js
new file mode 100644
index 000000000..844da8165
--- /dev/null
+++ b/frontend/src/Components/Label.js
@@ -0,0 +1,48 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { kinds, sizes } from 'Helpers/Props';
+import styles from './Label.css';
+
+function Label(props) {
+ const {
+ className,
+ kind,
+ size,
+ outline,
+ children,
+ ...otherProps
+ } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+
+Label.propTypes = {
+ className: PropTypes.string.isRequired,
+ title: PropTypes.string,
+ kind: PropTypes.oneOf(kinds.all).isRequired,
+ size: PropTypes.oneOf(sizes.all).isRequired,
+ outline: PropTypes.bool.isRequired,
+ children: PropTypes.node.isRequired
+};
+
+Label.defaultProps = {
+ className: styles.label,
+ kind: kinds.DEFAULT,
+ size: sizes.SMALL,
+ outline: false
+};
+
+export default Label;
diff --git a/frontend/src/Components/Label.tsx b/frontend/src/Components/Label.tsx
deleted file mode 100644
index 9ab360f42..000000000
--- a/frontend/src/Components/Label.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import classNames from 'classnames';
-import React, { ComponentProps, ReactNode } from 'react';
-import { kinds, sizes } from 'Helpers/Props';
-import { Kind } from 'Helpers/Props/kinds';
-import { Size } from 'Helpers/Props/sizes';
-import styles from './Label.css';
-
-export interface LabelProps extends ComponentProps<'span'> {
- kind?: Extract;
- size?: Extract;
- outline?: boolean;
- children: ReactNode;
-}
-
-export default function Label({
- className = styles.label,
- kind = kinds.DEFAULT,
- size = sizes.SMALL,
- outline = false,
- ...otherProps
-}: LabelProps) {
- return (
-
- );
-}
diff --git a/frontend/src/Components/Link/Button.js b/frontend/src/Components/Link/Button.js
new file mode 100644
index 000000000..cbe4691d4
--- /dev/null
+++ b/frontend/src/Components/Link/Button.js
@@ -0,0 +1,54 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { align, kinds, sizes } from 'Helpers/Props';
+import Link from './Link';
+import styles from './Button.css';
+
+class Button extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ className,
+ buttonGroupPosition,
+ kind,
+ size,
+ children,
+ ...otherProps
+ } = this.props;
+
+ return (
+
+ {children}
+
+ );
+ }
+
+}
+
+Button.propTypes = {
+ className: PropTypes.string.isRequired,
+ buttonGroupPosition: PropTypes.oneOf(align.all),
+ kind: PropTypes.oneOf(kinds.all),
+ size: PropTypes.oneOf(sizes.all),
+ children: PropTypes.node
+};
+
+Button.defaultProps = {
+ className: styles.button,
+ kind: kinds.DEFAULT,
+ size: sizes.MEDIUM
+};
+
+export default Button;
diff --git a/frontend/src/Components/Link/Button.tsx b/frontend/src/Components/Link/Button.tsx
deleted file mode 100644
index cf2293f59..000000000
--- a/frontend/src/Components/Link/Button.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import classNames from 'classnames';
-import React from 'react';
-import { align, kinds, sizes } from 'Helpers/Props';
-import { Kind } from 'Helpers/Props/kinds';
-import { Size } from 'Helpers/Props/sizes';
-import Link, { LinkProps } from './Link';
-import styles from './Button.css';
-
-export interface ButtonProps extends Omit {
- buttonGroupPosition?: Extract<
- (typeof align.all)[number],
- keyof typeof styles
- >;
- kind?: Extract;
- size?: Extract;
- children: Required;
-}
-
-export default function Button({
- className = styles.button,
- buttonGroupPosition,
- kind = kinds.DEFAULT,
- size = sizes.MEDIUM,
- ...otherProps
-}: ButtonProps) {
- return (
-
- );
-}
diff --git a/frontend/src/Components/Link/ClipboardButton.js b/frontend/src/Components/Link/ClipboardButton.js
new file mode 100644
index 000000000..55843f05f
--- /dev/null
+++ b/frontend/src/Components/Link/ClipboardButton.js
@@ -0,0 +1,139 @@
+import Clipboard from 'clipboard';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import FormInputButton from 'Components/Form/FormInputButton';
+import Icon from 'Components/Icon';
+import { icons, kinds } from 'Helpers/Props';
+import getUniqueElememtId from 'Utilities/getUniqueElementId';
+import styles from './ClipboardButton.css';
+
+class ClipboardButton extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this._id = getUniqueElememtId();
+ this._successTimeout = null;
+ this._testResultTimeout = null;
+
+ this.state = {
+ showSuccess: false,
+ showError: false
+ };
+ }
+
+ componentDidMount() {
+ this._clipboard = new Clipboard(`#${this._id}`, {
+ text: () => this.props.value,
+ container: document.getElementById(this._id)
+ });
+
+ this._clipboard.on('success', this.onSuccess);
+ }
+
+ componentDidUpdate() {
+ const {
+ showSuccess,
+ showError
+ } = this.state;
+
+ if (showSuccess || showError) {
+ this._testResultTimeout = setTimeout(this.resetState, 3000);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._clipboard) {
+ this._clipboard.destroy();
+ }
+
+ if (this._testResultTimeout) {
+ clearTimeout(this._testResultTimeout);
+ }
+ }
+
+ //
+ // Control
+
+ resetState = () => {
+ this.setState({
+ showSuccess: false,
+ showError: false
+ });
+ };
+
+ //
+ // Listeners
+
+ onSuccess = () => {
+ this.setState({
+ showSuccess: true
+ });
+ };
+
+ onError = () => {
+ this.setState({
+ showError: true
+ });
+ };
+
+ //
+ // Render
+
+ render() {
+ const {
+ value,
+ className,
+ ...otherProps
+ } = this.props;
+
+ const {
+ showSuccess,
+ showError
+ } = this.state;
+
+ const showStateIcon = showSuccess || showError;
+ const iconName = showError ? icons.DANGER : icons.CHECK;
+ const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
+
+ return (
+
+
+ {
+ showSuccess &&
+
+
+
+ }
+
+ {
+
+
+
+ }
+
+
+ );
+ }
+}
+
+ClipboardButton.propTypes = {
+ className: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired
+};
+
+ClipboardButton.defaultProps = {
+ className: styles.button
+};
+
+export default ClipboardButton;
diff --git a/frontend/src/Components/Link/ClipboardButton.tsx b/frontend/src/Components/Link/ClipboardButton.tsx
deleted file mode 100644
index dfce115ac..000000000
--- a/frontend/src/Components/Link/ClipboardButton.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import copy from 'copy-to-clipboard';
-import React, { useCallback, useEffect, useState } from 'react';
-import FormInputButton from 'Components/Form/FormInputButton';
-import Icon from 'Components/Icon';
-import { icons, kinds } from 'Helpers/Props';
-import { ButtonProps } from './Button';
-import styles from './ClipboardButton.css';
-
-export interface ClipboardButtonProps extends Omit {
- value: string;
-}
-
-export type ClipboardState = 'success' | 'error' | null;
-
-export default function ClipboardButton({
- id,
- value,
- className = styles.button,
- ...otherProps
-}: ClipboardButtonProps) {
- const [state, setState] = useState(null);
-
- useEffect(() => {
- if (!state) {
- return;
- }
-
- const timeoutId = setTimeout(() => {
- setState(null);
- }, 3000);
-
- return () => {
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- };
- }, [state]);
-
- const handleClick = useCallback(async () => {
- try {
- if ('clipboard' in navigator) {
- await navigator.clipboard.writeText(value);
- } else {
- copy(value);
- }
-
- setState('success');
- } catch (e) {
- setState('error');
- console.error(`Failed to copy to clipboard`, e);
- }
- }, [value]);
-
- return (
-
-
- {state ? (
-
-
-
- ) : null}
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/Components/Link/Link.tsx b/frontend/src/Components/Link/Link.tsx
index 6f1fd1ff7..b0676febf 100644
--- a/frontend/src/Components/Link/Link.tsx
+++ b/frontend/src/Components/Link/Link.tsx
@@ -1,93 +1,96 @@
import classNames from 'classnames';
import React, {
- ComponentPropsWithoutRef,
- ElementType,
+ ComponentClass,
+ FunctionComponent,
SyntheticEvent,
useCallback,
} from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
-export type LinkProps =
- ComponentPropsWithoutRef & {
- component?: C;
- to?: string;
- target?: string;
- isDisabled?: LinkProps['disabled'];
- noRouter?: boolean;
- onPress?(event: SyntheticEvent): void;
- };
+interface ReactRouterLinkProps {
+ to?: string;
+}
-export default function Link({
- className,
- component,
- to,
- target,
- type,
- isDisabled,
- noRouter,
- onPress,
- ...otherProps
-}: LinkProps) {
- const Component = component || 'button';
+export interface LinkProps extends React.HTMLProps {
+ className?: string;
+ component?:
+ | string
+ | FunctionComponent
+ | ComponentClass;
+ to?: string;
+ target?: string;
+ isDisabled?: boolean;
+ noRouter?: boolean;
+ onPress?(event: SyntheticEvent): void;
+}
+function Link(props: LinkProps) {
+ const {
+ className,
+ component = 'button',
+ to,
+ target,
+ type,
+ isDisabled,
+ noRouter = false,
+ onPress,
+ ...otherProps
+ } = props;
const onClick = useCallback(
(event: SyntheticEvent) => {
- if (isDisabled) {
- return;
+ if (!isDisabled && onPress) {
+ onPress(event);
}
-
- onPress?.(event);
},
[isDisabled, onPress]
);
- const linkClass = classNames(
+ const linkProps: React.HTMLProps & ReactRouterLinkProps = {
+ target,
+ };
+ let el = component;
+
+ if (to) {
+ if (/\w+?:\/\//.test(to)) {
+ el = 'a';
+ linkProps.href = to;
+ linkProps.target = target || '_blank';
+ linkProps.rel = 'noreferrer';
+ } else if (noRouter) {
+ el = 'a';
+ linkProps.href = to;
+ linkProps.target = target || '_self';
+ } else {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ el = RouterLink;
+ linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
+ linkProps.target = target;
+ }
+ }
+
+ if (el === 'button' || el === 'input') {
+ linkProps.type = type || 'button';
+ linkProps.disabled = isDisabled;
+ }
+
+ linkProps.className = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
- if (to) {
- const toLink = /\w+?:\/\//.test(to);
+ const elementProps = {
+ ...otherProps,
+ type,
+ ...linkProps,
+ };
- if (toLink || noRouter) {
- return (
-
- );
- }
+ elementProps.onClick = onClick;
- return (
-
- );
- }
-
- return (
-
- );
+ return React.createElement(el, elementProps);
}
+
+export default Link;
diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js
index 7bc23c066..516fbb648 100644
--- a/frontend/src/Components/Menu/FilterMenuContent.js
+++ b/frontend/src/Components/Menu/FilterMenuContent.js
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import FilterMenuItem from './FilterMenuItem';
import MenuContent from './MenuContent';
@@ -41,26 +40,18 @@ class FilterMenuContent extends Component {
}
{
- customFilters.length > 0 ?
- :
- null
- }
-
- {
- customFilters
- .sort(sortByProp('label'))
- .map((filter) => {
- return (
-
- {filter.label}
-
- );
- })
+ customFilters.map((filter) => {
+ return (
+
+ {filter.label}
+
+ );
+ })
}
{
diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js
index b032c1eb3..dac20563a 100644
--- a/frontend/src/Components/Page/Header/PageHeader.js
+++ b/frontend/src/Components/Page/Header/PageHeader.js
@@ -7,7 +7,7 @@ import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import IndexerSearchInputConnector from './IndexerSearchInputConnector';
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
-import PageHeaderActionsMenu from './PageHeaderActionsMenu';
+import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
import styles from './PageHeader.css';
class PageHeader extends Component {
@@ -87,8 +87,7 @@ class PageHeader extends Component {
to="https://translate.servarr.com/projects/servarr/prowlarr/"
size={24}
/>
-
-
diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
new file mode 100644
index 000000000..e70d32708
--- /dev/null
+++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
@@ -0,0 +1,90 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Icon from 'Components/Icon';
+import Menu from 'Components/Menu/Menu';
+import MenuButton from 'Components/Menu/MenuButton';
+import MenuContent from 'Components/Menu/MenuContent';
+import MenuItem from 'Components/Menu/MenuItem';
+import MenuItemSeparator from 'Components/Menu/MenuItemSeparator';
+import { align, icons, kinds } from 'Helpers/Props';
+import translate from 'Utilities/String/translate';
+import styles from './PageHeaderActionsMenu.css';
+
+function PageHeaderActionsMenu(props) {
+ const {
+ formsAuth,
+ onKeyboardShortcutsPress,
+ onRestartPress,
+ onShutdownPress
+ } = props;
+
+ return (
+