diff --git a/frontend/src/Components/Form/AppProfileSelectInputConnector.js b/frontend/src/Components/Form/AppProfileSelectInputConnector.js
index 1aef10c30..0ab181e2f 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 sortByName from 'Utilities/Array/sortByName';
+import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
- createSortedSectionSelector('settings.appProfiles', sortByName),
+ createSortedSectionSelector('settings.appProfiles', sortByProp('name')),
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(appProfiles, includeNoChange, includeMixed) => {
@@ -24,16 +24,20 @@ function createMapStateToProps() {
if (includeNoChange) {
values.unshift({
key: 'noChange',
- value: translate('NoChange'),
- disabled: true
+ get value() {
+ return translate('NoChange');
+ },
+ isDisabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
- value: '(Mixed)',
- disabled: true
+ get value() {
+ return `(${translate('Mixed')})`;
+ },
+ isDisabled: true
});
}
diff --git a/frontend/src/Components/Form/AvailabilitySelectInput.js b/frontend/src/Components/Form/AvailabilitySelectInput.js
deleted file mode 100644
index af9bdb2d6..000000000
--- a/frontend/src/Components/Form/AvailabilitySelectInput.js
+++ /dev/null
@@ -1,54 +0,0 @@
-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 162c79885..9cf7a429a 100644
--- a/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
+++ b/frontend/src/Components/Form/DownloadClientSelectInputConnector.js
@@ -3,7 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
-import sortByName from 'Utilities/Array/sortByName';
+import sortByProp from 'Utilities/Array/sortByProp';
+import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
@@ -21,16 +22,17 @@ function createMapStateToProps() {
const values = items
.filter((downloadClient) => downloadClient.protocol === protocolFilter)
- .sort(sortByName)
+ .sort(sortByProp('name'))
.map((downloadClient) => ({
key: downloadClient.id,
- value: downloadClient.name
+ value: downloadClient.name,
+ hint: `(${downloadClient.id})`
}));
if (includeAny) {
values.unshift({
key: 0,
- value: '(Any)'
+ value: `(${translate('Any')})`
});
}
diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js
index cc4215025..79b1c999c 100644
--- a/frontend/src/Components/Form/EnhancedSelectInput.js
+++ b/frontend/src/Components/Form/EnhancedSelectInput.js
@@ -20,6 +20,8 @@ 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;
}
@@ -137,18 +139,9 @@ class EnhancedSelectInput extends Component {
// Listeners
onComputeMaxHeight = (data) => {
- const {
- top,
- bottom
- } = data.offsets.reference;
-
const windowHeight = window.innerHeight;
- if ((/^botton/).test(data.placement)) {
- data.styles.maxHeight = windowHeight - bottom;
- } else {
- data.styles.maxHeight = top;
- }
+ data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE;
return data;
};
@@ -271,26 +264,29 @@ class EnhancedSelectInput extends Component {
this.setState({ isOpen: !this.state.isOpen });
};
- onSelect = (value) => {
- if (Array.isArray(this.props.value)) {
- let newValue = null;
- const index = this.props.value.indexOf(value);
+ onSelect = (newValue) => {
+ const { name, value, values, onChange } = this.props;
+
+ if (Array.isArray(value)) {
+ let arrayValue = null;
+ const index = value.indexOf(newValue);
+
if (index === -1) {
- newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
+ arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
} else {
- newValue = [...this.props.value];
- newValue.splice(index, 1);
+ arrayValue = [...value];
+ arrayValue.splice(index, 1);
}
- this.props.onChange({
- name: this.props.name,
- value: newValue
+ onChange({
+ name,
+ value: arrayValue
});
} else {
this.setState({ isOpen: false });
- this.props.onChange({
- name: this.props.name,
- value
+ onChange({
+ name,
+ value: newValue
});
}
};
@@ -457,6 +453,10 @@ 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 && value.includes(v.parentKey);
+ const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
return (
{error.errorMessage}
+
+ {
+ error.detailedDescription ?
+ }
+ tooltip={error.detailedDescription}
+ kind={kinds.INVERSE}
+ position={tooltipPositions.TOP}
+ /> :
+ null
+ }
);
})
@@ -39,6 +53,18 @@ 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
deleted file mode 100644
index a7145363a..000000000
--- a/frontend/src/Components/Form/FormInputButton.js
+++ /dev/null
@@ -1,54 +0,0 @@
-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
new file mode 100644
index 000000000..f61779122
--- /dev/null
+++ b/frontend/src/Components/Form/FormInputButton.tsx
@@ -0,0 +1,38 @@
+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 a00f6f8de..5b3b42de2 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -5,7 +5,6 @@ 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';
@@ -37,9 +36,6 @@ function getComponent(type) {
case inputTypes.AUTO_COMPLETE:
return AutoCompleteInput;
- case inputTypes.AVAILABILITY_SELECT:
- return AvailabilitySelectInput;
-
case inputTypes.CAPTCHA:
return CaptchaInputConnector;
@@ -260,6 +256,7 @@ 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 39a0a8e74..00024684e 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 a36650726..fade1e758 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 sortByName from 'Utilities/Array/sortByName';
+import sortByProp from 'Utilities/Array/sortByProp';
import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
- createSortedSectionSelector('indexers', sortByName),
+ createSortedSectionSelector('indexers', sortByProp('name')),
(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
new file mode 100644
index 000000000..6a37f8ddf
--- /dev/null
+++ b/frontend/src/Components/Form/InfoInput.css
@@ -0,0 +1,13 @@
+.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/PasswordInput.css.d.ts b/frontend/src/Components/Form/InfoInput.css.d.ts
similarity index 88%
rename from frontend/src/Components/Form/PasswordInput.css.d.ts
rename to frontend/src/Components/Form/InfoInput.css.d.ts
index 774807ef4..65c237dff 100644
--- a/frontend/src/Components/Form/PasswordInput.css.d.ts
+++ b/frontend/src/Components/Form/InfoInput.css.d.ts
@@ -1,7 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
- 'input': string;
+ 'message': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/Components/Form/InfoInput.js b/frontend/src/Components/Form/InfoInput.js
index eb3e9dee3..77b833c4b 100644
--- a/frontend/src/Components/Form/InfoInput.js
+++ b/frontend/src/Components/Form/InfoInput.js
@@ -1,5 +1,8 @@
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 {
@@ -7,12 +10,15 @@ class InfoInput extends Component {
// Render
render() {
- const {
- value
- } = this.props;
+ const { value } = this.props;
return (
-
+
+
+
);
}
}
diff --git a/frontend/src/Components/Form/PasswordInput.css b/frontend/src/Components/Form/PasswordInput.css
deleted file mode 100644
index 6cb162784..000000000
--- a/frontend/src/Components/Form/PasswordInput.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.input {
- composes: input from '~Components/Form/TextInput.css';
-
- font-family: $passwordFamily;
-}
diff --git a/frontend/src/Components/Form/PasswordInput.js b/frontend/src/Components/Form/PasswordInput.js
index fef54fd5a..dbc4cfdb4 100644
--- a/frontend/src/Components/Form/PasswordInput.js
+++ b/frontend/src/Components/Form/PasswordInput.js
@@ -1,7 +1,5 @@
-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) {
@@ -13,17 +11,14 @@ function PasswordInput(props) {
return (
);
}
PasswordInput.propTypes = {
- className: PropTypes.string.isRequired
-};
-
-PasswordInput.defaultProps = {
- className: styles.input
+ ...TextInput.props
};
export default PasswordInput;
diff --git a/frontend/src/Components/Form/SelectInput.css b/frontend/src/Components/Form/SelectInput.css
index aa1dfc79b..f6806b065 100644
--- a/frontend/src/Components/Form/SelectInput.css
+++ b/frontend/src/Components/Form/SelectInput.css
@@ -1,7 +1,14 @@
.select {
+ @add-mixin truncate;
+
composes: input from '~Components/Form/Input.css';
- padding: 0 11px;
+ 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;
}
.hasError {
diff --git a/frontend/src/Components/Label.js b/frontend/src/Components/Label.js
deleted file mode 100644
index 844da8165..000000000
--- a/frontend/src/Components/Label.js
+++ /dev/null
@@ -1,48 +0,0 @@
-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
new file mode 100644
index 000000000..9ab360f42
--- /dev/null
+++ b/frontend/src/Components/Label.tsx
@@ -0,0 +1,33 @@
+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
deleted file mode 100644
index cbe4691d4..000000000
--- a/frontend/src/Components/Link/Button.js
+++ /dev/null
@@ -1,54 +0,0 @@
-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
new file mode 100644
index 000000000..cf2293f59
--- /dev/null
+++ b/frontend/src/Components/Link/Button.tsx
@@ -0,0 +1,37 @@
+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
deleted file mode 100644
index 55843f05f..000000000
--- a/frontend/src/Components/Link/ClipboardButton.js
+++ /dev/null
@@ -1,139 +0,0 @@
-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
new file mode 100644
index 000000000..dfce115ac
--- /dev/null
+++ b/frontend/src/Components/Link/ClipboardButton.tsx
@@ -0,0 +1,76 @@
+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 b0676febf..6f1fd1ff7 100644
--- a/frontend/src/Components/Link/Link.tsx
+++ b/frontend/src/Components/Link/Link.tsx
@@ -1,96 +1,93 @@
import classNames from 'classnames';
import React, {
- ComponentClass,
- FunctionComponent,
+ ComponentPropsWithoutRef,
+ ElementType,
SyntheticEvent,
useCallback,
} from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
-interface ReactRouterLinkProps {
- to?: string;
-}
+export type LinkProps =
+ ComponentPropsWithoutRef & {
+ component?: C;
+ to?: string;
+ target?: string;
+ isDisabled?: LinkProps['disabled'];
+ noRouter?: boolean;
+ onPress?(event: SyntheticEvent): void;
+ };
-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;
+export default function Link({
+ className,
+ component,
+ to,
+ target,
+ type,
+ isDisabled,
+ noRouter,
+ onPress,
+ ...otherProps
+}: LinkProps) {
+ const Component = component || 'button';
const onClick = useCallback(
(event: SyntheticEvent) => {
- if (!isDisabled && onPress) {
- onPress(event);
+ if (isDisabled) {
+ return;
}
+
+ onPress?.(event);
},
[isDisabled, onPress]
);
- 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(
+ const linkClass = classNames(
className,
styles.link,
to && styles.to,
isDisabled && 'isDisabled'
);
- const elementProps = {
- ...otherProps,
- type,
- ...linkProps,
- };
+ if (to) {
+ const toLink = /\w+?:\/\//.test(to);
- elementProps.onClick = onClick;
+ if (toLink || noRouter) {
+ return (
+
+ );
+ }
- return React.createElement(el, elementProps);
+ return (
+
+ );
+ }
+
+ return (
+
+ );
}
-
-export default Link;
diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js
index 516fbb648..7bc23c066 100644
--- a/frontend/src/Components/Menu/FilterMenuContent.js
+++ b/frontend/src/Components/Menu/FilterMenuContent.js
@@ -1,5 +1,6 @@
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';
@@ -40,18 +41,26 @@ class FilterMenuContent extends Component {
}
{
- customFilters.map((filter) => {
- return (
-
- {filter.label}
-
- );
- })
+ customFilters.length > 0 ?
+ :
+ null
+ }
+
+ {
+ customFilters
+ .sort(sortByProp('label'))
+ .map((filter) => {
+ return (
+
+ {filter.label}
+
+ );
+ })
}
{
diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js
index dac20563a..b032c1eb3 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 PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector';
+import PageHeaderActionsMenu from './PageHeaderActionsMenu';
import styles from './PageHeader.css';
class PageHeader extends Component {
@@ -87,7 +87,8 @@ 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
deleted file mode 100644
index e70d32708..000000000
--- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
+++ /dev/null
@@ -1,90 +0,0 @@
-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 (
-