- Label
+ {translate('Label')}
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
index ed375b745..b02844c61 100644
--- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
@@ -3,10 +3,13 @@ import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import IconButton from 'Components/Link/IconButton';
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
+import sortByProp from 'Utilities/Array/sortByProp';
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
+import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
+import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
@@ -55,9 +58,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.BOOL:
return BoolFilterBuilderRowValue;
+ case filterBuilderValueTypes.CATEGORY:
+ return CategoryFilterBuilderRowValue;
+
case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue;
+ case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
+ return HistoryEventTypeFilterBuilderRowValue;
+
case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector;
@@ -198,11 +207,13 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
+ const { name, label } = availablePropFilter;
+
return {
- key: availablePropFilter.name,
- value: availablePropFilter.label
+ key: name,
+ value: typeof label === 'function' ? label() : label
};
- });
+ }).sort(sortByProp('value'));
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js
index a7aed80b6..d1419327a 100644
--- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueConnector.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { filterBuilderTypes } from 'Helpers/Props';
import * as filterTypes from 'Helpers/Props/filterTypes';
-import sortByName from 'Utilities/Array/sortByName';
+import sortByProp from 'Utilities/Array/sortByProp';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createTagListSelector() {
@@ -38,7 +38,7 @@ function createTagListSelector() {
}
return acc;
- }, []).sort(sortByName);
+ }, []).sort(sortByProp('name'));
}
return _.uniqBy(items, 'id');
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts
new file mode 100644
index 000000000..5bf9e5785
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts
@@ -0,0 +1,16 @@
+import { FilterBuilderProp } from 'App/State/AppState';
+
+interface FilterBuilderRowOnChangeProps {
+ name: string;
+ value: unknown[];
+}
+
+interface FilterBuilderRowValueProps {
+ filterType?: string;
+ filterValue: string | number | object | string[] | number[] | object[];
+ selectedFilterBuilderProp: FilterBuilderProp
;
+ sectionItem: unknown[];
+ onChange: (payload: FilterBuilderRowOnChangeProps) => void;
+}
+
+export default FilterBuilderRowValueProps;
diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx
new file mode 100644
index 000000000..03c5f7227
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import translate from 'Utilities/String/translate';
+import FilterBuilderRowValue from './FilterBuilderRowValue';
+import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
+
+const EVENT_TYPE_OPTIONS = [
+ {
+ id: 1,
+ get name() {
+ return translate('Grabbed');
+ },
+ },
+ {
+ id: 3,
+ get name() {
+ return translate('IndexerRss');
+ },
+ },
+ {
+ id: 2,
+ get name() {
+ return translate('IndexerQuery');
+ },
+ },
+ {
+ id: 4,
+ get name() {
+ return translate('IndexerAuth');
+ },
+ },
+];
+
+function HistoryEventTypeFilterBuilderRowValue(
+ props: FilterBuilderRowValueProps
+) {
+ return ;
+}
+
+export default HistoryEventTypeFilterBuilderRowValue;
diff --git a/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js
index 4004f0ced..4f6250151 100644
--- a/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js
+++ b/frontend/src/Components/Filter/Builder/PrivacyFilterBuilderRowValue.js
@@ -3,9 +3,24 @@ import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const privacyTypes = [
- { id: 'public', name: translate('Public') },
- { id: 'private', name: translate('Private') },
- { id: 'semiPrivate', name: translate('SemiPrivate') }
+ {
+ id: 'public',
+ get name() {
+ return translate('Public');
+ }
+ },
+ {
+ id: 'private',
+ get name() {
+ return translate('Private');
+ }
+ },
+ {
+ id: 'semiPrivate',
+ get name() {
+ return translate('SemiPrivate');
+ }
+ }
];
function PrivacyFilterBuilderRowValue(props) {
diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js
index 7407f729a..9f378d5a2 100644
--- a/frontend/src/Components/Filter/CustomFilters/CustomFilter.js
+++ b/frontend/src/Components/Filter/CustomFilters/CustomFilter.js
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
dispatchSetFilter
} = this.props;
- // Assume that delete and then unmounting means the delete was successful.
- // Moving this check to a ancestor would be more accurate, but would have
+ // Assume that delete and then unmounting means the deletion was successful.
+ // Moving this check to an ancestor would be more accurate, but would have
// more boilerplate.
if (this.state.isDeleting && id === selectedFilterKey) {
dispatchSetFilter({ selectedFilterKey: 'all' });
diff --git a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js
index 07660426e..99cb6ec5c 100644
--- a/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js
+++ b/frontend/src/Components/Filter/CustomFilters/CustomFiltersModalContent.js
@@ -5,6 +5,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
+import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import CustomFilter from './CustomFilter';
import styles from './CustomFiltersModalContent.css';
@@ -30,22 +31,24 @@ function CustomFiltersModalContent(props) {
{
- customFilters.map((customFilter) => {
- return (
-
- );
- })
+ customFilters
+ .sort((a, b) => sortByProp(a, b, 'label'))
+ .map((customFilter) => {
+ return (
+
+ );
+ })
}
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 bd0e9184d..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,
@@ -270,6 +267,7 @@ FormInputGroup.propTypes = {
helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
+ autoFocus: PropTypes.bool,
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,
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/FormLabel.css b/frontend/src/Components/Form/FormLabel.css
index 074b6091d..54a4678e8 100644
--- a/frontend/src/Components/Form/FormLabel.css
+++ b/frontend/src/Components/Form/FormLabel.css
@@ -2,8 +2,10 @@
display: flex;
justify-content: flex-end;
margin-right: $formLabelRightMarginWidth;
+ padding-top: 8px;
+ min-height: 35px;
+ text-align: end;
font-weight: bold;
- line-height: 35px;
}
.hasError {
diff --git a/frontend/src/Components/Form/HintedSelectInputOption.js b/frontend/src/Components/Form/HintedSelectInputOption.js
index 4957ece2a..4f59fc0a4 100644
--- a/frontend/src/Components/Form/HintedSelectInputOption.js
+++ b/frontend/src/Components/Form/HintedSelectInputOption.js
@@ -33,11 +33,11 @@ function HintedSelectInputOption(props) {
isMobile && styles.isMobile
)}
>
- {value}
+ {typeof value === 'function' ? value() : value}
{
hint != null &&
-
+
{hint}
}
@@ -48,7 +48,7 @@ function HintedSelectInputOption(props) {
HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
- value: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
hint: PropTypes.node,
depth: PropTypes.number,
isSelected: PropTypes.bool.isRequired,
diff --git a/frontend/src/Components/Form/HintedSelectInputSelectedValue.js b/frontend/src/Components/Form/HintedSelectInputSelectedValue.js
index 07f6c9e25..a3fecf324 100644
--- a/frontend/src/Components/Form/HintedSelectInputSelectedValue.js
+++ b/frontend/src/Components/Form/HintedSelectInputSelectedValue.js
@@ -24,7 +24,7 @@ function HintedSelectInputSelectedValue(props) {
>
{
- isMultiSelect &&
+ isMultiSelect ?
value.map((key, index) => {
const v = valuesMap[key];
return (
@@ -32,26 +32,28 @@ function HintedSelectInputSelectedValue(props) {
{v ? v.value : key}
);
- })
+ }) :
+ null
}
{
- !isMultiSelect && value
+ isMultiSelect ? null : value
}
{
- hint != null && includeHint &&
+ hint != null && includeHint ?
{hint}
-
+
:
+ null
}
);
}
HintedSelectInputSelectedValue.propTypes = {
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
hint: PropTypes.string,
isMultiSelect: PropTypes.bool.isRequired,
diff --git a/frontend/src/Components/Form/IndexersSelectInputConnector.js b/frontend/src/Components/Form/IndexersSelectInputConnector.js
index e7cca1feb..fade1e758 100644
--- a/frontend/src/Components/Form/IndexersSelectInputConnector.js
+++ b/frontend/src/Components/Form/IndexersSelectInputConnector.js
@@ -1,18 +1,20 @@
-import _ from 'lodash';
+import { groupBy, map } from 'lodash';
import PropTypes from 'prop-types';
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 titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
- (state) => state.indexers,
+ createSortedSectionSelector('indexers', sortByProp('name')),
(value, indexers) => {
const values = [];
- const groupedIndexers = _.map(_.groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
+ const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
groupedIndexers.forEach((element) => {
values.push({
@@ -25,6 +27,7 @@ function createMapStateToProps() {
values.push({
key: indexer.id,
value: indexer.name,
+ hint: `(${indexer.id})`,
isDisabled: !indexer.enable,
parentKey: element.protocol === 'usenet' ? -1 : -2
});
@@ -50,7 +53,6 @@ class IndexersSelectInputConnector extends Component {
// Render
render() {
-
return (
+
+
+
);
}
}
diff --git a/frontend/src/Components/Form/NumberInput.js b/frontend/src/Components/Form/NumberInput.js
index 454aad997..cac274d95 100644
--- a/frontend/src/Components/Form/NumberInput.js
+++ b/frontend/src/Components/Form/NumberInput.js
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) {
const { value } = this.props;
- if (value !== prevProps.value && !this.state.isFocused) {
+ if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({
value: value == null ? '' : value.toString()
});
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/PathInputConnector.js b/frontend/src/Components/Form/PathInputConnector.js
index 3917a8d3f..563437f9a 100644
--- a/frontend/src/Components/Form/PathInputConnector.js
+++ b/frontend/src/Components/Form/PathInputConnector.js
@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
}
PathInputConnector.propTypes = {
+ ...PathInput.props,
includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired
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/Form/SelectInput.js b/frontend/src/Components/Form/SelectInput.js
index 0a60ffe1e..553501afc 100644
--- a/frontend/src/Components/Form/SelectInput.js
+++ b/frontend/src/Components/Form/SelectInput.js
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key}
{...otherOptionProps}
>
- {optionValue}
+ {typeof optionValue === 'function' ? optionValue() : optionValue}
);
})
@@ -75,7 +75,7 @@ SelectInput.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.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,
diff --git a/frontend/src/Components/Icon.js b/frontend/src/Components/Icon.js
index e8c7c5178..d200b8c08 100644
--- a/frontend/src/Components/Icon.js
+++ b/frontend/src/Components/Icon.js
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return (
{icon}
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
- title: PropTypes.string,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};
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.js b/frontend/src/Components/Link/Link.js
deleted file mode 100644
index 6b5baca4e..000000000
--- a/frontend/src/Components/Link/Link.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { Link as RouterLink } from 'react-router-dom';
-import styles from './Link.css';
-
-class Link extends Component {
-
- //
- // Listeners
-
- onClick = (event) => {
- const {
- isDisabled,
- onPress
- } = this.props;
-
- if (!isDisabled && onPress) {
- onPress(event);
- }
- };
-
- //
- // Render
-
- render() {
- const {
- className,
- component,
- to,
- target,
- isDisabled,
- noRouter,
- onPress,
- ...otherProps
- } = this.props;
-
- const linkProps = { 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 {
- el = RouterLink;
- linkProps.to = `${window.Prowlarr.urlBase}/${to.replace(/^\//, '')}`;
- linkProps.target = target;
- }
- }
-
- if (el === 'button' || el === 'input') {
- linkProps.type = otherProps.type || 'button';
- linkProps.disabled = isDisabled;
- }
-
- linkProps.className = classNames(
- className,
- styles.link,
- to && styles.to,
- isDisabled && 'isDisabled'
- );
-
- const props = {
- ...otherProps,
- ...linkProps
- };
-
- props.onClick = this.onClick;
-
- return (
- React.createElement(el, props)
- );
- }
-}
-
-Link.propTypes = {
- className: PropTypes.string,
- component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
- to: PropTypes.string,
- target: PropTypes.string,
- isDisabled: PropTypes.bool,
- noRouter: PropTypes.bool,
- onPress: PropTypes.func
-};
-
-Link.defaultProps = {
- component: 'button',
- noRouter: false
-};
-
-export default Link;
diff --git a/frontend/src/Components/Link/Link.tsx b/frontend/src/Components/Link/Link.tsx
new file mode 100644
index 000000000..6f1fd1ff7
--- /dev/null
+++ b/frontend/src/Components/Link/Link.tsx
@@ -0,0 +1,93 @@
+import classNames from 'classnames';
+import React, {
+ ComponentPropsWithoutRef,
+ ElementType,
+ 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;
+ };
+
+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) {
+ return;
+ }
+
+ onPress?.(event);
+ },
+ [isDisabled, onPress]
+ );
+
+ const linkClass = classNames(
+ className,
+ styles.link,
+ to && styles.to,
+ isDisabled && 'isDisabled'
+ );
+
+ if (to) {
+ const toLink = /\w+?:\/\//.test(to);
+
+ if (toLink || noRouter) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/src/Components/Link/SpinnerErrorButton.js b/frontend/src/Components/Link/SpinnerErrorButton.js
index 81d34f7c2..b0f39bc26 100644
--- a/frontend/src/Components/Link/SpinnerErrorButton.js
+++ b/frontend/src/Components/Link/SpinnerErrorButton.js
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() {
const {
+ kind,
isSpinning,
error,
children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK;
- let iconKind = kinds.SUCCESS;
+ let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) {
iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return (
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
}
SpinnerErrorButton.propTypes = {
+ kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object,
children: PropTypes.node.isRequired
diff --git a/frontend/src/Components/Markdown/InlineMarkdown.js b/frontend/src/Components/Markdown/InlineMarkdown.js
index dc9ea9bf3..993bb241e 100644
--- a/frontend/src/Components/Markdown/InlineMarkdown.js
+++ b/frontend/src/Components/Markdown/InlineMarkdown.js
@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
render() {
const {
className,
- data
+ data,
+ blockClassName
} = this.props;
- // For now only replace links
+ // For now only replace links or code blocks (not both)
const markdownBlocks = [];
if (data) {
- const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
+ const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
- while ((match = regex.exec(data)) !== null) {
+
+ while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
+
markdownBlocks.push({match[1]});
endIndex = match.index + match[0].length;
}
- if (endIndex !== data.length) {
+ if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
+
+ const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
+
+ endIndex = 0;
+ match = null;
+ let matchedCode = false;
+
+ while ((match = codeRegex.exec(data)) !== null) {
+ matchedCode = true;
+
+ if (match.index > endIndex) {
+ markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
+ }
+
+ markdownBlocks.push({match[0].substring(1, match[0].length - 1)}
);
+ endIndex = match.index + match[0].length;
+ }
+
+ if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
+ markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
+ }
+
+ if (markdownBlocks.length === 0) {
+ markdownBlocks.push(data);
+ }
}
return {markdownBlocks};
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = {
className: PropTypes.string,
- data: PropTypes.string
+ data: PropTypes.string,
+ blockClassName: PropTypes.string
};
export default InlineMarkdown;
diff --git a/frontend/src/Components/Menu/FilterMenuContent.js b/frontend/src/Components/Menu/FilterMenuContent.js
index 1fdb2476f..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';
@@ -33,25 +34,33 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
- {filter.label}
+ {typeof filter.label === 'function' ? filter.label() : filter.label}
);
})
}
{
- 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/Modal/ModalContent.js b/frontend/src/Components/Modal/ModalContent.js
index 8883bf2b9..1d3862a13 100644
--- a/frontend/src/Components/Modal/ModalContent.js
+++ b/frontend/src/Components/Modal/ModalContent.js
@@ -3,6 +3,7 @@ import React from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
+import translate from 'Utilities/String/translate';
import styles from './ModalContent.css';
function ModalContent(props) {
@@ -28,6 +29,7 @@ function ModalContent(props) {
}
diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js
index 77bed6a31..a2b0fca32 100644
--- a/frontend/src/Components/Page/ErrorPage.js
+++ b/frontend/src/Components/Page/ErrorPage.js
@@ -7,6 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
+ translationsError,
indexersError,
indexerStatusError,
indexerCategoriesError,
@@ -21,6 +22,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
+ } else if (translationsError) {
+ errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (indexersError) {
errorMessage = getErrorMessage(indexersError, 'Failed to load indexers from API');
} else if (indexerStatusError) {
@@ -55,6 +58,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
+ translationsError: PropTypes.object,
indexersError: PropTypes.object,
indexerStatusError: PropTypes.object,
indexerCategoriesError: PropTypes.object,
diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js
index 1d8b0cc55..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 {
@@ -78,6 +78,7 @@ class PageHeader extends Component {
aria-label="Donate"
to="https://prowlarr.com/donate"
size={14}
+ title={translate('Donate')}
/>
-
diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
deleted file mode 100644
index 87fee6b0d..000000000
--- a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js
+++ /dev/null
@@ -1,89 +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 (
-
-
-
- );
-}
-
-PageHeaderActionsMenu.propTypes = {
- formsAuth: PropTypes.bool.isRequired,
- onKeyboardShortcutsPress: PropTypes.func.isRequired,
- onRestartPress: PropTypes.func.isRequired,
- onShutdownPress: PropTypes.func.isRequired
-};
-
-export default PageHeaderActionsMenu;
diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx
new file mode 100644
index 000000000..6b7da03eb
--- /dev/null
+++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.tsx
@@ -0,0 +1,90 @@
+import React, { useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import AppState from 'App/State/AppState';
+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 { restart, shutdown } from 'Store/Actions/systemActions';
+import translate from 'Utilities/String/translate';
+import styles from './PageHeaderActionsMenu.css';
+
+interface PageHeaderActionsMenuProps {
+ onKeyboardShortcutsPress(): void;
+}
+
+function PageHeaderActionsMenu(props: PageHeaderActionsMenuProps) {
+ const { onKeyboardShortcutsPress } = props;
+
+ const dispatch = useDispatch();
+
+ const { authentication, isDocker } = useSelector(
+ (state: AppState) => state.system.status.item
+ );
+
+ const formsAuth = authentication === 'forms';
+
+ const handleRestartPress = useCallback(() => {
+ dispatch(restart());
+ }, [dispatch]);
+
+ const handleShutdownPress = useCallback(() => {
+ dispatch(shutdown());
+ }, [dispatch]);
+
+ return (
+
+
+
+ );
+}
+
+export default PageHeaderActionsMenu;
diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js
deleted file mode 100644
index 3aba95065..000000000
--- a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { restart, shutdown } from 'Store/Actions/systemActions';
-import PageHeaderActionsMenu from './PageHeaderActionsMenu';
-
-function createMapStateToProps() {
- return createSelector(
- (state) => state.system.status,
- (status) => {
- return {
- formsAuth: status.item.authentication === 'forms'
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- restart,
- shutdown
-};
-
-class PageHeaderActionsMenuConnector extends Component {
-
- //
- // Listeners
-
- onRestartPress = () => {
- this.props.restart();
- };
-
- onShutdownPress = () => {
- this.props.shutdown();
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-PageHeaderActionsMenuConnector.propTypes = {
- restart: PropTypes.func.isRequired,
- shutdown: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector);
diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js
index aa23f4d88..c2e368827 100644
--- a/frontend/src/Components/Page/Page.js
+++ b/frontend/src/Components/Page/Page.js
@@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
+import AppUpdatedModal from 'App/AppUpdatedModal';
import ColorImpairedContext from 'App/ColorImpairedContext';
-import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
+import ConnectionLostModal from 'App/ConnectionLostModal';
import SignalRConnector from 'Components/SignalRConnector';
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
import locationShape from 'Helpers/Props/Shapes/locationShape';
@@ -102,12 +102,12 @@ class Page extends Component {
{children}
-
-
diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js
index 5ac032c0f..5c1f6f42e 100644
--- a/frontend/src/Components/Page/PageConnector.js
+++ b/frontend/src/Components/Page/PageConnector.js
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
-import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
+import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
@@ -54,6 +54,7 @@ const selectIsPopulated = createSelector(
(state) => state.indexerStatus.isPopulated,
(state) => state.settings.indexerCategories.isPopulated,
(state) => state.system.status.isPopulated,
+ (state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@@ -63,7 +64,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated,
indexerStatusIsPopulated,
indexerCategoriesIsPopulated,
- systemStatusIsPopulated
+ systemStatusIsPopulated,
+ translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@@ -74,7 +76,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated &&
indexerStatusIsPopulated &&
indexerCategoriesIsPopulated &&
- systemStatusIsPopulated
+ systemStatusIsPopulated &&
+ translationsIsPopulated
);
}
);
@@ -89,6 +92,7 @@ const selectErrors = createSelector(
(state) => state.indexerStatus.error,
(state) => state.settings.indexerCategories.error,
(state) => state.system.status.error,
+ (state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@@ -98,7 +102,8 @@ const selectErrors = createSelector(
indexersError,
indexerStatusError,
indexerCategoriesError,
- systemStatusError
+ systemStatusError,
+ translationsError
) => {
const hasError = !!(
customFiltersError ||
@@ -109,7 +114,8 @@ const selectErrors = createSelector(
indexersError ||
indexerStatusError ||
indexerCategoriesError ||
- systemStatusError
+ systemStatusError ||
+ translationsError
);
return {
@@ -122,7 +128,8 @@ const selectErrors = createSelector(
indexersError,
indexerStatusError,
indexerCategoriesError,
- systemStatusError
+ systemStatusError,
+ translationsError
};
}
);
@@ -184,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
+ dispatchFetchTranslations() {
+ dispatch(fetchTranslations());
+ },
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@@ -217,6 +227,7 @@ class PageConnector extends Component {
this.props.dispatchFetchUISettings();
this.props.dispatchFetchGeneralSettings();
this.props.dispatchFetchStatus();
+ this.props.dispatchFetchTranslations();
}
}
@@ -242,6 +253,7 @@ class PageConnector extends Component {
dispatchFetchUISettings,
dispatchFetchGeneralSettings,
dispatchFetchStatus,
+ dispatchFetchTranslations,
...otherProps
} = this.props;
@@ -282,6 +294,7 @@ PageConnector.propTypes = {
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
+ dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};
diff --git a/frontend/src/Components/Page/PageContentBody.tsx b/frontend/src/Components/Page/PageContentBody.tsx
index 75317f113..ce9b0e7e4 100644
--- a/frontend/src/Components/Page/PageContentBody.tsx
+++ b/frontend/src/Components/Page/PageContentBody.tsx
@@ -1,22 +1,19 @@
-import React, { forwardRef, ReactNode, useCallback } from 'react';
-import Scroller from 'Components/Scroller/Scroller';
+import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
+import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
interface PageContentBodyProps {
- className: string;
- innerClassName: string;
+ className?: string;
+ innerClassName?: string;
children: ReactNode;
initialScrollTop?: number;
- onScroll?: (payload) => void;
+ onScroll?: (payload: OnScroll) => void;
}
const PageContentBody = forwardRef(
- (
- props: PageContentBodyProps,
- ref: React.MutableRefObject
- ) => {
+ (props: PageContentBodyProps, ref: ForwardedRef) => {
const {
className = styles.contentBody,
innerClassName = styles.innerContentBody,
@@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
} = props;
const onScrollWrapper = useCallback(
- (payload) => {
+ (payload: OnScroll) => {
if (onScroll && !isLocked()) {
onScroll(payload);
}
diff --git a/frontend/src/Components/Page/PageJumpBar.css b/frontend/src/Components/Page/PageJumpBar.css
index 9a116fb54..f5ae7a729 100644
--- a/frontend/src/Components/Page/PageJumpBar.css
+++ b/frontend/src/Components/Page/PageJumpBar.css
@@ -1,4 +1,5 @@
.jumpBar {
+ z-index: $pageJumpBarZIndex;
display: flex;
align-content: stretch;
align-items: stretch;
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js
index 045789075..6eef54eab 100644
--- a/frontend/src/Components/Page/Sidebar/PageSidebar.js
+++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js
@@ -8,7 +8,7 @@ import Scroller from 'Components/Scroller/Scroller';
import { icons } from 'Helpers/Props';
import locationShape from 'Helpers/Props/Shapes/locationShape';
import dimensions from 'Styles/Variables/dimensions';
-import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector';
+import HealthStatus from 'System/Status/Health/HealthStatus';
import translate from 'Utilities/String/translate';
import MessagesConnector from './Messages/MessagesConnector';
import PageSidebarItem from './PageSidebarItem';
@@ -20,12 +20,12 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
- title: translate('Indexers'),
+ title: () => translate('Indexers'),
to: '/',
alias: '/indexers',
children: [
{
- title: translate('Stats'),
+ title: () => translate('Stats'),
to: '/indexers/stats'
}
]
@@ -33,47 +33,47 @@ const links = [
{
iconName: icons.SEARCH,
- title: translate('Search'),
+ title: () => translate('Search'),
to: '/search'
},
{
iconName: icons.ACTIVITY,
- title: translate('History'),
+ title: () => translate('History'),
to: '/history'
},
{
iconName: icons.SETTINGS,
- title: translate('Settings'),
+ title: () => translate('Settings'),
to: '/settings',
children: [
{
- title: translate('Indexers'),
+ title: () => translate('Indexers'),
to: '/settings/indexers'
},
{
- title: translate('Apps'),
+ title: () => translate('Apps'),
to: '/settings/applications'
},
{
- title: translate('DownloadClients'),
+ title: () => translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
- title: translate('Connect'),
+ title: () => translate('Connect'),
to: '/settings/connect'
},
{
- title: translate('Tags'),
+ title: () => translate('Tags'),
to: '/settings/tags'
},
{
- title: translate('General'),
+ title: () => translate('General'),
to: '/settings/general'
},
{
- title: translate('UI'),
+ title: () => translate('UI'),
to: '/settings/ui'
}
]
@@ -81,32 +81,32 @@ const links = [
{
iconName: icons.SYSTEM,
- title: translate('System'),
+ title: () => translate('System'),
to: '/system/status',
children: [
{
- title: translate('Status'),
+ title: () => translate('Status'),
to: '/system/status',
- statusComponent: HealthStatusConnector
+ statusComponent: HealthStatus
},
{
- title: translate('Tasks'),
+ title: () => translate('Tasks'),
to: '/system/tasks'
},
{
- title: translate('Backup'),
+ title: () => translate('Backup'),
to: '/system/backup'
},
{
- title: translate('Updates'),
+ title: () => translate('Updates'),
to: '/system/updates'
},
{
- title: translate('Events'),
+ title: () => translate('Events'),
to: '/system/events'
},
{
- title: translate('LogFiles'),
+ title: () => translate('LogFiles'),
to: '/system/logs/files'
}
]
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css
index 5e3e3b52c..409062f97 100644
--- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css
+++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css
@@ -24,6 +24,7 @@
composes: link;
padding: 10px 24px;
+ padding-left: 35px;
}
.isActiveLink {
@@ -41,10 +42,6 @@
text-align: center;
}
-.noIcon {
- margin-left: 25px;
-}
-
.status {
float: right;
}
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts
index 77e23c767..5bf0eb815 100644
--- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts
+++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css.d.ts
@@ -8,7 +8,6 @@ interface CssExports {
'isActiveParentLink': string;
'item': string;
'link': string;
- 'noIcon': string;
'status': string;
}
export const cssExports: CssExports;
diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js
index 9ad78db6b..8d0e4e790 100644
--- a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js
+++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js
@@ -63,9 +63,7 @@ class PageSidebarItem extends Component {
}
-
- {title}
-
+ {typeof title === 'function' ? title() : title}
{
!!StatusComponent &&
@@ -88,7 +86,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = {
iconName: PropTypes.object,
- title: PropTypes.string.isRequired,
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired,
isActive: PropTypes.bool,
isActiveParent: PropTypes.bool,
diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css
index 0b6918296..e9a1b666d 100644
--- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css
+++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css
@@ -22,11 +22,14 @@
display: flex;
align-items: center;
justify-content: center;
+ overflow: hidden;
height: 24px;
}
.label {
padding: 0 3px;
+ max-width: 100%;
+ max-height: 100%;
color: var(--toolbarLabelColor);
font-size: $extraSmallFontSize;
line-height: calc($extraSmallFontSize + 1px);
diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js
index c93603aa9..675bdfd02 100644
--- a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js
+++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js
@@ -23,6 +23,7 @@ function PageToolbarButton(props) {
isDisabled && styles.isDisabled
)}
isDisabled={isDisabled || isSpinning}
+ title={label}
{...otherProps}
>
void;
+ onScroll?: (payload: OnScroll) => void;
}
const Scroller = forwardRef(
- (props: ScrollerProps, ref: React.MutableRefObject) => {
+ (props: ScrollerProps, ref: ForwardedRef) => {
const {
className,
autoFocus = false,
@@ -30,7 +42,7 @@ const Scroller = forwardRef(
} = props;
const internalRef = useRef();
- const currentRef = ref ?? internalRef;
+ const currentRef = (ref as MutableRefObject) ?? internalRef;
useEffect(
() => {
diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js
index 28c12df12..d39c05e10 100644
--- a/frontend/src/Components/SignalRConnector.js
+++ b/frontend/src/Components/SignalRConnector.js
@@ -141,6 +141,16 @@ class SignalRConnector extends Component {
console.error(`signalR: Unable to find handler for ${name}`);
};
+ handleApplications = ({ action, resource }) => {
+ const section = 'settings.applications';
+
+ if (action === 'created' || action === 'updated') {
+ this.props.dispatchUpdateItem({ section, ...resource });
+ } else if (action === 'deleted') {
+ this.props.dispatchRemoveItem({ section, id: resource.id });
+ }
+ };
+
handleCommand = (body) => {
if (body.action === 'sync') {
this.props.dispatchFetchCommands();
@@ -150,8 +160,8 @@ class SignalRConnector extends Component {
const resource = body.resource;
const status = resource.status;
- // Both sucessful and failed commands need to be
- // completed, otherwise they spin until they timeout.
+ // Both successful and failed commands need to be
+ // completed, otherwise they spin until they time out.
if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource);
@@ -160,6 +170,16 @@ class SignalRConnector extends Component {
}
};
+ handleDownloadclient = ({ action, resource }) => {
+ const section = 'settings.downloadClients';
+
+ if (action === 'created' || action === 'updated') {
+ this.props.dispatchUpdateItem({ section, ...resource });
+ } else if (action === 'deleted') {
+ this.props.dispatchRemoveItem({ section, id: resource.id });
+ }
+ };
+
handleHealth = () => {
this.props.dispatchFetchHealth();
};
@@ -168,14 +188,33 @@ class SignalRConnector extends Component {
this.props.dispatchFetchIndexerStatus();
};
- handleIndexer = (body) => {
- const action = body.action;
+ handleIndexer = ({ action, resource }) => {
const section = 'indexers';
- if (action === 'updated') {
- this.props.dispatchUpdateItem({ section, ...body.resource });
+ if (action === 'created' || action === 'updated') {
+ this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
- this.props.dispatchRemoveItem({ section, id: body.resource.id });
+ this.props.dispatchRemoveItem({ section, id: resource.id });
+ }
+ };
+
+ handleIndexerproxy = ({ action, resource }) => {
+ const section = 'settings.indexerProxies';
+
+ if (action === 'created' || action === 'updated') {
+ this.props.dispatchUpdateItem({ section, ...resource });
+ } else if (action === 'deleted') {
+ this.props.dispatchRemoveItem({ section, id: resource.id });
+ }
+ };
+
+ handleNotification = ({ action, resource }) => {
+ const section = 'settings.notifications';
+
+ if (action === 'created' || action === 'updated') {
+ this.props.dispatchUpdateItem({ section, ...resource });
+ } else if (action === 'deleted') {
+ this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js
index 207b97752..4bf94cf57 100644
--- a/frontend/src/Components/Table/Cells/RelativeDateCell.js
+++ b/frontend/src/Components/Table/Cells/RelativeDateCell.js
@@ -1,58 +1,66 @@
import PropTypes from 'prop-types';
-import React, { PureComponent } from 'react';
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import TableRowCell from './TableRowCell';
import styles from './RelativeDateCell.css';
-class RelativeDateCell extends PureComponent {
+function createRelativeDateCellSelector() {
+ return createSelector(createUISettingsSelector(), (uiSettings) => {
+ return {
+ showRelativeDates: uiSettings.showRelativeDates,
+ shortDateFormat: uiSettings.shortDateFormat,
+ longDateFormat: uiSettings.longDateFormat,
+ timeFormat: uiSettings.timeFormat
+ };
+ });
+}
+function RelativeDateCell(props) {
//
// Render
- render() {
- const {
- className,
- date,
- includeSeconds,
- showRelativeDates,
- shortDateFormat,
- longDateFormat,
- timeFormat,
- component: Component,
- dispatch,
- ...otherProps
- } = this.props;
+ const {
+ className,
+ date,
+ includeSeconds,
+ component: Component,
+ dispatch,
+ ...otherProps
+ } = props;
- if (!date) {
- return (
-
- );
- }
+ const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
+ useSelector(createRelativeDateCellSelector());
- return (
-
- {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
-
- );
+ if (!date) {
+ return ;
}
+
+ return (
+
+ {getRelativeDate(date, shortDateFormat, showRelativeDates, {
+ timeFormat,
+ includeSeconds,
+ timeForToday: true
+ })}
+
+ );
}
RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired,
date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired,
- showRelativeDates: PropTypes.bool.isRequired,
- shortDateFormat: PropTypes.string.isRequired,
- longDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
component: PropTypes.elementType,
dispatch: PropTypes.func
};
diff --git a/frontend/src/Components/Form/PasswordInput.css.d.ts b/frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
similarity index 89%
rename from frontend/src/Components/Form/PasswordInput.css.d.ts
rename to frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
index 774807ef4..c748f6f97 100644
--- a/frontend/src/Components/Form/PasswordInput.css.d.ts
+++ b/frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
@@ -1,7 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
- 'input': string;
+ 'cell': string;
}
export const cssExports: CssExports;
export default cssExports;
diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.js b/frontend/src/Components/Table/Cells/TableRowCellButton.js
deleted file mode 100644
index ff50d3bc9..000000000
--- a/frontend/src/Components/Table/Cells/TableRowCellButton.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Link from 'Components/Link/Link';
-import TableRowCell from './TableRowCell';
-import styles from './TableRowCellButton.css';
-
-function TableRowCellButton({ className, ...otherProps }) {
- return (
-
- );
-}
-
-TableRowCellButton.propTypes = {
- className: PropTypes.string.isRequired
-};
-
-TableRowCellButton.defaultProps = {
- className: styles.cell
-};
-
-export default TableRowCellButton;
diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.tsx b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx
new file mode 100644
index 000000000..c80a3d626
--- /dev/null
+++ b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx
@@ -0,0 +1,19 @@
+import React, { ReactNode } from 'react';
+import Link, { LinkProps } from 'Components/Link/Link';
+import TableRowCell from './TableRowCell';
+import styles from './TableRowCellButton.css';
+
+interface TableRowCellButtonProps extends LinkProps {
+ className?: string;
+ children: ReactNode;
+}
+
+function TableRowCellButton(props: TableRowCellButtonProps) {
+ const { className = styles.cell, ...otherProps } = props;
+
+ return (
+
+ );
+}
+
+export default TableRowCellButton;
diff --git a/frontend/src/Components/Table/Column.ts b/frontend/src/Components/Table/Column.ts
index 8c2122c65..24674c3fc 100644
--- a/frontend/src/Components/Table/Column.ts
+++ b/frontend/src/Components/Table/Column.ts
@@ -1,8 +1,12 @@
import React from 'react';
+type PropertyFunction = () => T;
+
+// TODO: Convert to generic so `name` can be a type
interface Column {
name: string;
- label: string | React.ReactNode;
+ label: string | PropertyFunction | React.ReactNode;
+ className?: string;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;
diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js
index befc8219a..8afbf9ea0 100644
--- a/frontend/src/Components/Table/Table.js
+++ b/frontend/src/Components/Table/Table.js
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)}
{...column}
>
- {column.label}
+ {typeof column.label === 'function' ? column.label() : column.label}
);
})
diff --git a/frontend/src/Components/Table/TableHeaderCell.js b/frontend/src/Components/Table/TableHeaderCell.js
index 21766978b..b0ed5c571 100644
--- a/frontend/src/Components/Table/TableHeaderCell.js
+++ b/frontend/src/Components/Table/TableHeaderCell.js
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const {
className,
name,
+ label,
columnLabel,
isSortable,
isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps}
component="th"
className={className}
- title={columnLabel}
+ label={typeof label === 'function' ? label() : label}
+ title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress}
>
{children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = {
className: PropTypes.string,
name: PropTypes.string.isRequired,
- columnLabel: PropTypes.string,
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
+ columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool,
isVisible: PropTypes.bool,
isModifiable: PropTypes.bool,
diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js
index 2d91c7c63..402ef5ae1 100644
--- a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js
+++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
- {label}
+ {typeof label === 'function' ? label() : label}
{
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
- label: PropTypes.string.isRequired,
+ label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,
diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js
index 100559660..77d18463f 100644
--- a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js
+++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
void;
+}
+
+function usePaging(options: PagingOptions) {
+ const { page, totalPages, gotoPage } = options;
+ const dispatch = useDispatch();
+
+ const handleFirstPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: 1 }));
+ }, [dispatch, gotoPage]);
+
+ const handlePreviousPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: Math.max(page - 1, 1) }));
+ }, [page, dispatch, gotoPage]);
+
+ const handleNextPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: Math.min(page + 1, totalPages) }));
+ }, [page, totalPages, dispatch, gotoPage]);
+
+ const handleLastPagePress = useCallback(() => {
+ dispatch(gotoPage({ page: totalPages }));
+ }, [totalPages, dispatch, gotoPage]);
+
+ const handlePageSelect = useCallback(
+ (page: number) => {
+ dispatch(gotoPage({ page }));
+ },
+ [dispatch, gotoPage]
+ );
+
+ return useMemo(() => {
+ return {
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ };
+ }, [
+ handleFirstPagePress,
+ handlePreviousPagePress,
+ handleNextPagePress,
+ handleLastPagePress,
+ handlePageSelect,
+ ]);
+}
+
+export default usePaging;
diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js
index f4d4e2af4..fe700b8fe 100644
--- a/frontend/src/Components/TagList.js
+++ b/frontend/src/Components/TagList.js
@@ -1,14 +1,15 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
+import sortByProp from 'Utilities/Array/sortByProp';
import Label from './Label';
import styles from './TagList.css';
function TagList({ tags, tagList }) {
const sortedTags = tags
.map((tagId) => tagList.find((tag) => tag.id === tagId))
- .filter((t) => t !== undefined)
- .sort((a, b) => a.label.localeCompare(b.label));
+ .filter((tag) => !!tag)
+ .sort(sortByProp('label'));
return (
diff --git a/frontend/src/Components/keyboardShortcuts.js b/frontend/src/Components/keyboardShortcuts.js
index b576a988c..8513a65eb 100644
--- a/frontend/src/Components/keyboardShortcuts.js
+++ b/frontend/src/Components/keyboardShortcuts.js
@@ -6,37 +6,51 @@ import translate from 'Utilities/String/translate';
export const shortcuts = {
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
key: '?',
- name: translate('OpenThisModal')
+ get name() {
+ return translate('OpenThisModal');
+ }
},
CLOSE_MODAL: {
key: 'Esc',
- name: translate('CloseCurrentModal')
+ get name() {
+ return translate('CloseCurrentModal');
+ }
},
ACCEPT_CONFIRM_MODAL: {
key: 'Enter',
- name: translate('AcceptConfirmationModal')
+ get name() {
+ return translate('AcceptConfirmationModal');
+ }
},
MOVIE_SEARCH_INPUT: {
key: 's',
- name: translate('FocusSearchBox')
+ get name() {
+ return translate('FocusSearchBox');
+ }
},
SAVE_SETTINGS: {
key: 'mod+s',
- name: translate('SaveSettings')
+ get name() {
+ return translate('SaveSettings');
+ }
},
SCROLL_TOP: {
key: 'mod+home',
- name: translate('MovieIndexScrollTop')
+ get name() {
+ return translate('MovieIndexScrollTop');
+ }
},
SCROLL_BOTTOM: {
key: 'mod+end',
- name: translate('MovieIndexScrollBottom')
+ get name() {
+ return translate('MovieIndexScrollBottom');
+ }
}
};
diff --git a/frontend/src/Components/withScrollPosition.tsx b/frontend/src/Components/withScrollPosition.tsx
index ec13c6ab8..f688a6253 100644
--- a/frontend/src/Components/withScrollPosition.tsx
+++ b/frontend/src/Components/withScrollPosition.tsx
@@ -1,24 +1,30 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
import scrollPositions from 'Store/scrollPositions';
-function withScrollPosition(WrappedComponent, scrollPositionKey) {
- function ScrollPosition(props) {
+interface WrappedComponentProps {
+ initialScrollTop: number;
+}
+
+interface ScrollPositionProps {
+ history: RouteComponentProps['history'];
+ location: RouteComponentProps['location'];
+ match: RouteComponentProps['match'];
+}
+
+function withScrollPosition(
+ WrappedComponent: React.FC
,
+ scrollPositionKey: string
+) {
+ function ScrollPosition(props: ScrollPositionProps) {
const { history } = props;
const initialScrollTop =
- history.action === 'POP' ||
- (history.location.state && history.location.state.restoreScrollPosition)
- ? scrollPositions[scrollPositionKey]
- : 0;
+ history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
return ;
}
- ScrollPosition.propTypes = {
- history: PropTypes.object.isRequired,
- };
-
return ScrollPosition;
}
diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css
index bf31501dd..e0f1bf5dc 100644
--- a/frontend/src/Content/Fonts/fonts.css
+++ b/frontend/src/Content/Fonts/fonts.css
@@ -25,14 +25,3 @@
font-family: 'Ubuntu Mono';
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
}
-
-/*
- * text-security-disc
- */
-
-@font-face {
- font-weight: normal;
- font-style: normal;
- font-family: 'text-security-disc';
- src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
-}
diff --git a/frontend/src/Content/Fonts/text-security-disc.ttf b/frontend/src/Content/Fonts/text-security-disc.ttf
deleted file mode 100644
index 86038dba8..000000000
Binary files a/frontend/src/Content/Fonts/text-security-disc.ttf and /dev/null differ
diff --git a/frontend/src/Content/Fonts/text-security-disc.woff b/frontend/src/Content/Fonts/text-security-disc.woff
deleted file mode 100644
index bc4cc324b..000000000
Binary files a/frontend/src/Content/Fonts/text-security-disc.woff and /dev/null differ
diff --git a/frontend/src/DownloadClient/DownloadProtocol.ts b/frontend/src/DownloadClient/DownloadProtocol.ts
new file mode 100644
index 000000000..417db8178
--- /dev/null
+++ b/frontend/src/DownloadClient/DownloadProtocol.ts
@@ -0,0 +1,3 @@
+type DownloadProtocol = 'usenet' | 'torrent' | 'unknown';
+
+export default DownloadProtocol;
diff --git a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js
index 920c59a31..17a04e403 100644
--- a/frontend/src/FirstRun/AuthenticationRequiredModalContent.js
+++ b/frontend/src/FirstRun/AuthenticationRequiredModalContent.js
@@ -11,7 +11,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
-import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
+import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css';
@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
authenticationMethod,
authenticationRequired,
username,
- password
+ password,
+ passwordConfirmation
} = settings;
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
@@ -63,71 +64,75 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
- {authenticationRequiredWarning}
+ {translate('AuthenticationRequiredWarning')}
{
isPopulated && !error ?
- {translate('Authentication')}
+ {translate('AuthenticationMethod')}
- {
- authenticationEnabled ?
-
- {translate('AuthenticationRequired')}
+
+ {translate('AuthenticationRequired')}
-
- :
- null
- }
+
+
- {
- authenticationEnabled ?
-
- {translate('Username')}
+
+ {translate('Username')}
-
- :
- null
- }
+
+
- {
- authenticationEnabled ?
-
- {translate('Password')}
+
+ {translate('Password')}
-
- :
- null
- }
+
+
+
+
+ {translate('PasswordConfirmation')}
+
+
+
:
null
}
diff --git a/frontend/src/Helpers/Hooks/useCurrentPage.ts b/frontend/src/Helpers/Hooks/useCurrentPage.ts
new file mode 100644
index 000000000..3caf66df2
--- /dev/null
+++ b/frontend/src/Helpers/Hooks/useCurrentPage.ts
@@ -0,0 +1,9 @@
+import { useHistory } from 'react-router-dom';
+
+function useCurrentPage() {
+ const history = useHistory();
+
+ return history.action === 'POP';
+}
+
+export default useCurrentPage;
diff --git a/frontend/src/Helpers/Hooks/useModalOpenState.ts b/frontend/src/Helpers/Hooks/useModalOpenState.ts
new file mode 100644
index 000000000..24cffb2f1
--- /dev/null
+++ b/frontend/src/Helpers/Hooks/useModalOpenState.ts
@@ -0,0 +1,17 @@
+import { useCallback, useState } from 'react';
+
+export default function useModalOpenState(
+ initialState: boolean
+): [boolean, () => void, () => void] {
+ const [isOpen, setIsOpen] = useState(initialState);
+
+ const setModalOpen = useCallback(() => {
+ setIsOpen(true);
+ }, [setIsOpen]);
+
+ const setModalClosed = useCallback(() => {
+ setIsOpen(false);
+ }, [setIsOpen]);
+
+ return [isOpen, setModalOpen, setModalClosed];
+}
diff --git a/frontend/src/Helpers/Props/TooltipPosition.ts b/frontend/src/Helpers/Props/TooltipPosition.ts
new file mode 100644
index 000000000..885c73470
--- /dev/null
+++ b/frontend/src/Helpers/Props/TooltipPosition.ts
@@ -0,0 +1,3 @@
+type TooltipPosition = 'top' | 'right' | 'bottom' | 'left';
+
+export default TooltipPosition;
diff --git a/frontend/src/Helpers/Props/align.js b/frontend/src/Helpers/Props/align.ts
similarity index 100%
rename from frontend/src/Helpers/Props/align.js
rename to frontend/src/Helpers/Props/align.ts
diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js
index 7fed535f2..73ef41956 100644
--- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js
+++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js
@@ -2,9 +2,10 @@ export const BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date';
export const DEFAULT = 'default';
+export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const PROTOCOL = 'protocol';
export const PRIVACY = 'privacy';
export const APP_PROFILE = 'appProfile';
-export const MOVIE_STATUS = 'movieStatus';
+export const CATEGORY = 'category';
export const TAG = 'tag';
diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js
index 589add5a8..773748996 100644
--- a/frontend/src/Helpers/Props/icons.js
+++ b/frontend/src/Helpers/Props/icons.js
@@ -43,6 +43,7 @@ import {
faChevronCircleRight as fasChevronCircleRight,
faChevronCircleUp as fasChevronCircleUp,
faCircle as fasCircle,
+ faCircleDown as fasCircleDown,
faCloud as fasCloud,
faCloudDownloadAlt as fasCloudDownloadAlt,
faCog as fasCog,
@@ -75,6 +76,7 @@ import {
faListCheck as fasListCheck,
faLocationArrow as fasLocationArrow,
faLock as fasLock,
+ faMagnet as fasMagnet,
faMedkit as fasMedkit,
faMinus as fasMinus,
faMusic as fasMusic,
@@ -140,6 +142,7 @@ export const CHECK_INDETERMINATE = fasMinus;
export const CHECK_CIRCLE = fasCheckCircle;
export const CHECK_SQUARE = fasSquareCheck;
export const CIRCLE = fasCircle;
+export const CIRCLE_DOWN = fasCircleDown;
export const CIRCLE_OUTLINE = farCircle;
export const CLEAR = fasTrashAlt;
export const CLIPBOARD = fasCopy;
@@ -181,6 +184,7 @@ export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard;
export const LOCK = fasLock;
export const LOGOUT = fasSignOutAlt;
+export const MAGNET = fasMagnet;
export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice;
export const MISSING = fasExclamationTriangle;
diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js
index d26d08616..f9cd58e6d 100644
--- a/frontend/src/Helpers/Props/inputTypes.js
+++ b/frontend/src/Helpers/Props/inputTypes.js
@@ -1,6 +1,5 @@
export const AUTO_COMPLETE = 'autoComplete';
export const APP_PROFILE_SELECT = 'appProfileSelect';
-export const AVAILABILITY_SELECT = 'availabilitySelect';
export const CAPTCHA = 'captcha';
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
export const CHECK = 'check';
@@ -10,6 +9,7 @@ export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
+export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
@@ -26,7 +26,6 @@ export const TAG_SELECT = 'tagSelect';
export const all = [
AUTO_COMPLETE,
APP_PROFILE_SELECT,
- AVAILABILITY_SELECT,
CAPTCHA,
CARDIGANNCAPTCHA,
CHECK,
@@ -35,6 +34,7 @@ export const all = [
INFO,
MOVIE_MONITORED_SELECT,
CATEGORY_SELECT,
+ FLOAT,
NUMBER,
OAUTH,
PASSWORD,
diff --git a/frontend/src/Helpers/Props/kinds.js b/frontend/src/Helpers/Props/kinds.ts
similarity index 72%
rename from frontend/src/Helpers/Props/kinds.js
rename to frontend/src/Helpers/Props/kinds.ts
index b0f5ac87f..7ce606716 100644
--- a/frontend/src/Helpers/Props/kinds.js
+++ b/frontend/src/Helpers/Props/kinds.ts
@@ -7,7 +7,6 @@ export const PRIMARY = 'primary';
export const PURPLE = 'purple';
export const SUCCESS = 'success';
export const WARNING = 'warning';
-export const QUEUE = 'queue';
export const all = [
DANGER,
@@ -19,5 +18,15 @@ export const all = [
PURPLE,
SUCCESS,
WARNING,
- QUEUE
-];
+] as const;
+
+export type Kind =
+ | 'danger'
+ | 'default'
+ | 'disabled'
+ | 'info'
+ | 'inverse'
+ | 'primary'
+ | 'purple'
+ | 'success'
+ | 'warning';
diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.ts
similarity index 71%
rename from frontend/src/Helpers/Props/sizes.js
rename to frontend/src/Helpers/Props/sizes.ts
index d7f85df5e..ca7a50fbf 100644
--- a/frontend/src/Helpers/Props/sizes.js
+++ b/frontend/src/Helpers/Props/sizes.ts
@@ -4,4 +4,6 @@ export const MEDIUM = 'medium';
export const LARGE = 'large';
export const EXTRA_LARGE = 'extraLarge';
-export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
+export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE] as const;
+
+export type Size = 'extraSmall' | 'small' | 'medium' | 'large' | 'extraLarge';
diff --git a/frontend/src/History/Details/HistoryDetails.js b/frontend/src/History/Details/HistoryDetails.js
index e0ae06eb1..6d5ab260e 100644
--- a/frontend/src/History/Details/HistoryDetails.js
+++ b/frontend/src/History/Details/HistoryDetails.js
@@ -3,6 +3,7 @@ import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Link from 'Components/Link/Link';
+import formatDateTime from 'Utilities/Date/formatDateTime';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
@@ -10,7 +11,10 @@ function HistoryDetails(props) {
const {
indexer,
eventType,
- data
+ date,
+ data,
+ shortDateFormat,
+ timeFormat
} = props;
if (eventType === 'indexerQuery' || eventType === 'indexerRss') {
@@ -21,7 +25,10 @@ function HistoryDetails(props) {
limit,
offset,
source,
- url
+ host,
+ url,
+ elapsedTime,
+ cached
} = data;
return (
@@ -86,6 +93,15 @@ function HistoryDetails(props) {
null
}
+ {
+ data ?
+ :
+ null
+ }
+
{
data ?
:
null
}
+
+ {
+ elapsedTime ?
+ :
+ null
+ }
+
+ {
+ date ?
+ :
+ null
+ }
);
}
@@ -101,10 +135,19 @@ function HistoryDetails(props) {
if (eventType === 'releaseGrabbed') {
const {
source,
+ host,
grabTitle,
- url
+ url,
+ publishedDate,
+ infoUrl,
+ downloadClient,
+ downloadClientName,
+ elapsedTime,
+ grabMethod
} = data;
+ const downloadClientNameInfo = downloadClientName ?? downloadClient;
+
return (
{
@@ -125,6 +168,15 @@ function HistoryDetails(props) {
null
}
+ {
+ data ?
+ :
+ null
+ }
+
{
data ?
{infoUrl}}
+ /> :
+ null
+ }
+
+ {
+ publishedDate ?
+ :
+ null
+ }
+
+ {
+ downloadClientNameInfo ?
+ :
+ null
+ }
+
{
data ?
:
null
}
+
+ {
+ elapsedTime ?
+ :
+ null
+ }
+
+ {
+ grabMethod ?
+ :
+ null
+ }
+
+ {
+ date ?
+ :
+ null
+ }
);
}
if (eventType === 'indexerAuth') {
+ const { elapsedTime } = data;
+
return (
:
null
}
+
+ {
+ elapsedTime ?
+ :
+ null
+ }
+
+ {
+ date ?
+ :
+ null
+ }
);
}
@@ -171,6 +297,15 @@ function HistoryDetails(props) {
title={translate('Name')}
data={data.query}
/>
+
+ {
+ date ?
+ :
+ null
+ }
);
}
@@ -178,6 +313,7 @@ function HistoryDetails(props) {
HistoryDetails.propTypes = {
indexer: PropTypes.object.isRequired,
eventType: PropTypes.string.isRequired,
+ date: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
diff --git a/frontend/src/History/Details/HistoryDetailsModal.css b/frontend/src/History/Details/HistoryDetailsModal.css
deleted file mode 100644
index 271d422ff..000000000
--- a/frontend/src/History/Details/HistoryDetailsModal.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.markAsFailedButton {
- composes: button from '~Components/Link/Button.css';
-
- margin-right: auto;
-}
diff --git a/frontend/src/History/Details/HistoryDetailsModal.js b/frontend/src/History/Details/HistoryDetailsModal.js
index e6f960c48..560955de3 100644
--- a/frontend/src/History/Details/HistoryDetailsModal.js
+++ b/frontend/src/History/Details/HistoryDetailsModal.js
@@ -1,16 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
-import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
-import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails';
-import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
switch (eventType) {
@@ -32,11 +29,10 @@ function HistoryDetailsModal(props) {
isOpen,
eventType,
indexer,
+ date,
data,
- isMarkingAsFailed,
shortDateFormat,
timeFormat,
- onMarkAsFailedPress,
onModalClose
} = props;
@@ -54,6 +50,7 @@ function HistoryDetailsModal(props) {
- {
- eventType === 'grabbed' &&
-
- Mark as Failed
-
- }
-
- );
- }
-}
-
-HistoryRowParameter.propTypes = {
- title: PropTypes.string.isRequired,
- value: PropTypes.string.isRequired
-};
-
-export default HistoryRowParameter;
diff --git a/frontend/src/History/HistoryRowParameter.tsx b/frontend/src/History/HistoryRowParameter.tsx
new file mode 100644
index 000000000..ad83d5d77
--- /dev/null
+++ b/frontend/src/History/HistoryRowParameter.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import Link from 'Components/Link/Link';
+import { HistoryQueryType } from 'typings/History';
+import styles from './HistoryRowParameter.css';
+
+interface HistoryRowParameterProps {
+ title: string;
+ value: string;
+ queryType: HistoryQueryType;
+}
+
+function HistoryRowParameter(props: HistoryRowParameterProps) {
+ const { title, value, queryType } = props;
+
+ const type = title.toLowerCase();
+
+ let link = null;
+
+ if (type === 'imdb') {
+ link = {value};
+ } else if (type === 'tmdb') {
+ link = (
+
+ {value}
+
+ );
+ } else if (type === 'tvdb') {
+ link = (
+
+ {value}
+
+ );
+ } else if (type === 'tvmaze') {
+ link = {value};
+ }
+
+ return (
+
+
+ {title}
+
+
+
{link ? link : value}
+
+ );
+}
+
+export default HistoryRowParameter;
diff --git a/frontend/src/Indexer/Add/AddIndexerModal.js b/frontend/src/Indexer/Add/AddIndexerModal.js
deleted file mode 100644
index 4c4db24b9..000000000
--- a/frontend/src/Indexer/Add/AddIndexerModal.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Modal from 'Components/Modal/Modal';
-import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
-import styles from './AddIndexerModal.css';
-
-function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps }) {
- return (
-
-
-
- );
-}
-
-AddIndexerModal.propTypes = {
- isOpen: PropTypes.bool.isRequired,
- onModalClose: PropTypes.func.isRequired,
- onSelectIndexer: PropTypes.func.isRequired
-};
-
-export default AddIndexerModal;
diff --git a/frontend/src/Indexer/Add/AddIndexerModal.tsx b/frontend/src/Indexer/Add/AddIndexerModal.tsx
new file mode 100644
index 000000000..be22eec57
--- /dev/null
+++ b/frontend/src/Indexer/Add/AddIndexerModal.tsx
@@ -0,0 +1,44 @@
+import React, { useCallback } from 'react';
+import { useDispatch } from 'react-redux';
+import Modal from 'Components/Modal/Modal';
+import { sizes } from 'Helpers/Props';
+import { clearIndexerSchema } from 'Store/Actions/indexerActions';
+import AddIndexerModalContent from './AddIndexerModalContent';
+import styles from './AddIndexerModal.css';
+
+interface AddIndexerModalProps {
+ isOpen: boolean;
+ onSelectIndexer(): void;
+ onModalClose(): void;
+}
+
+function AddIndexerModal({
+ isOpen,
+ onSelectIndexer,
+ onModalClose,
+ ...otherProps
+}: AddIndexerModalProps) {
+ const dispatch = useDispatch();
+
+ const onModalClosePress = useCallback(() => {
+ dispatch(clearIndexerSchema());
+ onModalClose();
+ }, [dispatch, onModalClose]);
+
+ return (
+
+
+
+ );
+}
+
+export default AddIndexerModal;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css b/frontend/src/Indexer/Add/AddIndexerModalContent.css
index 5a92b40cb..e824c5475 100644
--- a/frontend/src/Indexer/Add/AddIndexerModalContent.css
+++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css
@@ -19,12 +19,18 @@
margin-bottom: 16px;
}
-.alert {
+.notice {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}
+.alert {
+ composes: alert from '~Components/Alert.css';
+
+ text-align: center;
+}
+
.scroller {
flex: 1 1 auto;
}
@@ -40,7 +46,6 @@
flex: 1;
flex-direction: column;
margin-right: 12px;
- max-width: 50%;
}
.filterContainer:last-child {
@@ -53,17 +58,22 @@
}
@media only screen and (max-width: $breakpointSmall) {
+ .filterInput {
+ margin-bottom: 5px;
+ }
+
.alert {
display: none;
}
.filterRow {
- flex-direction: column;
+ display: block;
+ margin-bottom: 10px;
}
.filterContainer {
margin-right: 0;
- margin-bottom: 12px;
+ margin-bottom: 5px;
}
.scroller {
@@ -73,6 +83,12 @@
}
}
+@media only screen and (min-width: $breakpointSmall) {
+ .filterContainer {
+ max-width: 50%;
+ }
+}
+
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
@@ -88,4 +104,8 @@
flex-direction: column;
gap: 10px;
}
+
+ .available {
+ display: none;
+ }
}
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts
index cbedc72a4..5978832e4 100644
--- a/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts
+++ b/frontend/src/Indexer/Add/AddIndexerModalContent.css.d.ts
@@ -10,6 +10,7 @@ interface CssExports {
'indexers': string;
'modalBody': string;
'modalFooter': string;
+ 'notice': string;
'scroller': string;
}
export const cssExports: CssExports;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.js b/frontend/src/Indexer/Add/AddIndexerModalContent.js
deleted file mode 100644
index 4617664ad..000000000
--- a/frontend/src/Indexer/Add/AddIndexerModalContent.js
+++ /dev/null
@@ -1,311 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Alert from 'Components/Alert';
-import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
-import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
-import TextInput from 'Components/Form/TextInput';
-import Button from 'Components/Link/Button';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import ModalBody from 'Components/Modal/ModalBody';
-import ModalContent from 'Components/Modal/ModalContent';
-import ModalFooter from 'Components/Modal/ModalFooter';
-import ModalHeader from 'Components/Modal/ModalHeader';
-import Scroller from 'Components/Scroller/Scroller';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import { kinds, scrollDirections } from 'Helpers/Props';
-import getErrorMessage from 'Utilities/Object/getErrorMessage';
-import translate from 'Utilities/String/translate';
-import SelectIndexerRowConnector from './SelectIndexerRowConnector';
-import styles from './AddIndexerModalContent.css';
-
-const columns = [
- {
- name: 'protocol',
- label: translate('Protocol'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'sortName',
- label: translate('Name'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'language',
- label: translate('Language'),
- isSortable: true,
- isVisible: true
- },
- {
- name: 'description',
- label: translate('Description'),
- isSortable: false,
- isVisible: true
- },
- {
- name: 'privacy',
- label: translate('Privacy'),
- isSortable: true,
- isVisible: true
- }
-];
-
-const protocols = [
- {
- key: 'torrent',
- value: 'torrent'
- },
- {
- key: 'usenet',
- value: 'nzb'
- }
-];
-
-const privacyLevels = [
- {
- key: 'private',
- value: translate('Private')
- },
- {
- key: 'semiPrivate',
- value: translate('SemiPrivate')
- },
- {
- key: 'public',
- value: translate('Public')
- }
-];
-
-class AddIndexerModalContent extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- filter: '',
- filterProtocols: [],
- filterLanguages: [],
- filterPrivacyLevels: [],
- filterCategories: []
- };
- }
-
- //
- // Listeners
-
- onFilterChange = ({ value }) => {
- this.setState({ filter: value });
- };
-
- //
- // Render
-
- render() {
- const {
- indexers,
- onIndexerSelect,
- sortKey,
- sortDirection,
- isFetching,
- isPopulated,
- error,
- onSortPress,
- onModalClose
- } = this.props;
-
- const languages = Array.from(new Set(indexers.map(({ language }) => language)))
- .sort((a, b) => a.localeCompare(b))
- .map((language) => ({ key: language, value: language }));
-
- const filteredIndexers = indexers.filter((indexer) => {
- const {
- filter,
- filterProtocols,
- filterLanguages,
- filterPrivacyLevels,
- filterCategories
- } = this.state;
-
- if (!indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) && !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())) {
- return false;
- }
-
- if (filterProtocols.length && !filterProtocols.includes(indexer.protocol)) {
- return false;
- }
-
- if (filterLanguages.length && !filterLanguages.includes(indexer.language)) {
- return false;
- }
-
- if (filterPrivacyLevels.length && !filterPrivacyLevels.includes(indexer.privacy)) {
- return false;
- }
-
- if (filterCategories.length) {
- const { categories = [] } = indexer.capabilities || {};
- const flat = ({ id, subCategories = [] }) => [id, ...subCategories.flatMap(flat)];
- const flatCategories = categories
- .filter((item) => item.id < 100000)
- .flatMap(flat);
-
- if (!filterCategories.every((item) => flatCategories.includes(item))) {
- return false;
- }
- }
-
- return true;
- });
-
- const errorMessage = getErrorMessage(error, translate('UnableToLoadIndexers'));
-
- return (
-
-
- {translate('AddIndexer')}
-
-
-
-
-
-
-
-
- this.setState({ filterProtocols: value })}
- />
-
-
-
-
- this.setState({ filterLanguages: value })}
- />
-
-
-
-
- this.setState({ filterPrivacyLevels: value })}
- />
-
-
-
-
- this.setState({ filterCategories: value })}
- />
-
-
-
-
-
- {translate('ProwlarrSupportsAnyIndexer')}
-
-
-
-
- {
- isFetching ? : null
- }
- {
- error ? {errorMessage} : null
- }
- {
- isPopulated && !!indexers.length ?
-
-
- {
- filteredIndexers.map((indexer) => (
-
- ))
- }
-
-
:
- null
- }
- {
- isPopulated && !!indexers.length && !filteredIndexers.length ?
-
- {translate('NoIndexersFound')}
- :
- null
- }
-
-
-
-
-
- {
- isPopulated ?
- translate('CountIndexersAvailable', [filteredIndexers.length]) :
- null
- }
-
-
-
-
-
-
-
- );
- }
-}
-
-AddIndexerModalContent.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- error: PropTypes.object,
- sortKey: PropTypes.string,
- sortDirection: PropTypes.string,
- onSortPress: PropTypes.func.isRequired,
- indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
- onIndexerSelect: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default AddIndexerModalContent;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContent.tsx b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx
new file mode 100644
index 000000000..be1413769
--- /dev/null
+++ b/frontend/src/Indexer/Add/AddIndexerModalContent.tsx
@@ -0,0 +1,434 @@
+import { some } from 'lodash';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { createSelector } from 'reselect';
+import IndexerAppState from 'App/State/IndexerAppState';
+import Alert from 'Components/Alert';
+import EnhancedSelectInput from 'Components/Form/EnhancedSelectInput';
+import NewznabCategorySelectInputConnector from 'Components/Form/NewznabCategorySelectInputConnector';
+import TextInput from 'Components/Form/TextInput';
+import Button from 'Components/Link/Button';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import Scroller from 'Components/Scroller/Scroller';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import { kinds, scrollDirections } from 'Helpers/Props';
+import Indexer, { IndexerCategory } from 'Indexer/Indexer';
+import {
+ fetchIndexerSchema,
+ selectIndexerSchema,
+ setIndexerSchemaSort,
+} from 'Store/Actions/indexerActions';
+import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
+import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
+import { SortCallback } from 'typings/callbacks';
+import sortByProp from 'Utilities/Array/sortByProp';
+import getErrorMessage from 'Utilities/Object/getErrorMessage';
+import translate from 'Utilities/String/translate';
+import SelectIndexerRow from './SelectIndexerRow';
+import styles from './AddIndexerModalContent.css';
+
+const COLUMNS = [
+ {
+ name: 'protocol',
+ label: () => translate('Protocol'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'sortName',
+ label: () => translate('Name'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'language',
+ label: () => translate('Language'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'description',
+ label: () => translate('Description'),
+ isSortable: false,
+ isVisible: true,
+ },
+ {
+ name: 'privacy',
+ label: () => translate('Privacy'),
+ isSortable: true,
+ isVisible: true,
+ },
+ {
+ name: 'categories',
+ label: () => translate('Categories'),
+ isSortable: false,
+ isVisible: true,
+ },
+];
+
+const PROTOCOLS = [
+ {
+ key: 'torrent',
+ value: 'torrent',
+ },
+ {
+ key: 'usenet',
+ value: 'nzb',
+ },
+];
+
+const PRIVACY_LEVELS = [
+ {
+ key: 'private',
+ get value() {
+ return translate('Private');
+ },
+ },
+ {
+ key: 'semiPrivate',
+ get value() {
+ return translate('SemiPrivate');
+ },
+ },
+ {
+ key: 'public',
+ get value() {
+ return translate('Public');
+ },
+ },
+];
+
+interface IndexerSchema extends Indexer {
+ isExistingIndexer: boolean;
+}
+
+function createAddIndexersSelector() {
+ return createSelector(
+ createClientSideCollectionSelector('indexers.schema'),
+ createAllIndexersSelector(),
+ (indexers: IndexerAppState, allIndexers) => {
+ const { isFetching, isPopulated, error, items, sortDirection, sortKey } =
+ indexers;
+
+ const indexerList: IndexerSchema[] = items.map((item) => {
+ const { definitionName } = item;
+ return {
+ ...item,
+ isExistingIndexer: some(allIndexers, { definitionName }),
+ };
+ });
+
+ return {
+ isFetching,
+ isPopulated,
+ error,
+ indexers: indexerList,
+ sortKey,
+ sortDirection,
+ };
+ }
+ );
+}
+
+interface AddIndexerModalContentProps {
+ onSelectIndexer(): void;
+ onModalClose(): void;
+}
+
+function AddIndexerModalContent(props: AddIndexerModalContentProps) {
+ const { onSelectIndexer, onModalClose } = props;
+
+ const { isFetching, isPopulated, error, indexers, sortKey, sortDirection } =
+ useSelector(createAddIndexersSelector());
+ const dispatch = useDispatch();
+
+ const [filter, setFilter] = useState('');
+ const [filterProtocols, setFilterProtocols] = useState([]);
+ const [filterLanguages, setFilterLanguages] = useState([]);
+ const [filterPrivacyLevels, setFilterPrivacyLevels] = useState([]);
+ const [filterCategories, setFilterCategories] = useState([]);
+
+ useEffect(
+ () => {
+ dispatch(fetchIndexerSchema());
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ []
+ );
+
+ const onFilterChange = useCallback(
+ ({ value }: { value: string }) => {
+ setFilter(value);
+ },
+ [setFilter]
+ );
+
+ const onFilterProtocolsChange = useCallback(
+ ({ value }: { value: string[] }) => {
+ setFilterProtocols(value);
+ },
+ [setFilterProtocols]
+ );
+
+ const onFilterLanguagesChange = useCallback(
+ ({ value }: { value: string[] }) => {
+ setFilterLanguages(value);
+ },
+ [setFilterLanguages]
+ );
+
+ const onFilterPrivacyLevelsChange = useCallback(
+ ({ value }: { value: string[] }) => {
+ setFilterPrivacyLevels(value);
+ },
+ [setFilterPrivacyLevels]
+ );
+
+ const onFilterCategoriesChange = useCallback(
+ ({ value }: { value: number[] }) => {
+ setFilterCategories(value);
+ },
+ [setFilterCategories]
+ );
+
+ const onIndexerSelect = useCallback(
+ ({
+ implementation,
+ implementationName,
+ name,
+ }: {
+ implementation: string;
+ implementationName: string;
+ name: string;
+ }) => {
+ dispatch(
+ selectIndexerSchema({
+ implementation,
+ implementationName,
+ name,
+ })
+ );
+
+ onSelectIndexer();
+ },
+ [dispatch, onSelectIndexer]
+ );
+
+ const onSortPress = useCallback(
+ (sortKey, sortDirection) => {
+ dispatch(setIndexerSchemaSort({ sortKey, sortDirection }));
+ },
+ [dispatch]
+ );
+
+ const languages = useMemo(
+ () =>
+ Array.from(new Set(indexers.map(({ language }) => language)))
+ .map((language) => ({ key: language, value: language }))
+ .sort(sortByProp('value')),
+ [indexers]
+ );
+
+ const filteredIndexers = useMemo(() => {
+ const flat = ({
+ id,
+ subCategories = [],
+ }: {
+ id: number;
+ subCategories: IndexerCategory[];
+ }): number[] => [id, ...subCategories.flatMap(flat)];
+
+ return indexers.filter((indexer) => {
+ if (
+ filter.length &&
+ !indexer.name.toLowerCase().includes(filter.toLocaleLowerCase()) &&
+ !indexer.description.toLowerCase().includes(filter.toLocaleLowerCase())
+ ) {
+ return false;
+ }
+
+ if (
+ filterProtocols.length &&
+ !filterProtocols.includes(indexer.protocol)
+ ) {
+ return false;
+ }
+
+ if (
+ filterLanguages.length &&
+ !filterLanguages.includes(indexer.language)
+ ) {
+ return false;
+ }
+
+ if (
+ filterPrivacyLevels.length &&
+ !filterPrivacyLevels.includes(indexer.privacy)
+ ) {
+ return false;
+ }
+
+ if (filterCategories.length) {
+ const { categories = [] } = indexer.capabilities || {};
+
+ const flatCategories = categories
+ .filter((item) => item.id < 100000)
+ .flatMap(flat);
+
+ if (
+ !filterCategories.every((categoryId) =>
+ flatCategories.includes(categoryId)
+ )
+ ) {
+ return false;
+ }
+ }
+
+ return true;
+ });
+ }, [
+ indexers,
+ filter,
+ filterProtocols,
+ filterLanguages,
+ filterPrivacyLevels,
+ filterCategories,
+ ]);
+
+ const errorMessage = getErrorMessage(
+ error,
+ translate('UnableToLoadIndexers')
+ );
+
+ return (
+
+ {translate('AddIndexer')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {translate('ProwlarrSupportsAnyIndexer')}
+
+
+
+ {isFetching ? : null}
+
+ {error ? (
+
+ {errorMessage}
+
+ ) : null}
+
+ {isPopulated && !!indexers.length ? (
+
+
+ {filteredIndexers.map((indexer) => (
+
+ ))}
+
+
+ ) : null}
+
+ {isPopulated && !!indexers.length && !filteredIndexers.length ? (
+
+ {translate('NoIndexersFound')}
+
+ ) : null}
+
+
+
+
+
+ {isPopulated
+ ? translate('CountIndexersAvailable', {
+ count: filteredIndexers.length,
+ })
+ : null}
+
+
+
+
+
+
+
+ );
+}
+
+export default AddIndexerModalContent;
diff --git a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js b/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js
deleted file mode 100644
index 0dc810608..000000000
--- a/frontend/src/Indexer/Add/AddIndexerModalContentConnector.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
-import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
-import AddIndexerModalContent from './AddIndexerModalContent';
-
-function createMapStateToProps() {
- return createSelector(
- createClientSideCollectionSelector('indexers.schema'),
- (indexers) => {
- const {
- isFetching,
- isPopulated,
- error,
- items,
- sortDirection,
- sortKey
- } = indexers;
-
- return {
- isFetching,
- isPopulated,
- error,
- indexers: items,
- sortKey,
- sortDirection
- };
- }
- );
-}
-
-const mapDispatchToProps = {
- fetchIndexerSchema,
- selectIndexerSchema,
- setIndexerSchemaSort
-};
-
-class AddIndexerModalContentConnector extends Component {
-
- //
- // Lifecycle
-
- componentDidMount() {
- this.props.fetchIndexerSchema();
- }
-
- //
- // Listeners
-
- onIndexerSelect = ({ implementation, name }) => {
- this.props.selectIndexerSchema({ implementation, name });
- this.props.onSelectIndexer();
- };
-
- onSortPress = (sortKey, sortDirection) => {
- this.props.setIndexerSchemaSort({ sortKey, sortDirection });
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-AddIndexerModalContentConnector.propTypes = {
- fetchIndexerSchema: PropTypes.func.isRequired,
- selectIndexerSchema: PropTypes.func.isRequired,
- setIndexerSchemaSort: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired,
- onSelectIndexer: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);
diff --git a/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js b/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js
index 03196e526..8f98d0e12 100644
--- a/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js
+++ b/frontend/src/Indexer/Add/AddIndexerPresetMenuItem.js
@@ -10,12 +10,14 @@ class AddIndexerPresetMenuItem extends Component {
onPress = () => {
const {
name,
- implementation
+ implementation,
+ implementationName
} = this.props;
this.props.onPress({
name,
- implementation
+ implementation,
+ implementationName
});
};
@@ -26,6 +28,7 @@ class AddIndexerPresetMenuItem extends Component {
const {
name,
implementation,
+ implementationName,
...otherProps
} = this.props;
@@ -43,6 +46,7 @@ class AddIndexerPresetMenuItem extends Component {
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
+ implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.js b/frontend/src/Indexer/Add/SelectIndexerRow.js
deleted file mode 100644
index c3f33220d..000000000
--- a/frontend/src/Indexer/Add/SelectIndexerRow.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Icon from 'Components/Icon';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import TableRowButton from 'Components/Table/TableRowButton';
-import { icons } from 'Helpers/Props';
-import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
-import firstCharToUpper from 'Utilities/String/firstCharToUpper';
-import translate from 'Utilities/String/translate';
-import styles from './SelectIndexerRow.css';
-
-class SelectIndexerRow extends Component {
-
- //
- // Listeners
-
- onPress = () => {
- const {
- implementation,
- name
- } = this.props;
-
- this.props.onIndexerSelect({ implementation, name });
- };
-
- //
- // Render
-
- render() {
- const {
- protocol,
- privacy,
- name,
- language,
- description,
- isExistingIndexer
- } = this.props;
-
- return (
-
-
-
-
-
-
- {name}
- {
- isExistingIndexer ?
- :
- null
- }
-
-
-
- {language}
-
-
-
- {description}
-
-
-
- {translate(firstCharToUpper(privacy))}
-
-
- );
- }
-}
-
-SelectIndexerRow.propTypes = {
- name: PropTypes.string.isRequired,
- protocol: PropTypes.string.isRequired,
- privacy: PropTypes.string.isRequired,
- language: PropTypes.string.isRequired,
- description: PropTypes.string.isRequired,
- implementation: PropTypes.string.isRequired,
- onIndexerSelect: PropTypes.func.isRequired,
- isExistingIndexer: PropTypes.bool.isRequired
-};
-
-export default SelectIndexerRow;
diff --git a/frontend/src/Indexer/Add/SelectIndexerRow.tsx b/frontend/src/Indexer/Add/SelectIndexerRow.tsx
new file mode 100644
index 000000000..157050e41
--- /dev/null
+++ b/frontend/src/Indexer/Add/SelectIndexerRow.tsx
@@ -0,0 +1,78 @@
+import React, { useCallback } from 'react';
+import Icon from 'Components/Icon';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import TableRowButton from 'Components/Table/TableRowButton';
+import DownloadProtocol from 'DownloadClient/DownloadProtocol';
+import { icons } from 'Helpers/Props';
+import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
+import PrivacyLabel from 'Indexer/Index/Table/PrivacyLabel';
+import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
+import { IndexerCapabilities, IndexerPrivacy } from 'Indexer/Indexer';
+import translate from 'Utilities/String/translate';
+import styles from './SelectIndexerRow.css';
+
+interface SelectIndexerRowProps {
+ name: string;
+ protocol: DownloadProtocol;
+ privacy: IndexerPrivacy;
+ language: string;
+ description: string;
+ capabilities: IndexerCapabilities;
+ implementation: string;
+ implementationName: string;
+ isExistingIndexer: boolean;
+ onIndexerSelect(...args: unknown[]): void;
+}
+
+function SelectIndexerRow(props: SelectIndexerRowProps) {
+ const {
+ name,
+ protocol,
+ privacy,
+ language,
+ description,
+ capabilities,
+ implementation,
+ implementationName,
+ isExistingIndexer,
+ onIndexerSelect,
+ } = props;
+
+ const onPress = useCallback(() => {
+ onIndexerSelect({ implementation, implementationName, name });
+ }, [implementation, implementationName, name, onIndexerSelect]);
+
+ return (
+
+
+
+
+
+
+ {name}
+ {isExistingIndexer ? (
+
+ ) : null}
+
+
+ {language}
+
+ {description}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default SelectIndexerRow;
diff --git a/frontend/src/Indexer/Add/SelectIndexerRowConnector.js b/frontend/src/Indexer/Add/SelectIndexerRowConnector.js
deleted file mode 100644
index f507689c8..000000000
--- a/frontend/src/Indexer/Add/SelectIndexerRowConnector.js
+++ /dev/null
@@ -1,18 +0,0 @@
-
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import createExistingIndexerSelector from 'Store/Selectors/createExistingIndexerSelector';
-import SelectIndexerRow from './SelectIndexerRow';
-
-function createMapStateToProps() {
- return createSelector(
- createExistingIndexerSelector(),
- (isExistingIndexer, dimensions) => {
- return {
- isExistingIndexer
- };
- }
- );
-}
-
-export default connect(createMapStateToProps)(SelectIndexerRow);
diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModal.js b/frontend/src/Indexer/Delete/DeleteIndexerModal.js
deleted file mode 100644
index aed954829..000000000
--- a/frontend/src/Indexer/Delete/DeleteIndexerModal.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import Modal from 'Components/Modal/Modal';
-import { sizes } from 'Helpers/Props';
-import DeleteIndexerModalContentConnector from './DeleteIndexerModalContentConnector';
-
-function DeleteIndexerModal(props) {
- const {
- isOpen,
- onModalClose,
- ...otherProps
- } = props;
-
- return (
-
-
-
- );
-}
-
-DeleteIndexerModal.propTypes = {
- ...DeleteIndexerModalContentConnector.propTypes,
- isOpen: PropTypes.bool.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default DeleteIndexerModal;
diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModal.tsx b/frontend/src/Indexer/Delete/DeleteIndexerModal.tsx
new file mode 100644
index 000000000..13850aa77
--- /dev/null
+++ b/frontend/src/Indexer/Delete/DeleteIndexerModal.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import Modal from 'Components/Modal/Modal';
+import { sizes } from 'Helpers/Props';
+import DeleteIndexerModalContent from './DeleteIndexerModalContent';
+
+interface DeleteIndexerModalProps {
+ isOpen: boolean;
+ indexerId: number;
+ onModalClose(): void;
+}
+
+function DeleteIndexerModal(props: DeleteIndexerModalProps) {
+ const { isOpen, indexerId, onModalClose } = props;
+
+ return (
+
+
+
+ );
+}
+
+export default DeleteIndexerModal;
diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModalContent.js b/frontend/src/Indexer/Delete/DeleteIndexerModalContent.js
deleted file mode 100644
index e3d46e108..000000000
--- a/frontend/src/Indexer/Delete/DeleteIndexerModalContent.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import Button from 'Components/Link/Button';
-import ModalBody from 'Components/Modal/ModalBody';
-import ModalContent from 'Components/Modal/ModalContent';
-import ModalFooter from 'Components/Modal/ModalFooter';
-import ModalHeader from 'Components/Modal/ModalHeader';
-import { kinds } from 'Helpers/Props';
-import translate from 'Utilities/String/translate';
-
-class DeleteIndexerModalContent extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- deleteFiles: false,
- addImportExclusion: false
- };
- }
-
- //
- // Listeners
-
- onDeleteFilesChange = ({ value }) => {
- this.setState({ deleteFiles: value });
- };
-
- onAddImportExclusionChange = ({ value }) => {
- this.setState({ addImportExclusion: value });
- };
-
- onDeleteMovieConfirmed = () => {
- const deleteFiles = this.state.deleteFiles;
- const addImportExclusion = this.state.addImportExclusion;
-
- this.setState({ deleteFiles: false, addImportExclusion: false });
- this.props.onDeletePress(deleteFiles, addImportExclusion);
- };
-
- //
- // Render
-
- render() {
- const {
- name,
- onModalClose
- } = this.props;
-
- return (
-
-
- Delete - {name}
-
-
-
- {`Are you sure you want to delete ${name} from Prowlarr`}
-
-
-
-
-
-
-
-
- );
- }
-}
-
-DeleteIndexerModalContent.propTypes = {
- name: PropTypes.string.isRequired,
- onDeletePress: PropTypes.func.isRequired,
- onModalClose: PropTypes.func.isRequired
-};
-
-export default DeleteIndexerModalContent;
diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx b/frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx
new file mode 100644
index 000000000..aeae273a9
--- /dev/null
+++ b/frontend/src/Indexer/Delete/DeleteIndexerModalContent.tsx
@@ -0,0 +1,54 @@
+import React, { useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import Button from 'Components/Link/Button';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import { kinds } from 'Helpers/Props';
+import Indexer from 'Indexer/Indexer';
+import { deleteIndexer } from 'Store/Actions/indexerActions';
+import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
+import translate from 'Utilities/String/translate';
+
+interface DeleteIndexerModalContentProps {
+ indexerId: number;
+ onModalClose(): void;
+}
+
+function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
+ const { indexerId, onModalClose } = props;
+
+ const { name } = useSelector(
+ createIndexerSelectorForHook(indexerId)
+ ) as Indexer;
+ const dispatch = useDispatch();
+
+ const onConfirmDelete = useCallback(() => {
+ dispatch(deleteIndexer({ id: indexerId }));
+
+ onModalClose();
+ }, [indexerId, dispatch, onModalClose]);
+
+ return (
+
+
+ {translate('Delete')} - {name}
+
+
+
+ {translate('AreYouSureYouWantToDeleteIndexer', { name })}
+
+
+
+
+
+
+
+
+ );
+}
+
+export default DeleteIndexerModalContent;
diff --git a/frontend/src/Indexer/Delete/DeleteIndexerModalContentConnector.js b/frontend/src/Indexer/Delete/DeleteIndexerModalContentConnector.js
deleted file mode 100644
index 1e92eb845..000000000
--- a/frontend/src/Indexer/Delete/DeleteIndexerModalContentConnector.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { push } from 'connected-react-router';
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { deleteIndexer } from 'Store/Actions/indexerActions';
-import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
-import DeleteIndexerModalContent from './DeleteIndexerModalContent';
-
-function createMapStateToProps() {
- return createSelector(
- createIndexerSelector(),
- (indexer) => {
- return indexer;
- }
- );
-}
-
-const mapDispatchToProps = {
- deleteIndexer,
- push
-};
-
-class DeleteIndexerModalContentConnector extends Component {
-
- //
- // Listeners
-
- onDeletePress = () => {
- this.props.deleteIndexer({
- id: this.props.indexerId
- });
-
- this.props.onModalClose(true);
- };
-
- //
- // Render
-
- render() {
- return (
-
- );
- }
-}
-
-DeleteIndexerModalContentConnector.propTypes = {
- indexerId: PropTypes.number.isRequired,
- onModalClose: PropTypes.func.isRequired,
- deleteIndexer: PropTypes.func.isRequired,
- push: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector);
diff --git a/frontend/src/Indexer/Edit/EditIndexerModalContent.js b/frontend/src/Indexer/Edit/EditIndexerModalContent.js
index 4a2ec4c0e..7dabc50d9 100644
--- a/frontend/src/Indexer/Edit/EditIndexerModalContent.js
+++ b/frontend/src/Indexer/Edit/EditIndexerModalContent.js
@@ -61,7 +61,7 @@ function EditIndexerModalContent(props) {
return (
- {`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`}
+ {id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })}
@@ -97,7 +97,7 @@ function EditIndexerModalContent(props) {
@@ -144,6 +144,7 @@ function EditIndexerModalContent(props) {
}) :
null
}
+
diff --git a/frontend/src/Indexer/Index/IndexerIndex.tsx b/frontend/src/Indexer/Index/IndexerIndex.tsx
index dcb7b8c9e..e20e269f8 100644
--- a/frontend/src/Indexer/Index/IndexerIndex.tsx
+++ b/frontend/src/Indexer/Index/IndexerIndex.tsx
@@ -1,6 +1,16 @@
-import React, { useCallback, useMemo, useRef, useState } from 'react';
+import React, {
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
+import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
+import IndexerAppState, {
+ IndexerIndexAppState,
+} from 'App/State/IndexerAppState';
import { APP_INDEXER_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@@ -18,12 +28,17 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer';
import { executeCommand } from 'Store/Actions/commandActions';
-import { testAllIndexers } from 'Store/Actions/indexerActions';
+import {
+ cloneIndexer,
+ fetchIndexers,
+ testAllIndexers,
+} from 'Store/Actions/indexerActions';
import {
setIndexerFilter,
setIndexerSort,
setIndexerTableOption,
} from 'Store/Actions/indexerIndexActions';
+import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
@@ -41,9 +56,7 @@ import IndexerIndexTable from './Table/IndexerIndexTable';
import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions';
import styles from './IndexerIndex.css';
-function getViewComponent() {
- return IndexerIndexTable;
-}
+const getViewComponent = () => IndexerIndexTable;
interface IndexerIndexProps {
initialScrollTop?: number;
@@ -64,27 +77,25 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
sortKey,
sortDirection,
view,
- } = useSelector(
- createIndexerClientSideCollectionItemsSelector('indexerIndex')
- );
+ }: IndexerAppState & IndexerIndexAppState & ClientSideCollectionAppState =
+ useSelector(createIndexerClientSideCollectionItemsSelector('indexerIndex'));
const isSyncingIndexers = useSelector(
createCommandExecutingSelector(APP_INDEXER_SYNC)
);
const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch();
- const scrollerRef = useRef();
+ const scrollerRef = useRef(null);
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
- const [jumpToCharacter, setJumpToCharacter] = useState(null);
+ const [jumpToCharacter, setJumpToCharacter] = useState(
+ undefined
+ );
const [isSelectMode, setIsSelectMode] = useState(false);
- const onAppIndexerSyncPress = useCallback(() => {
- dispatch(
- executeCommand({
- name: APP_INDEXER_SYNC,
- })
- );
+ useEffect(() => {
+ dispatch(fetchIndexers());
+ dispatch(fetchIndexerStatus());
}, [dispatch]);
const onAddIndexerPress = useCallback(() => {
@@ -103,6 +114,24 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
+ const onCloneIndexerPress = useCallback(
+ (id: number) => {
+ dispatch(cloneIndexer({ id }));
+
+ setIsEditIndexerModalOpen(true);
+ },
+ [dispatch, setIsEditIndexerModalOpen]
+ );
+
+ const onAppIndexerSyncPress = useCallback(() => {
+ dispatch(
+ executeCommand({
+ name: APP_INDEXER_SYNC,
+ forceSync: true,
+ })
+ );
+ }, [dispatch]);
+
const onTestAllPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);
@@ -112,37 +141,37 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
- (payload) => {
+ (payload: unknown) => {
dispatch(setIndexerTableOption(payload));
},
[dispatch]
);
const onSortSelect = useCallback(
- (value) => {
+ (value: string) => {
dispatch(setIndexerSort({ sortKey: value }));
},
[dispatch]
);
const onFilterSelect = useCallback(
- (value) => {
+ (value: string) => {
dispatch(setIndexerFilter({ selectedFilterKey: value }));
},
[dispatch]
);
const onJumpBarItemPress = useCallback(
- (character) => {
+ (character: string) => {
setJumpToCharacter(character);
},
[setJumpToCharacter]
);
const onScroll = useCallback(
- ({ scrollTop }) => {
- setJumpToCharacter(null);
- scrollPositions.seriesIndex = scrollTop;
+ ({ scrollTop }: { scrollTop: number }) => {
+ setJumpToCharacter(undefined);
+ scrollPositions.indexerIndex = scrollTop;
},
[setJumpToCharacter]
);
@@ -155,7 +184,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
};
}
- const characters = items.reduce((acc, item) => {
+ const characters = items.reduce((acc: Record, item) => {
let char = item.sortName.charAt(0);
if (!isNaN(Number(char))) {
@@ -277,6 +306,8 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
{
jumpToCharacter={jumpToCharacter}
isSelectMode={isSelectMode}
isSmallScreen={isSmallScreen}
+ onCloneIndexerPress={onCloneIndexerPress}
/>
diff --git a/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx b/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx
index 8a151907a..1b4bfb6de 100644
--- a/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx
+++ b/frontend/src/Indexer/Index/IndexerIndexFilterModal.tsx
@@ -1,12 +1,13 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
+import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setIndexerFilter } from 'Store/Actions/indexerIndexActions';
function createIndexerSelector() {
return createSelector(
- (state) => state.indexers.items,
+ (state: AppState) => state.indexers.items,
(indexers) => {
return indexers;
}
@@ -15,14 +16,20 @@ function createIndexerSelector() {
function createFilterBuilderPropsSelector() {
return createSelector(
- (state) => state.indexerIndex.filterBuilderProps,
+ (state: AppState) => state.indexerIndex.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
-export default function IndexerIndexFilterModal(props) {
+interface IndexerIndexFilterModalProps {
+ isOpen: boolean;
+}
+
+export default function IndexerIndexFilterModal(
+ props: IndexerIndexFilterModalProps
+) {
const sectionItems = useSelector(createIndexerSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'indexerIndex';
@@ -30,7 +37,7 @@ export default function IndexerIndexFilterModal(props) {
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
- (payload) => {
+ (payload: unknown) => {
dispatch(setIndexerFilter(payload));
},
[dispatch]
@@ -38,6 +45,7 @@ export default function IndexerIndexFilterModal(props) {
return (
{
+ (indexers: IndexerAppState) => {
return indexers.items.map((s) => {
const { protocol, privacy, enable } = s;
diff --git a/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx b/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx
index 0b6021bad..57ebf7b2f 100644
--- a/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx
+++ b/frontend/src/Indexer/Index/Menus/IndexerIndexFilterMenu.tsx
@@ -1,10 +1,18 @@
-import PropTypes from 'prop-types';
import React from 'react';
+import { CustomFilter } from 'App/State/AppState';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
import IndexerIndexFilterModal from 'Indexer/Index/IndexerIndexFilterModal';
-function IndexerIndexFilterMenu(props) {
+interface IndexerIndexFilterMenuProps {
+ selectedFilterKey: string | number;
+ filters: object[];
+ customFilters: CustomFilter[];
+ isDisabled: boolean;
+ onFilterSelect(filterName: string): unknown;
+}
+
+function IndexerIndexFilterMenu(props: IndexerIndexFilterMenuProps) {
const {
selectedFilterKey,
filters,
@@ -26,15 +34,6 @@ function IndexerIndexFilterMenu(props) {
);
}
-IndexerIndexFilterMenu.propTypes = {
- selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
- .isRequired,
- filters: PropTypes.arrayOf(PropTypes.object).isRequired,
- customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
- isDisabled: PropTypes.bool.isRequired,
- onFilterSelect: PropTypes.func.isRequired,
-};
-
IndexerIndexFilterMenu.defaultProps = {
showCustomFilters: false,
};
diff --git a/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx b/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx
index 723db799f..088cbca90 100644
--- a/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx
+++ b/frontend/src/Indexer/Index/Menus/IndexerIndexSortMenu.tsx
@@ -1,12 +1,19 @@
-import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
-import { align, sortDirections } from 'Helpers/Props';
+import { align } from 'Helpers/Props';
+import SortDirection from 'Helpers/Props/SortDirection';
import translate from 'Utilities/String/translate';
-function IndexerIndexSortMenu(props) {
+interface IndexerIndexSortMenuProps {
+ sortKey?: string;
+ sortDirection?: SortDirection;
+ isDisabled: boolean;
+ onSortSelect(sortKey: string): unknown;
+}
+
+function IndexerIndexSortMenu(props: IndexerIndexSortMenuProps) {
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return (
@@ -79,11 +86,4 @@ function IndexerIndexSortMenu(props) {
);
}
-IndexerIndexSortMenu.propTypes = {
- sortKey: PropTypes.string,
- sortDirection: PropTypes.oneOf(sortDirections.all),
- isDisabled: PropTypes.bool.isRequired,
- onSortSelect: PropTypes.func.isRequired,
-};
-
export default IndexerIndexSortMenu;
diff --git a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx
index 0e27902fe..0793af82d 100644
--- a/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx
+++ b/frontend/src/Indexer/Index/Select/Delete/DeleteIndexerModalContent.tsx
@@ -7,6 +7,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
+import Indexer from 'Indexer/Indexer';
import { bulkDeleteIndexers } from 'Store/Actions/indexerActions';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import translate from 'Utilities/String/translate';
@@ -20,15 +21,15 @@ interface DeleteIndexerModalContentProps {
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerIds, onModalClose } = props;
- const allIndexers = useSelector(createAllIndexersSelector());
+ const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
const dispatch = useDispatch();
- const selectedIndexers = useMemo(() => {
- const indexers = indexerIds.map((id) => {
+ const indexers = useMemo((): Indexer[] => {
+ const indexerList = indexerIds.map((id) => {
return allIndexers.find((s) => s.id === id);
- });
+ }) as Indexer[];
- return orderBy(indexers, ['sortName']);
+ return orderBy(indexerList, ['sortName']);
}, [indexerIds, allIndexers]);
const onDeleteIndexerConfirmed = useCallback(() => {
@@ -47,13 +48,13 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
- {translate('DeleteSelectedIndexersMessageText', [
- selectedIndexers.length,
- ])}
+ {translate('DeleteSelectedIndexersMessageText', {
+ count: indexers.length,
+ })}
- {selectedIndexers.map((s) => {
+ {indexers.map((s) => {
return (
-
{s.name}
diff --git a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx
index f3bb9cca7..9d42aa389 100644
--- a/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx
+++ b/frontend/src/Indexer/Index/Select/Edit/EditIndexerModalContent.tsx
@@ -19,6 +19,7 @@ interface SavePayload {
seedRatio?: number;
seedTime?: number;
packSeedTime?: number;
+ preferMagnetUrl?: boolean;
}
interface EditIndexerModalContentProps {
@@ -30,9 +31,25 @@ interface EditIndexerModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
- { key: NO_CHANGE, value: translate('NoChange'), disabled: true },
- { key: 'true', value: translate('Enabled') },
- { key: 'false', value: translate('Disabled') },
+ {
+ key: NO_CHANGE,
+ get value() {
+ return translate('NoChange');
+ },
+ isDisabled: true,
+ },
+ {
+ key: 'true',
+ get value() {
+ return translate('Enabled');
+ },
+ },
+ {
+ key: 'false',
+ get value() {
+ return translate('Disabled');
+ },
+ },
];
function EditIndexerModalContent(props: EditIndexerModalContentProps) {
@@ -49,6 +66,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
const [packSeedTime, setPackSeedTime] = useState(
null
);
+ const [preferMagnetUrl, setPreferMagnetUrl] = useState<
+ null | string | boolean
+ >(null);
const save = useCallback(() => {
let hasChanges = false;
@@ -89,6 +109,11 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
payload.packSeedTime = packSeedTime as number;
}
+ if (preferMagnetUrl !== null) {
+ hasChanges = true;
+ payload.preferMagnetUrl = preferMagnetUrl === 'true';
+ }
+
if (hasChanges) {
onSavePress(payload);
}
@@ -102,12 +127,13 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
seedRatio,
seedTime,
packSeedTime,
+ preferMagnetUrl,
onSavePress,
onModalClose,
]);
const onInputChange = useCallback(
- ({ name, value }) => {
+ ({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'enable':
setEnable(value);
@@ -130,6 +156,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
case 'packSeedTime':
setPackSeedTime(value);
break;
+ case 'preferMagnetUrl':
+ setPreferMagnetUrl(value);
+ break;
default:
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
}
@@ -208,6 +237,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
name="seedRatio"
value={seedRatio}
helpText={translate('SeedRatioHelpText')}
+ isFloat={true}
onChange={onInputChange}
/>
@@ -237,11 +267,23 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
onChange={onInputChange}
/>
+
+
+ {translate('PreferMagnetUrl')}
+
+
+
- {translate('CountIndexersSelected', [selectedCount])}
+ {translate('CountIndexersSelected', { count: selectedCount })}
diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx
index bd7682018..d6fc776d6 100644
--- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx
+++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectAllButton.tsx
@@ -7,7 +7,7 @@ import translate from 'Utilities/String/translate';
interface IndexerIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
- overflowComponent: React.FunctionComponent;
+ overflowComponent: React.FunctionComponent;
}
function IndexerIndexSelectAllButton(props: IndexerIndexSelectAllButtonProps) {
diff --git a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx
index 953d0daf9..64fe8c1cb 100644
--- a/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx
+++ b/frontend/src/Indexer/Index/Select/IndexerIndexSelectFooter.tsx
@@ -15,6 +15,16 @@ import EditIndexerModal from './Edit/EditIndexerModal';
import TagsModal from './Tags/TagsModal';
import styles from './IndexerIndexSelectFooter.css';
+interface SavePayload {
+ enable?: boolean;
+ appProfileId?: number;
+ priority?: number;
+ minimumSeeders?: number;
+ seedRatio?: number;
+ seedTime?: number;
+ packSeedTime?: number;
+}
+
const indexersEditorSelector = createSelector(
(state: AppState) => state.indexers,
(indexers) => {
@@ -60,7 +70,7 @@ function IndexerIndexSelectFooter() {
}, [setIsEditModalOpen]);
const onSavePress = useCallback(
- (payload) => {
+ (payload: SavePayload) => {
setIsSavingIndexer(true);
setIsEditModalOpen(false);
@@ -83,7 +93,7 @@ function IndexerIndexSelectFooter() {
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
- (tags, applyTags) => {
+ (tags: number[], applyTags: string) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);
@@ -155,7 +165,7 @@ function IndexerIndexSelectFooter() {
- {translate('CountIndexersSelected', [selectedCount])}
+ {translate('CountIndexersSelected', { count: selectedCount })}
;
onPress: () => void;
}
diff --git a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx
index 964d9ad57..1964d271c 100644
--- a/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx
+++ b/frontend/src/Indexer/Index/Select/Tags/TagsModalContent.tsx
@@ -1,6 +1,7 @@
-import { concat, uniq } from 'lodash';
+import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
+import { Tag } from 'App/State/TagsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
+import Indexer from 'Indexer/Indexer';
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import translate from 'Utilities/String/translate';
@@ -26,29 +28,35 @@ interface TagsModalContentProps {
function TagsModalContent(props: TagsModalContentProps) {
const { indexerIds, onModalClose, onApplyTagsPress } = props;
- const allIndexers = useSelector(createAllIndexersSelector());
- const tagList = useSelector(createTagsSelector());
+ const allIndexers: Indexer[] = useSelector(createAllIndexersSelector());
+ const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState([]);
const [applyTags, setApplyTags] = useState('add');
const indexerTags = useMemo(() => {
- const indexers = indexerIds.map((id) => {
- return allIndexers.find((s) => s.id === id);
- });
+ const tags = indexerIds.reduce((acc: number[], id) => {
+ const s = allIndexers.find((s) => s.id === id);
- return uniq(concat(...indexers.map((s) => s.tags)));
+ if (s) {
+ acc.push(...s.tags);
+ }
+
+ return acc;
+ }, []);
+
+ return uniq(tags);
}, [indexerIds, allIndexers]);
const onTagsChange = useCallback(
- ({ value }) => {
+ ({ value }: { value: number[] }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
- ({ value }) => {
+ ({ value }: { value: string }) => {
setApplyTags(value);
},
[setApplyTags]
diff --git a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx
index 5f742d902..8e30532cc 100644
--- a/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx
+++ b/frontend/src/Indexer/Index/Table/CapabilitiesLabel.tsx
@@ -1,6 +1,8 @@
+import { uniqBy } from 'lodash';
import React from 'react';
import Label from 'Components/Label';
import { IndexerCapabilities } from 'Indexer/Indexer';
+import translate from 'Utilities/String/translate';
interface CapabilitiesLabelProps {
capabilities: IndexerCapabilities;
@@ -23,17 +25,21 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
);
}
- const nameList = Array.from(
- new Set(filteredList.map((item) => item.name).sort())
+ const indexerCategories = uniqBy(filteredList, 'id').sort(
+ (a, b) => a.id - b.id
);
return (
- {nameList.map((category) => {
- return ;
+ {indexerCategories.map((category) => {
+ return (
+
+ );
})}
- {filteredList.length === 0 ? : null}
+ {filteredList.length === 0 ? : null}
);
}
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css
index a0a0daee4..a20efded3 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css
@@ -11,6 +11,12 @@
flex: 0 0 60px;
}
+.id {
+ composes: cell;
+
+ flex: 0 0 60px;
+}
+
.sortName {
composes: cell;
@@ -23,7 +29,8 @@
.minimumSeeders,
.seedRatio,
.seedTime,
-.packSeedTime {
+.packSeedTime,
+.preferMagnetUrl {
composes: cell;
flex: 0 0 90px;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts
index c5d22cf6d..42821bd74 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.css.d.ts
@@ -8,8 +8,10 @@ interface CssExports {
'cell': string;
'checkInput': string;
'externalLink': string;
+ 'id': string;
'minimumSeeders': string;
'packSeedTime': string;
+ 'preferMagnetUrl': string;
'priority': string;
'privacy': string;
'protocol': string;
diff --git a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx
index 5325028e9..e4c3cd32e 100644
--- a/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx
+++ b/frontend/src/Indexer/Index/Table/IndexerIndexRow.tsx
@@ -1,9 +1,9 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
-import Label from 'Components/Label';
+import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton';
-import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
+import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
@@ -12,11 +12,13 @@ import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
+import Indexer from 'Indexer/Indexer';
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
-import firstCharToUpper from 'Utilities/String/firstCharToUpper';
+import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import CapabilitiesLabel from './CapabilitiesLabel';
import IndexerStatusCell from './IndexerStatusCell';
+import PrivacyLabel from './PrivacyLabel';
import ProtocolLabel from './ProtocolLabel';
import styles from './IndexerIndexRow.css';
@@ -25,13 +27,14 @@ interface IndexerIndexRowProps {
sortKey: string;
columns: Column[];
isSelectMode: boolean;
+ onCloneIndexerPress(id: number): void;
}
function IndexerIndexRow(props: IndexerIndexRowProps) {
- const { indexerId, columns, isSelectMode } = props;
+ const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props;
const { indexer, appProfile, status, longDateFormat, timeFormat } =
- useSelector(createIndexerIndexItemSelector(props.indexerId));
+ useSelector(createIndexerIndexItemSelector(indexerId));
const {
id,
@@ -46,7 +49,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields,
added,
capabilities,
- } = indexer;
+ } = indexer as Indexer;
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??
@@ -72,6 +75,10 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields.find((field) => field.name === 'torrentBaseSettings.packSeedTime')
?.value ?? undefined;
+ const preferMagnetUrl =
+ fields.find((field) => field.name === 'torrentBaseSettings.preferMagnetUrl')
+ ?.value ?? undefined;
+
const rssUrl = `${window.location.origin}${
window.Prowlarr.urlBase
}/${id}/api?apikey=${encodeURIComponent(
@@ -105,7 +112,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
}, []);
const onSelectedChange = useCallback(
- ({ id, value, shiftKey }) => {
+ ({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({
type: 'toggleSelected',
id,
@@ -149,12 +156,25 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
);
}
+ if (name === 'id') {
+ return (
+
+
+
+ );
+ }
+
if (name === 'sortName') {
return (
);
@@ -163,7 +183,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
if (name === 'privacy') {
return (
-
+
);
}
@@ -202,7 +222,9 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
if (name === 'added') {
return (
-
+ {preferMagnetUrl === undefined ? null : (
+
+ )}
+
+ );
+ }
+
if (name === 'actions') {
return (
;
+ scrollerRef: RefObject;
isSelectMode: boolean;
isSmallScreen: boolean;
+ onCloneIndexerPress(id: number): void;
}
const columnsSelector = createSelector(
- (state) => state.indexerIndex.columns,
+ (state: AppState) => state.indexerIndex.columns,
(columns) => columns
);
-const Row: React.FC> = ({
- index,
- style,
- data,
-}) => {
- const { items, sortKey, columns, isSelectMode } = data;
+function Row({ index, style, data }: ListChildComponentProps) {
+ const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
if (index >= items.length) {
return null;
@@ -64,16 +62,18 @@ const Row: React.FC> = ({
justifyContent: 'space-between',
...style,
}}
+ className={styles.row}
>
);
-};
+}
function getWindowScrollTopPosition() {
return document.documentElement.scrollTop || document.body.scrollTop || 0;
@@ -88,25 +88,25 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
isSelectMode,
isSmallScreen,
scrollerRef,
+ onCloneIndexerPress,
} = props;
const columns = useSelector(columnsSelector);
- const { showBanners } = useSelector(selectTableOptions);
- const listRef = useRef