client: partially migrate to TypeScript

This commit is contained in:
Jesse Chan
2020-09-08 15:11:18 +08:00
parent 146daf2ecb
commit c78e5d88d1
199 changed files with 4153 additions and 3050 deletions

View File

@@ -8,14 +8,14 @@ module.exports = {
'consistent-return': 0,
'implicit-arrow-linebreak': 0,
'import/extensions': [
"error",
"ignorePackages",
'error',
'ignorePackages',
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'import/no-extraneous-dependencies': 0,
'import/prefer-default-export': 0,
@@ -33,7 +33,7 @@ module.exports = {
'no-param-reassign': 0,
'no-plusplus': 0,
'no-underscore-dangle': [2, {allow: ['_id']}],
'no-unused-vars': [0, { "argsIgnorePattern": "^_" }],
'no-unused-vars': [0, {argsIgnorePattern: '^_'}],
'object-curly-newline': 0,
'object-curly-spacing': 0,
@@ -48,4 +48,15 @@ module.exports = {
},
],
},
overrides: [
{
files: ['*.ts', '*.tsx', '**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
plugins: ['import', '@typescript-eslint/eslint-plugin'],
rules: {
'no-unused-vars': 0,
'@typescript-eslint/no-unused-vars': 1,
},
},
],
};

View File

@@ -2,7 +2,12 @@ const path = require('path');
module.exports = {
parser: 'babel-eslint',
extends: ['plugin:@typescript-eslint/recommended', 'prettier', 'prettier/@typescript-eslint'],
extends: [
'plugin:import/typescript',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
],
env: {
browser: 1,
node: 0,
@@ -14,12 +19,18 @@ module.exports = {
},
plugins: ['import'],
rules: {
"no-restricted-imports": ["error", {
"patterns": ["**/config", "**/server/**/*"]
}],
"no-restricted-modules": ["error", {
"patterns": ["**/server/**/*"]
}],
'no-restricted-imports': [
'error',
{
patterns: ['**/config', '**/server/**/*'],
},
],
'no-restricted-modules': [
'error',
{
patterns: ['**/server/**/*'],
},
],
'@typescript-eslint/no-var-requires': 0,
'@typescript-eslint/camelcase': ['error'],
camelcase: 0,
@@ -33,11 +44,7 @@ module.exports = {
'jsx-a11y/mouse-events-have-key-events': 0,
'jsx-a11y/no-noninteractive-element-interactions': 0,
'jsx-a11y/no-static-element-interactions': 0,
'lines-between-class-members': [
'error',
'always',
{ exceptAfterSingleLine: true },
],
'lines-between-class-members': ['error', 'always', {exceptAfterSingleLine: true}],
'no-console': [2, {allow: ['warn', 'error']}],
'react/button-has-type': 0,
'react/default-props-match-prop-types': 0,

View File

@@ -1,23 +1,24 @@
import axios from 'axios';
import ActionTypes from '../constants/ActionTypes';
import AppDispatcher from '../dispatcher/AppDispatcher';
import ClientActions from './ClientActions';
import ConfigStore from '../stores/ConfigStore';
import FloodActions from './FloodActions';
import SettingsActions from './SettingsActions';
import type {ConnectionSettings, Credentials, UserConfig} from '../stores/AuthStore';
const baseURI = ConfigStore.getBaseURI();
const AuthActions = {
authenticate: (credentials) =>
authenticate: (credentials: Credentials) =>
axios
.post(`${baseURI}auth/authenticate`, credentials)
.then((json = {}) => json.data)
.then((json) => json.data)
.then(
(data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LOGIN_SUCCESS,
type: 'AUTH_LOGIN_SUCCESS',
data,
});
},
@@ -35,7 +36,7 @@ const AuthActions = {
}
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LOGIN_ERROR,
type: 'AUTH_LOGIN_ERROR',
error: errorMessage,
});
@@ -50,19 +51,19 @@ const AuthActions = {
]);
}),
createUser: (credentials) =>
createUser: (config: UserConfig) =>
axios
.put(`${baseURI}auth/users`, credentials)
.then((json = {}) => json.data)
.put(`${baseURI}auth/users`, config)
.then((json) => json.data)
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_CREATE_USER_SUCCESS,
type: 'AUTH_CREATE_USER_SUCCESS',
data,
});
}),
updateUser: (username, connectionSettings) => {
const requestPayload = {};
updateUser: (username: Credentials['username'], connectionSettings: ConnectionSettings) => {
const requestPayload: Partial<Credentials> = {};
if (connectionSettings.connectionType === 'socket') {
requestPayload.socketPath = connectionSettings.rtorrentSocketPath;
@@ -73,17 +74,17 @@ const AuthActions = {
return axios
.patch(`${baseURI}auth/users/${encodeURIComponent(username)}`, requestPayload)
.then((json = {}) => json.data);
.then((json) => json.data);
},
deleteUser: (username) =>
deleteUser: (username: Credentials['username']) =>
axios
.delete(`${baseURI}auth/users/${encodeURIComponent(username)}`)
.then((json = {}) => json.data)
.then((json) => json.data)
.then(
(data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_DELETE_USER_SUCCESS,
type: 'AUTH_DELETE_USER_SUCCESS',
data: {
username,
...data,
@@ -92,7 +93,7 @@ const AuthActions = {
},
(error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_DELETE_USER_ERROR,
type: 'AUTH_DELETE_USER_ERROR',
error: {
username,
...error,
@@ -104,10 +105,10 @@ const AuthActions = {
fetchUsers: () =>
axios
.get(`${baseURI}auth/users`)
.then((json = {}) => json.data)
.then((json) => json.data)
.then((data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LIST_USERS_SUCCESS,
type: 'AUTH_LIST_USERS_SUCCESS',
data,
});
}),
@@ -116,31 +117,31 @@ const AuthActions = {
axios.get(`${baseURI}auth/logout`).then(
() => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LOGOUT_SUCCESS,
type: 'AUTH_LOGOUT_SUCCESS',
});
},
(error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_LOGOUT_ERROR,
type: 'AUTH_LOGOUT_ERROR',
error,
});
},
),
register: (credentials) =>
register: (config: UserConfig) =>
axios
.post(`${baseURI}auth/register`, credentials)
.then((json = {}) => json.data)
.post(`${baseURI}auth/register`, config)
.then((json) => json.data)
.then(
(data) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_REGISTER_SUCCESS,
type: 'AUTH_REGISTER_SUCCESS',
data,
});
},
(error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_REGISTER_ERROR,
type: 'AUTH_REGISTER_ERROR',
error: error.response.data.message,
});
},
@@ -149,11 +150,11 @@ const AuthActions = {
verify: () =>
axios
.get(`${baseURI}auth/verify?${Date.now()}`)
.then((json = {}) => json.data)
.then((json) => json.data)
.then(
(data) => {
(data: {isAdmin: boolean; success: boolean; initialUser?: boolean; token?: string; username: string}) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_VERIFY_SUCCESS,
type: 'AUTH_VERIFY_SUCCESS',
data,
});
@@ -163,7 +164,7 @@ const AuthActions = {
},
(error) => {
AppDispatcher.dispatchServerAction({
type: ActionTypes.AUTH_VERIFY_ERROR,
type: 'AUTH_VERIFY_ERROR',
error,
});

View File

@@ -9,7 +9,6 @@ import * as i18n from './i18n/languages';
import connectStores from './util/connectStores';
import AppWrapper from './components/AppWrapper';
import AuthActions from './actions/AuthActions';
import EventTypes from './constants/EventTypes';
import FloodActions from './actions/FloodActions';
import history from './util/history';
import Login from './components/views/Login';
@@ -52,7 +51,7 @@ const initialize = (): void => {
});
AuthActions.verify().then(
({initialUser}): void => {
({initialUser}: {initialUser?: boolean}): void => {
if (initialUser) {
history.replace('register');
} else {
@@ -99,10 +98,10 @@ const ConnectedFloodApp = connectStores(FloodApp, () => {
return [
{
store: SettingsStore,
event: EventTypes.SETTINGS_CHANGE,
event: 'SETTINGS_CHANGE',
getValue: () => {
return {
locale: SettingsStore.getFloodSettings('language'),
locale: SettingsStore.getFloodSetting('language'),
};
},
},

View File

@@ -1,6 +1,5 @@
import classnames from 'classnames';
import {CSSTransition, TransitionGroup} from 'react-transition-group';
import PropTypes from 'prop-types';
import React from 'react';
import AuthStore from '../stores/AuthStore';
@@ -9,22 +8,19 @@ import Checkmark from './icons/Checkmark';
import ClientConnectionInterruption from './general/ClientConnectionInterruption';
import ClientStatusStore from '../stores/ClientStatusStore';
import connectStores from '../util/connectStores';
import EventTypes from '../constants/EventTypes';
import LoadingIndicator from './general/LoadingIndicator';
import UIStore from '../stores/UIStore';
import WindowTitle from './general/WindowTitle';
import type {Dependencies} from '../stores/UIStore';
const ICONS = {
satisfied: <Checkmark />,
};
interface AuthEnforcerProps {
dependencies?: {
[key: string]: {
message: string;
satisfied: boolean;
};
};
children: React.ReactNode;
dependencies?: Dependencies;
dependenciesLoaded?: boolean;
isAuthenticated?: boolean;
isAuthenticating?: boolean;
@@ -32,10 +28,6 @@ interface AuthEnforcerProps {
}
class AuthEnforcer extends React.Component<AuthEnforcerProps> {
static propTypes = {
children: PropTypes.node,
};
isLoading() {
const {dependencies, dependenciesLoaded, isAuthenticated, isAuthenticating} = this.props;
// If the auth status is undetermined, show the loading indicator.
@@ -89,7 +81,7 @@ class AuthEnforcer extends React.Component<AuthEnforcerProps> {
const {dependencies} = this.props;
let listItems;
if (dependencies != null) {
listItems = Object.keys(dependencies).map((id) => {
listItems = Object.keys(dependencies).map((id: string) => {
const {message, satisfied} = dependencies[id];
const statusIcon = ICONS.satisfied;
const classes = classnames('dependency-list__dependency', {
@@ -123,12 +115,7 @@ const ConnectedAuthEnforcer = connectStores(AuthEnforcer, () => {
return [
{
store: AuthStore,
event: [
EventTypes.AUTH_LOGIN_SUCCESS,
EventTypes.AUTH_REGISTER_SUCCESS,
EventTypes.AUTH_VERIFY_SUCCESS,
EventTypes.AUTH_VERIFY_ERROR,
],
event: ['AUTH_LOGIN_SUCCESS', 'AUTH_REGISTER_SUCCESS', 'AUTH_VERIFY_SUCCESS', 'AUTH_VERIFY_ERROR'],
getValue: ({store}) => {
const storeAuth = store as typeof AuthStore;
return {
@@ -139,7 +126,7 @@ const ConnectedAuthEnforcer = connectStores(AuthEnforcer, () => {
},
{
store: UIStore,
event: EventTypes.UI_DEPENDENCIES_CHANGE,
event: 'UI_DEPENDENCIES_CHANGE',
getValue: ({store}) => {
const storeUI = store as typeof UIStore;
return {
@@ -149,7 +136,7 @@ const ConnectedAuthEnforcer = connectStores(AuthEnforcer, () => {
},
{
store: UIStore,
event: EventTypes.UI_DEPENDENCIES_LOADED,
event: 'UI_DEPENDENCIES_LOADED',
getValue: ({store}) => {
const storeUI = store as typeof UIStore;
return {
@@ -159,7 +146,7 @@ const ConnectedAuthEnforcer = connectStores(AuthEnforcer, () => {
},
{
store: ClientStatusStore,
event: EventTypes.CLIENT_CONNECTION_STATUS_CHANGE,
event: 'CLIENT_CONNECTION_STATUS_CHANGE',
getValue: ({store}) => {
const storeClientStatus = store as typeof ClientStatusStore;
return {

View File

@@ -1,17 +1,17 @@
import {FormattedMessage} from 'react-intl';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import CircleCheckmarkIcon from '../icons/CircleCheckmarkIcon';
import CircleExclamationIcon from '../icons/CircleExclamationIcon';
export default class Alert extends React.Component {
static propTypes = {
count: PropTypes.number,
id: PropTypes.string,
};
interface AlertProps {
id: string;
count: number;
type: 'success' | 'error';
}
export default class Alert extends React.Component<AlertProps> {
static defaultProps = {
count: 0,
type: 'success',

View File

@@ -4,17 +4,22 @@ import React from 'react';
import Alert from './Alert';
import AlertStore from '../../stores/AlertStore';
import connectStores from '../../util/connectStores';
import EventTypes from '../../constants/EventTypes';
class Alerts extends React.Component {
import type {Alert as AlertType} from '../../stores/AlertStore';
interface AlertsProps {
alerts?: Array<AlertType>;
}
class Alerts extends React.Component<AlertsProps> {
renderAlerts() {
const {alerts} = this.props;
if (alerts.length > 0) {
if (alerts != null && alerts.length > 0) {
return (
<CSSTransition classNames="alerts__list" timeout={{enter: 250, exit: 250}}>
<ul className="alerts__list" key="alerts-list">
{this.props.alerts.map((alert) => (
{alerts.map((alert) => (
<Alert {...alert} key={alert.id} />
))}
</ul>
@@ -34,10 +39,11 @@ const ConnectedAlerts = connectStores(Alerts, () => {
return [
{
store: AlertStore,
event: EventTypes.ALERTS_CHANGE,
event: 'ALERTS_CHANGE',
getValue: ({store}) => {
const storeAlert = store as typeof AlertStore;
return {
alerts: store.getAlerts(),
alerts: storeAlert.getAlerts(),
};
},
},

View File

@@ -1,4 +1,4 @@
import {injectIntl} from 'react-intl';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import {Button, Form, FormError, FormRow, Panel, PanelContent, PanelHeader, PanelFooter, Textbox} from '../../ui';
@@ -6,11 +6,23 @@ import AuthActions from '../../actions/AuthActions';
import AuthStore from '../../stores/AuthStore';
import connectStores from '../../util/connectStores';
import history from '../../util/history';
import EventTypes from '../../constants/EventTypes';
import RtorrentConnectionTypeSelection from '../general/RtorrentConnectionTypeSelection';
import RTorrentConnectionTypeSelection from '../general/RTorrentConnectionTypeSelection';
class AuthForm extends React.Component {
constructor(props) {
import type {Credentials, UserConfig} from '../../stores/AuthStore';
interface AuthFormProps extends WrappedComponentProps {
mode: 'login' | 'register';
error?: Error;
}
interface AuthFormStates {
isSubmitting: boolean;
}
class AuthForm extends React.Component<AuthFormProps, AuthFormStates> {
formRef?: Form | null;
constructor(props: AuthFormProps) {
super(props);
this.state = {
isSubmitting: false,
@@ -41,15 +53,27 @@ class AuthForm extends React.Component {
});
}
handleFormSubmit = (submission) => {
handleFormSubmit = (submission: {
event: Event | React.FormEvent<HTMLFormElement>;
formData: Record<string, unknown>;
}) => {
submission.event.preventDefault();
this.setState({isSubmitting: true});
if (this.props.mode === 'login') {
const credentials = submission.formData as Partial<Credentials>;
if (credentials.username == null || credentials.username === '') {
this.setState({isSubmitting: false}, () => {
// do nothing.
});
return;
}
AuthActions.authenticate({
username: submission.formData.username,
password: submission.formData.password,
username: credentials.username,
password: credentials.password,
})
.then(() => {
this.setState({isSubmitting: false}, () => history.replace('overview'));
@@ -58,12 +82,21 @@ class AuthForm extends React.Component {
this.setState({isSubmitting: false}, () => history.replace('login'));
});
} else {
const config = submission.formData as Partial<UserConfig>;
if (config.username == null || config.username === '') {
this.setState({isSubmitting: false}, () => {
// do nothing.
});
return;
}
AuthActions.register({
username: submission.formData.username,
password: submission.formData.password,
host: submission.formData.rtorrentHost,
port: submission.formData.rtorrentPort,
socketPath: submission.formData.rtorrentSocketPath,
username: config.username,
password: config.password,
host: config.rtorrentHost,
port: config.rtorrentPort,
socketPath: config.rtorrentSocketPath,
isAdmin: true,
}).then(() => {
this.setState({isSubmitting: false}, () => history.replace('overview'));
@@ -103,12 +136,18 @@ class AuthForm extends React.Component {
</PanelContent>
{isLoginMode ? null : (
<PanelContent hasBorder>
<RtorrentConnectionTypeSelection />
<RTorrentConnectionTypeSelection />
</PanelContent>
)}
<PanelFooter hasBorder>
<FormRow justify="end">
<Button priority="tertiary" onClick={() => this.formRef.resetForm()}>
<Button
priority="tertiary"
onClick={() => {
if (this.formRef != null) {
this.formRef.resetForm();
}
}}>
Clear
</Button>
<Button isLoading={this.state.isSubmitting} type="submit">
@@ -129,14 +168,14 @@ class AuthForm extends React.Component {
}
}
const ConnectedAuthForm = connectStores(injectIntl(AuthForm), () => {
const ConnectedAuthForm = connectStores<Omit<AuthFormProps, 'intl'>, AuthFormStates>(injectIntl(AuthForm), () => {
return [
{
store: AuthStore,
event: [EventTypes.AUTH_LOGIN_ERROR, EventTypes.AUTH_REGISTER_ERROR],
event: ['AUTH_LOGIN_ERROR', 'AUTH_REGISTER_ERROR'],
getValue: ({payload}) => {
return {
error: payload,
error: payload as AuthFormProps['error'],
};
},
},

View File

@@ -7,12 +7,27 @@ import AuthStore from '../../stores/AuthStore';
import Checkmark from '../icons/Checkmark';
import ClientActions from '../../actions/ClientActions';
import connectStores from '../../util/connectStores';
import EventTypes from '../../constants/EventTypes';
import FloodActions from '../../actions/FloodActions';
import RtorrentConnectionTypeSelection from './RtorrentConnectionTypeSelection';
import RTorrentConnectionTypeSelection from './RTorrentConnectionTypeSelection';
class ClientConnectionInterruption extends React.Component {
constructor(props) {
interface ClientConnectionInterruptionProps {
isAdmin?: boolean;
isInitialUser?: boolean;
}
interface ClientConnectionInterruptionStates {
hasTestedConnection: boolean;
isConnectionVerified: boolean;
isTestingConnection: boolean;
}
class ClientConnectionInterruption extends React.Component<
ClientConnectionInterruptionProps,
ClientConnectionInterruptionStates
> {
formRef?: Form | null;
constructor(props: ClientConnectionInterruptionProps) {
super(props);
this.state = {
hasTestedConnection: false,
@@ -30,8 +45,14 @@ class ClientConnectionInterruption extends React.Component {
}
};
handleFormSubmit = ({formData}) => {
AuthActions.updateUser(AuthStore.getCurrentUsername(), formData)
handleFormSubmit = ({formData}: {formData: Record<string, unknown>}) => {
const currentUsername = AuthStore.getCurrentUsername();
if (currentUsername == null) {
return;
}
AuthActions.updateUser(currentUsername, formData)
.then(() => {
FloodActions.restartActivityStream();
})
@@ -41,7 +62,7 @@ class ClientConnectionInterruption extends React.Component {
};
handleTestButtonClick = () => {
if (this.state.isTestingConnection) return;
if (this.state.isTestingConnection || this.formRef == null) return;
const formData = this.formRef.getFormData();
this.setState(
@@ -133,7 +154,7 @@ class ClientConnectionInterruption extends React.Component {
<FormattedMessage id="connection-interruption.verify-settings-prompt" />
</p>
{this.renderFormError()}
<RtorrentConnectionTypeSelection isDisabled={isTestingConnection} />
<RTorrentConnectionTypeSelection />
</PanelContent>
<PanelFooter hasBorder>
<FormRow justify="end">
@@ -156,16 +177,12 @@ const ConnectedClientConnectionInterruption = connectStores(ClientConnectionInte
return [
{
store: AuthStore,
event: [
EventTypes.AUTH_LOGIN_SUCCESS,
EventTypes.AUTH_REGISTER_SUCCESS,
EventTypes.AUTH_VERIFY_SUCCESS,
EventTypes.AUTH_VERIFY_ERROR,
],
event: ['AUTH_LOGIN_SUCCESS', 'AUTH_REGISTER_SUCCESS', 'AUTH_VERIFY_SUCCESS', 'AUTH_VERIFY_ERROR'],
getValue: ({store}) => {
const storeAuth = store as typeof AuthStore;
return {
isAdmin: store.isAdmin(),
isInitialUser: store.getIsInitialUser(),
isAdmin: storeAuth.isAdmin(),
isInitialUser: storeAuth.getIsInitialUser(),
};
},
},

View File

@@ -1,38 +1,45 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import {ContextMenu} from '../../ui';
import EventTypes from '../../constants/EventTypes';
import UIActions from '../../actions/UIActions';
import UIStore from '../../stores/UIStore';
class GlobalContextMenuMountPoint extends React.Component {
static propTypes = {
onMenuClose: PropTypes.func,
onMenuOpen: PropTypes.func,
id: PropTypes.string.isRequired,
width: PropTypes.number,
};
import type {ContextMenu as ContextMenuType, ContextMenuItem} from '../../stores/UIStore';
static defaultProps = {
width: 200,
};
interface GlobalContextMenuMountPointProps {
id: ContextMenuType['id'];
onMenuOpen: () => void;
onMenuClose: () => void;
}
constructor(props) {
interface GlobalContextMenuMountPointStates {
clickPosition: ContextMenuType['clickPosition'];
isOpen: boolean;
items: ContextMenuType['items'];
}
class GlobalContextMenuMountPoint extends React.Component<
GlobalContextMenuMountPointProps,
GlobalContextMenuMountPointStates
> {
constructor(props: GlobalContextMenuMountPointProps) {
super(props);
this.state = {
clickPosition: {},
clickPosition: {
x: 0,
y: 0,
},
isOpen: false,
items: [],
};
}
componentDidMount() {
UIStore.listen(EventTypes.UI_CONTEXT_MENU_CHANGE, this.handleContextMenuChange);
UIStore.listen('UI_CONTEXT_MENU_CHANGE', this.handleContextMenuChange);
}
shouldComponentUpdate(nextProps, nextState) {
shouldComponentUpdate(_nextProps: GlobalContextMenuMountPointProps, nextState: GlobalContextMenuMountPointStates) {
if (!this.state.isOpen && !nextState.isOpen) {
return false;
}
@@ -57,19 +64,15 @@ class GlobalContextMenuMountPoint extends React.Component {
return shouldUpdate;
}
componentWillUnmount() {
UIStore.unlisten(EventTypes.UI_CONTEXT_MENU_CHANGE, this.handleContextMenuChange);
}
comnponentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps: GlobalContextMenuMountPointProps, prevState: GlobalContextMenuMountPointStates) {
if (!prevState.isOpen && this.state.isOpen) {
global.document.addEventListener('keydown', this.handleKeyPress);
document.addEventListener('keydown', this.handleKeyPress);
if (prevProps.onMenuOpen) {
prevProps.onMenuOpen();
}
} else if (prevState.isOpen && !this.state.isOpen) {
global.document.removeEventListener('keydown', this.handleKeyPress);
document.removeEventListener('keydown', this.handleKeyPress);
if (prevProps.onMenuClose) {
prevProps.onMenuClose();
@@ -77,6 +80,10 @@ class GlobalContextMenuMountPoint extends React.Component {
}
}
componentWillUnmount() {
UIStore.unlisten('UI_CONTEXT_MENU_CHANGE', this.handleContextMenuChange);
}
getMenuItems() {
return this.state.items.map((item, index) => {
let labelAction;
@@ -137,13 +144,13 @@ class GlobalContextMenuMountPoint extends React.Component {
}
};
handleKeyPress = (event) => {
handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
UIActions.dismissContextMenu(this.props.id);
}
};
handleMenuItemClick(item, event) {
handleMenuItemClick(item: ContextMenuItem, event: React.MouseEvent<HTMLLIElement>) {
if (item.dismissMenu === false) {
event.nativeEvent.stopImmediatePropagation();
}

View File

@@ -1,33 +1,49 @@
import {FormattedMessage, injectIntl} from 'react-intl';
import PropTypes from 'prop-types';
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import React, {Component} from 'react';
import {FormGroup, FormRow, Radio, Textbox} from '../../ui';
class RtorrentConnectionTypeSelection extends Component {
static propTypes = {
onChange: PropTypes.func,
};
interface RTorrentConnectionTypeSelectionProps extends WrappedComponentProps {
onChange?: (connectionType: RTorrentConnectionTypeSelectionStates['connectionType']) => void;
}
interface RTorrentConnectionTypeSelectionStates {
connectionType: 'tcp' | 'socket';
}
class RTorrentConnectionTypeSelection extends Component<
RTorrentConnectionTypeSelectionProps,
RTorrentConnectionTypeSelectionStates
> {
static defaultProps = {
onChange: () => {
// do nothing.
},
};
constructor(props) {
constructor(props: RTorrentConnectionTypeSelectionProps) {
super(props);
this.state = {
connectionType: 'tcp',
};
}
handleTypeChange = (event) => {
if (this.state.connectionType !== event.target.value) {
this.setState({connectionType: event.target.value});
handleTypeChange(event: React.MouseEvent<HTMLInputElement> | KeyboardEvent) {
const inputElement = event.target as HTMLInputElement;
if (inputElement == null) {
return;
}
this.props.onChange(event.target.value);
};
const connectionType = inputElement.value as RTorrentConnectionTypeSelectionStates['connectionType'];
if (this.state.connectionType !== connectionType) {
this.setState({connectionType});
}
if (typeof this.props.onChange === 'function') {
this.props.onChange(connectionType);
}
}
renderConnectionOptions() {
if (this.state.connectionType === 'tcp') {
@@ -100,4 +116,4 @@ class RtorrentConnectionTypeSelection extends Component {
}
}
export default injectIntl(RtorrentConnectionTypeSelection);
export default injectIntl(RTorrentConnectionTypeSelection);

View File

@@ -1,15 +1,22 @@
import {FormattedNumber, injectIntl} from 'react-intl';
import {FormattedNumber, injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import {compute, getTranslationString} from '../../util/size';
class Size extends React.Component {
interface SizeProps extends WrappedComponentProps {
value: number;
precision?: number;
isSpeed?: boolean;
className?: string;
}
class Size extends React.Component<SizeProps> {
static defaultProps = {
isSpeed: false,
precision: 2,
};
renderNumber(computedNumber) {
renderNumber(computedNumber: ReturnType<typeof compute>) {
if (Number.isNaN(computedNumber.value)) {
return '—';
}

View File

@@ -1,18 +1,23 @@
import {injectIntl} from 'react-intl';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import connectStores from '../../util/connectStores';
import {compute, getTranslationString} from '../../util/size';
import EventTypes from '../../constants/EventTypes';
import TransferDataStore from '../../stores/TransferDataStore';
const WindowTitle = (props) => {
import type {TransferSummary} from '../../stores/TransferDataStore';
interface WindowTitleProps extends WrappedComponentProps {
summary?: TransferSummary;
}
const WindowTitleFunc = (props: WindowTitleProps) => {
const {intl, summary} = props;
React.useEffect(() => {
let title = 'Flood';
if (Object.keys(summary).length > 0) {
if (summary != null && Object.keys(summary).length > 0) {
const down = compute(summary.downRate);
const up = compute(summary.upRate);
@@ -54,18 +59,19 @@ const WindowTitle = (props) => {
return null;
};
const ConnectedWindowTitle = connectStores(injectIntl(WindowTitle), () => {
const WindowTitle = connectStores(injectIntl(WindowTitleFunc), () => {
return [
{
store: TransferDataStore,
event: EventTypes.CLIENT_TRANSFER_SUMMARY_CHANGE,
event: 'CLIENT_TRANSFER_SUMMARY_CHANGE',
getValue: ({store}) => {
const storeTransferData = store as typeof TransferDataStore;
return {
summary: store.getTransferSummary(),
summary: storeTransferData.getTransferSummary(),
};
},
},
];
});
export default ConnectedWindowTitle;
export default WindowTitle;

View File

@@ -1,28 +1,39 @@
import _ from 'lodash';
import {FormattedMessage, injectIntl} from 'react-intl';
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import {Checkbox, ContextMenu, FormElementAddon, FormRow, FormRowGroup, Portal, Textbox} from '../../../ui';
import EventTypes from '../../../constants/EventTypes';
import FilesystemBrowser from './FilesystemBrowser';
import Search from '../../icons/Search';
import SettingsStore from '../../../stores/SettingsStore';
import UIStore from '../../../stores/UIStore';
class TorrentDestination extends React.Component {
contextMenuInstanceRef = null;
interface TorrentDestinationProps extends WrappedComponentProps {
id: string;
label?: React.ReactNode;
suggested?: string;
onChange?: (destination: string) => void;
}
contextMenuNodeRef = null;
interface TorrentDestinationStates {
destination: string;
isDirectoryListOpen: boolean;
}
textboxRef = null;
class TorrentDestination extends React.Component<TorrentDestinationProps, TorrentDestinationStates> {
contextMenuInstanceRef: ContextMenu | null = null;
constructor(props) {
contextMenuNodeRef: HTMLDivElement | null = null;
textboxRef: HTMLInputElement | null = null;
constructor(props: TorrentDestinationProps) {
super(props);
const destination =
const destination: string =
props.suggested ||
SettingsStore.getFloodSettings('torrentDestination') ||
SettingsStore.getClientSettings('directoryDefault') ||
SettingsStore.getFloodSetting('torrentDestination') ||
(SettingsStore.getClientSetting('directoryDefault') as string | undefined) ||
'';
this.state = {
@@ -32,13 +43,13 @@ class TorrentDestination extends React.Component {
}
componentDidMount() {
UIStore.listen(EventTypes.UI_MODAL_DISMISSED, this.handleModalDismiss);
UIStore.listen('UI_MODAL_DISMISSED', this.handleModalDismiss);
// TODO: Fix ContextMenu in flood-ui-kit and remove the forced double render
// https://github.com/jfurrow/flood-ui-kit/issues/6
this.forceUpdate();
}
componentDidUpdate(_prevProps, prevState) {
componentDidUpdate(_prevProps: TorrentDestinationProps, prevState: TorrentDestinationStates) {
if (!prevState.isDirectoryListOpen && this.state.isDirectoryListOpen) {
this.addDestinationOpenEventListeners();
} else if (prevState.isDirectoryListOpen && !this.state.isDirectoryListOpen) {
@@ -47,13 +58,13 @@ class TorrentDestination extends React.Component {
}
componentWillUnmount() {
UIStore.unlisten(EventTypes.UI_MODAL_DISMISSED, this.handleModalDismiss);
UIStore.unlisten('UI_MODAL_DISMISSED', this.handleModalDismiss);
this.removeDestinationOpenEventListeners();
}
addDestinationOpenEventListeners() {
global.document.addEventListener('click', this.handleDocumentClick);
global.addEventListener('resize', this.handleWindowResize);
document.addEventListener('click', this.handleDocumentClick);
window.addEventListener('resize', this.handleWindowResize);
}
closeDirectoryList = () => {
@@ -65,6 +76,10 @@ class TorrentDestination extends React.Component {
/* eslint-disable react/sort-comp */
handleDestinationInputChange = _.debounce(
() => {
if (this.textboxRef == null) {
return;
}
const destination = this.textboxRef.value;
if (this.props.onChange) {
@@ -88,8 +103,10 @@ class TorrentDestination extends React.Component {
});
};
handleDirectorySelection = (destination) => {
this.textboxRef.value = destination;
handleDirectorySelection = (destination: string) => {
if (this.textboxRef != null) {
this.textboxRef.value = destination;
}
this.setState({destination});
};
@@ -106,8 +123,8 @@ class TorrentDestination extends React.Component {
};
removeDestinationOpenEventListeners() {
global.document.removeEventListener('click', this.handleDocumentClick);
global.removeEventListener('resize', this.handleWindowResize);
document.removeEventListener('click', this.handleDocumentClick);
window.removeEventListener('resize', this.handleWindowResize);
}
toggleOpenState = () => {
@@ -178,4 +195,4 @@ class TorrentDestination extends React.Component {
}
}
export default injectIntl(TorrentDestination, {withRef: true});
export default injectIntl(TorrentDestination, {forwardRef: true});

View File

@@ -1,36 +1,53 @@
import _ from 'lodash';
import classnames from 'classnames';
import {CSSTransition, TransitionGroup} from 'react-transition-group';
import PropTypes from 'prop-types';
import React from 'react';
import EventTypes from '../../../constants/EventTypes';
import UIActions from '../../../actions/UIActions';
import UIStore from '../../../stores/UIStore';
export interface DropdownItem {
className?: string;
displayName: React.ReactNode;
selectable: boolean;
selected?: boolean;
property?: string;
value?: number | null;
}
type DropdownItems = Array<DropdownItem>;
interface DropdownProps {
header: React.ReactNode;
trigger?: React.ReactNode;
dropdownButtonClass?: string;
menuItems: Array<DropdownItems>;
handleItemSelect: (item: DropdownItem) => void;
onOpen?: () => void;
dropdownWrapperClass?: string;
baseClassName?: string;
direction?: 'down' | 'up';
width?: 'small' | 'medium' | 'large';
matchButtonWidth?: boolean;
noWrap?: boolean;
}
interface DropdownStates {
isOpen: boolean;
}
const METHODS_TO_BIND = [
'closeDropdown',
'openDropdown',
'getDropdownButton',
'getDropdownMenu',
'getDropdownMenuItems',
'handleActiveDropdownChange',
'handleDropdownClick',
'handleItemSelect',
'handleKeyPress',
];
] as const;
class Dropdown extends React.Component {
static propTypes = {
direction: PropTypes.oneOf(['down', 'up']),
header: PropTypes.node,
trigger: PropTypes.node,
matchButtonWidth: PropTypes.bool,
menuItems: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object)).isRequired,
noWrap: PropTypes.bool,
onOpen: PropTypes.func,
width: PropTypes.oneOf(['small', 'medium', 'large']),
};
class Dropdown extends React.Component<DropdownProps, DropdownStates> {
id = _.uniqueId('dropdown_');
static defaultProps = {
baseClassName: 'dropdown',
@@ -41,34 +58,32 @@ class Dropdown extends React.Component {
noWrap: false,
};
constructor() {
super();
this.id = _.uniqueId('dropdown_');
constructor(props: DropdownProps) {
super(props);
this.state = {
isOpen: false,
};
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
METHODS_TO_BIND.forEach(<T extends typeof METHODS_TO_BIND[number]>(methodName: T) => {
this[methodName] = this[methodName].bind(this);
});
this.handleKeyPress = _.throttle(this.handleKeyPress, 200);
}
closeDropdown() {
global.removeEventListener('keydown', this.handleKeyPress);
global.removeEventListener('click', this.closeDropdown);
UIStore.unlisten(EventTypes.UI_DROPDOWN_MENU_CHANGE, this.handleActiveDropdownChange);
window.removeEventListener('keydown', this.handleKeyPress);
window.removeEventListener('click', this.closeDropdown);
UIStore.unlisten('UI_DROPDOWN_MENU_CHANGE', this.handleActiveDropdownChange);
this.setState({isOpen: false});
}
openDropdown() {
global.addEventListener('keydown', this.handleKeyPress);
global.addEventListener('click', this.closeDropdown);
UIStore.listen(EventTypes.UI_DROPDOWN_MENU_CHANGE, this.handleActiveDropdownChange);
window.addEventListener('keydown', this.handleKeyPress);
window.addEventListener('click', this.closeDropdown);
UIStore.listen('UI_DROPDOWN_MENU_CHANGE', this.handleActiveDropdownChange);
this.setState({isOpen: true});
@@ -79,7 +94,7 @@ class Dropdown extends React.Component {
UIActions.displayDropdownMenu(this.id);
}
handleDropdownClick(event) {
handleDropdownClick(event: React.MouseEvent<HTMLDivElement>) {
event.stopPropagation();
if (this.state.isOpen) {
@@ -95,18 +110,18 @@ class Dropdown extends React.Component {
}
}
handleItemSelect(item) {
handleItemSelect(item: DropdownItem) {
this.closeDropdown();
this.props.handleItemSelect(item);
}
handleKeyPress(event) {
handleKeyPress(event: KeyboardEvent) {
if (this.state.isOpen && event.keyCode === 27) {
this.closeDropdown();
}
}
getDropdownButton(options = {}) {
private getDropdownButton(options: {header?: boolean; trigger?: boolean} = {}) {
let label = this.props.header;
if (options.trigger && !!this.props.trigger) {
@@ -120,7 +135,7 @@ class Dropdown extends React.Component {
);
}
getDropdownMenu(items) {
private getDropdownMenu(items: Array<DropdownItems>) {
// TODO: Rewrite this function, wtf was I thinking
const arrayMethod = this.props.direction === 'up' ? 'unshift' : 'push';
const content = [
@@ -149,13 +164,13 @@ class Dropdown extends React.Component {
);
}
getDropdownMenuItems(listItems) {
private getDropdownMenuItems(listItems: DropdownItems) {
return listItems.map((property, index) => {
const classes = classnames('dropdown__item menu__item', property.className, {
'is-selectable': property.selectable !== false,
'is-selected': property.selected,
});
let clickHandler = null;
let clickHandler;
if (property.selectable !== false) {
clickHandler = this.handleItemSelect.bind(this, property);
@@ -178,12 +193,12 @@ class Dropdown extends React.Component {
{
[`${this.props.baseClassName}--match-button-width`]: this.props.matchButtonWidth,
[`${this.props.baseClassName}--width-${this.props.width}`]: this.props.width != null,
[`${this.props.baseClassName}--no-wrap`]: this.props.nowrap,
[`${this.props.baseClassName}--no-wrap`]: this.props.noWrap,
'is-expanded': this.state.isOpen,
},
);
let menu = null;
let menu: React.ReactNode = null;
if (this.state.isOpen) {
menu = this.getDropdownMenu(this.props.menuItems);

View File

@@ -4,10 +4,23 @@ import {FormElementAddon, FormRow, FormRowGroup, Textbox} from '../../../ui';
import AddMini from '../../icons/AddMini';
import RemoveMini from '../../icons/RemoveMini';
export default class TextboxRepeater extends React.PureComponent {
export type Textboxes = Array<{id: number; value: string}>;
interface TextboxRepeaterProps {
defaultValues?: Textboxes;
id: number | string;
label?: string;
placeholder?: string;
}
interface TextboxRepeaterStates {
textboxes: Textboxes;
}
export default class TextboxRepeater extends React.PureComponent<TextboxRepeaterProps, TextboxRepeaterStates> {
idCounter = 0;
constructor(props) {
constructor(props: TextboxRepeaterProps) {
super(props);
this.state = {
textboxes: this.props.defaultValues || [{id: 0, value: ''}],
@@ -54,7 +67,7 @@ export default class TextboxRepeater extends React.PureComponent {
);
});
handleTextboxAdd = (index) => {
handleTextboxAdd = (index: number) => {
this.setState((state) => {
const textboxes = Object.assign([], state.textboxes);
textboxes.splice(index + 1, 0, {id: this.getID(), value: ''});
@@ -62,7 +75,7 @@ export default class TextboxRepeater extends React.PureComponent {
});
};
handleTextboxRemove = (index) => {
handleTextboxRemove = (index: number) => {
this.setState((state) => {
const textboxes = Object.assign([], state.textboxes);
textboxes.splice(index, 1);

View File

@@ -1,12 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
export default class BaseIcon extends React.Component {
static propTypes = {
size: PropTypes.string,
viewBox: PropTypes.string,
};
interface BaseIconProps {
className?: string;
size?: string;
viewBox?: string;
}
export default class BaseIcon extends React.Component<BaseIconProps> {
static defaultProps = {
className: '',
viewBox: '0 0 60 60',

View File

@@ -4,11 +4,7 @@ import React from 'react';
import BaseIcon from './BaseIcon';
export default class SpinnerIcon extends BaseIcon {
constructor(...iconConfig) {
super(...iconConfig);
this.id = _.uniqueId();
}
id = _.uniqueId();
getViewBox() {
return '0 0 128 128';

View File

@@ -1,11 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
class ApplicationContent extends React.Component {
static propTypes = {
children: PropTypes.node,
};
render() {
return <div className="application__content">{this.props.children}</div>;
}

View File

@@ -1,14 +1,13 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
class ApplicationContent extends React.Component {
static propTypes = {
children: PropTypes.node,
className: PropTypes.string,
modifier: PropTypes.string,
};
interface ApplicationContentProps {
baseClassName: string;
className: string;
modifier: string;
}
class ApplicationContent extends React.Component<ApplicationContentProps> {
static defaultProps = {
baseClassName: 'application__panel',
};

View File

@@ -1,13 +1,11 @@
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
class ApplicationView extends React.Component {
static propTypes = {
children: PropTypes.node,
modifier: PropTypes.string,
};
interface ApplicationViewProps {
modifier?: string;
}
class ApplicationView extends React.Component<ApplicationViewProps> {
render() {
const classes = classnames('application__view', {
[`application__view--${this.props.modifier}`]: this.props.modifier != null,

View File

@@ -6,7 +6,7 @@ import SettingsStore from '../../../stores/SettingsStore';
class AddTorrentsActions extends PureComponent {
getActions() {
const startTorrentsOnLoad = SettingsStore.getFloodSettings('startTorrentsOnLoad');
const startTorrentsOnLoad = SettingsStore.getFloodSetting('startTorrentsOnLoad');
return [
{
checked: startTorrentsOnLoad === 'true' || startTorrentsOnLoad === true,

View File

@@ -118,7 +118,7 @@ class AddTorrentsByFile extends React.Component {
fileData.append('start', start);
TorrentActions.addTorrentsByFiles(fileData, destination);
SettingsStore.updateOptimisticallyOnly({id: 'startTorrentsOnLoad', data: start});
SettingsStore.setFloodSetting('startTorrentsOnLoad', start);
};
render() {
@@ -154,4 +154,4 @@ class AddTorrentsByFile extends React.Component {
}
}
export default injectIntl(AddTorrentsByFile, {withRef: true});
export default injectIntl(AddTorrentsByFile, {forwardRef: true});

View File

@@ -1,4 +1,4 @@
import {injectIntl} from 'react-intl';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import {Form, FormRow, Textbox} from '../../../ui';
@@ -11,11 +11,32 @@ import TorrentActions from '../../../actions/TorrentActions';
import TorrentDestination from '../../general/filesystem/TorrentDestination';
import UIStore from '../../../stores/UIStore';
class AddTorrentsByURL extends React.Component {
formRef = null;
import type {Textboxes} from '../../general/form-elements/TextboxRepeater';
constructor() {
super();
type AddTorrentsByURLFormData = {
[urls: string]: string;
} & {
destination: string;
useBasePath: boolean;
start: boolean;
tags: string;
};
interface AddTorrentsByURLProps extends WrappedComponentProps {
dismissModal: () => void;
}
interface AddTorrentsByURLStates {
isAddingTorrents: boolean;
tags: string;
urlTextboxes: Textboxes;
}
class AddTorrentsByURL extends React.Component<AddTorrentsByURLProps, AddTorrentsByURLStates> {
formRef: Form | null = null;
constructor(props: AddTorrentsByURLProps) {
super(props);
const activeModal = UIStore.getActiveModal();
const initialUrls = activeModal ? activeModal.torrents : null;
@@ -23,15 +44,22 @@ class AddTorrentsByURL extends React.Component {
this.state = {
isAddingTorrents: false,
tags: '',
urlTextboxes: initialUrls || [{id: 0, value: ''}],
urlTextboxes: (initialUrls as Textboxes) || [{id: 0, value: ''}],
};
}
getURLsFromForm() {
const formData = this.formRef.getFormData();
return Object.keys(formData).reduce((accumulator, formItemKey) => {
if (this.formRef == null) {
return [];
}
const formData = this.formRef.getFormData() as Partial<AddTorrentsByURLFormData>;
return Object.keys(formData).reduce((accumulator: Array<string>, formItemKey: string) => {
if (/^urls/.test(formItemKey)) {
accumulator.push(formData[formItemKey]);
const url = formData[formItemKey];
if (url != null) {
accumulator.push(url);
}
}
return accumulator;
@@ -39,7 +67,11 @@ class AddTorrentsByURL extends React.Component {
}
handleAddTorrents = () => {
const formData = this.formRef.getFormData();
if (this.formRef == null) {
return;
}
const formData = this.formRef.getFormData() as Partial<AddTorrentsByURLFormData>;
this.setState({isAddingTorrents: true});
TorrentActions.addTorrentsByUrls({
@@ -47,13 +79,10 @@ class AddTorrentsByURL extends React.Component {
destination: formData.destination,
isBasePath: formData.useBasePath,
start: formData.start,
tags: formData.tags.split(','),
tags: formData.tags != null ? formData.tags.split(',') : undefined,
});
SettingsStore.updateOptimisticallyOnly({
id: 'startTorrentsOnLoad',
data: formData.start,
});
SettingsStore.setFloodSetting('startTorrentsOnLoad', Boolean(formData.start));
};
render() {
@@ -98,4 +127,4 @@ class AddTorrentsByURL extends React.Component {
}
}
export default injectIntl(AddTorrentsByURL, {withRef: true});
export default injectIntl(AddTorrentsByURL, {forwardRef: true});

View File

@@ -1,5 +1,5 @@
import _ from 'lodash';
import {defineMessages, FormattedMessage, injectIntl} from 'react-intl';
import {defineMessages, FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import {
@@ -19,12 +19,33 @@ import connectStores from '../../../util/connectStores';
import Edit from '../../icons/Edit';
import Checkmark from '../../icons/Checkmark';
import Close from '../../icons/Close';
import EventTypes from '../../../constants/EventTypes';
import FeedMonitorStore from '../../../stores/FeedMonitorStore';
import FeedsStore from '../../../stores/FeedsStore';
import ModalFormSectionHeader from '../ModalFormSectionHeader';
import TorrentDestination from '../../general/filesystem/TorrentDestination';
import * as validators from '../../../util/validators';
import type {Feeds, Rule, Rules} from '../../../stores/FeedsStore';
type ValidatedFields = 'destination' | 'feedID' | 'label' | 'match' | 'exclude';
interface RuleFormData extends Omit<Rule, 'tags'> {
check: string;
tags: string;
}
interface DownloadRulesTabProps extends WrappedComponentProps {
feeds: Feeds;
rules: Rules;
}
interface DownloadRulesTabStates {
errors?: {
[field in ValidatedFields]?: string;
};
currentlyEditingRule: Rule | null;
doesPatternMatchTest: boolean;
}
const MESSAGES = defineMessages({
mustSpecifyDestination: {
id: 'feeds.validation.must.specify.destination',
@@ -65,7 +86,9 @@ const defaultRule = {
startOnLoad: false,
};
class DownloadRulesTab extends React.Component {
class DownloadRulesTab extends React.Component<DownloadRulesTabProps, DownloadRulesTabStates> {
formRef: Form | null = null;
validatedFields = {
destination: {
isValid: validators.isNotEmpty,
@@ -80,11 +103,11 @@ class DownloadRulesTab extends React.Component {
error: this.props.intl.formatMessage(MESSAGES.mustSpecifyLabel),
},
match: {
isValid: (value) => validators.isNotEmpty(value) && validators.isRegExValid(value),
isValid: (value: string) => validators.isNotEmpty(value) && validators.isRegExValid(value),
error: this.props.intl.formatMessage(MESSAGES.invalidRegularExpression),
},
exclude: {
isValid: (value) => {
isValid: (value: string) => {
if (validators.isNotEmpty(value)) {
return validators.isRegExValid(value);
}
@@ -95,18 +118,20 @@ class DownloadRulesTab extends React.Component {
},
};
formRef = null;
checkFieldValidity = _.throttle((fieldName, fieldValue) => {
checkFieldValidity = _.throttle((fieldName: ValidatedFields, fieldValue) => {
const {errors} = this.state;
if (errors == null) {
return;
}
if (errors[fieldName] && this.validatedFields[fieldName].isValid(fieldValue)) {
delete errors[fieldName];
this.setState({errors});
}
}, 150);
constructor(props) {
constructor(props: DownloadRulesTabProps) {
super(props);
this.state = {
errors: {},
@@ -115,7 +140,7 @@ class DownloadRulesTab extends React.Component {
};
}
checkMatchingPattern(match, exclude, check) {
checkMatchingPattern(match: RuleFormData['match'], exclude: RuleFormData['exclude'], check: RuleFormData['check']) {
let doesPatternMatchTest = false;
if (validators.isNotEmpty(check) && validators.isRegExValid(match) && validators.isRegExValid(exclude)) {
@@ -127,11 +152,30 @@ class DownloadRulesTab extends React.Component {
this.setState({doesPatternMatchTest});
}
getAmendedFormData() {
const formData = this.formRef.getFormData();
getAmendedFormData(): Rule | null {
if (this.formRef == null) {
return null;
}
const formData = this.formRef.getFormData() as Partial<RuleFormData>;
if (formData == null) {
return null;
}
delete formData.check;
return {...formData, field: 'title', tags: formData.tags.split(',')};
return {
...defaultRule,
...formData,
field: 'title',
...(formData.tags != null
? {
tags: formData.tags.split(','),
}
: {
tags: [],
}),
};
}
getAvailableFeedsOptions() {
@@ -150,7 +194,7 @@ class DownloadRulesTab extends React.Component {
return feeds.reduce(
(feedOptions, feed) =>
feedOptions.concat(
<SelectItem key={feed._id} id={feed._id}>
<SelectItem key={feed._id} id={`${feed._id}`}>
{feed.label}
</SelectItem>,
),
@@ -164,7 +208,7 @@ class DownloadRulesTab extends React.Component {
);
}
getModifyRuleForm(rule) {
getModifyRuleForm(rule: Rule) {
const {doesPatternMatchTest, currentlyEditingRule} = this.state;
return (
@@ -255,7 +299,7 @@ class DownloadRulesTab extends React.Component {
);
}
getRulesListItem(rule) {
getRulesListItem(rule: Rule) {
const matchedCount = rule.count || 0;
let excludeNode = null;
let tags = null;
@@ -349,9 +393,21 @@ class DownloadRulesTab extends React.Component {
return <ul className="interactive-list">{rulesList}</ul>;
}
handleFormChange = ({event, formData}) => {
this.checkFieldValidity(event.target.name, formData[event.target.name]);
this.checkMatchingPattern(formData.match, formData.exclude, formData.check);
handleFormChange = ({
event,
formData,
}: {
event: Event | React.FormEvent<HTMLFormElement>;
formData: Record<string, unknown>;
}) => {
const validatedField = (event.target as HTMLInputElement).name as ValidatedFields;
const ruleFormData = formData as Partial<RuleFormData>;
this.checkFieldValidity(validatedField, ruleFormData[validatedField]);
this.checkMatchingPattern(
ruleFormData.match != null ? ruleFormData.match : defaultRule.match,
ruleFormData.exclude != null ? ruleFormData.exclude : defaultRule.exclude,
ruleFormData.check != null ? ruleFormData.check : '',
);
};
handleFormSubmit = () => {
@@ -363,17 +419,25 @@ class DownloadRulesTab extends React.Component {
const currentRule = this.state.currentlyEditingRule;
const formData = this.getAmendedFormData();
if (currentRule !== null && currentRule !== defaultRule) {
FeedMonitorStore.removeRule(currentRule._id);
if (formData != null) {
if (currentRule !== null && currentRule !== defaultRule && currentRule._id != null) {
FeedsStore.removeRule(currentRule._id);
}
FeedsStore.addRule(formData);
}
FeedMonitorStore.addRule(formData);
this.formRef.resetForm();
if (this.formRef != null) {
this.formRef.resetForm();
}
this.setState({currentlyEditingRule: null});
}
};
handleRemoveRuleClick(rule) {
FeedMonitorStore.removeRule(rule._id);
handleRemoveRuleClick(rule: Rule) {
if (rule._id != null) {
FeedsStore.removeRule(rule._id);
}
if (rule === this.state.currentlyEditingRule) {
this.setState({currentlyEditingRule: null});
@@ -384,32 +448,50 @@ class DownloadRulesTab extends React.Component {
this.setState({currentlyEditingRule: defaultRule});
};
handleModifyRuleClick(rule) {
handleModifyRuleClick(rule: Rule) {
this.setState({currentlyEditingRule: rule});
}
validateForm() {
validateForm(): {errors?: DownloadRulesTabStates['errors']; isValid: boolean} {
const formData = this.getAmendedFormData();
const errors = Object.keys(this.validatedFields).reduce((accumulator, fieldName) => {
if (formData == null) {
return {isValid: false};
}
const errors = Object.keys(this.validatedFields).reduce((accumulator: DownloadRulesTabStates['errors'], field) => {
const fieldName = field as ValidatedFields;
const fieldValue = formData[fieldName];
if (!this.validatedFields[fieldName].isValid(fieldValue)) {
if (!this.validatedFields[fieldName].isValid(fieldValue) && accumulator != null) {
accumulator[fieldName] = this.validatedFields[fieldName].error;
}
return accumulator;
}, {});
if (errors == null) {
return {isValid: true};
}
return {errors, isValid: !Object.keys(errors).length};
}
render() {
const errors = Object.keys(this.state.errors).map((errorID) => (
<FormRow key={errorID}>
<FormError>{this.state.errors[errorID]}</FormError>
</FormRow>
));
let errors = null;
if (this.state.errors != null) {
errors = Object.keys(this.state.errors).map((error) => {
const errorID = error as ValidatedFields;
if (this.state.errors == null || this.state.errors[errorID] == null) {
return null;
}
return (
<FormRow key={errorID}>
<FormError>{this.state.errors[errorID]}</FormError>
</FormRow>
);
});
}
return (
<Form
@@ -441,19 +523,23 @@ class DownloadRulesTab extends React.Component {
}
}
const ConnectedDownloadRulesTab = connectStores(injectIntl(DownloadRulesTab), () => {
return [
{
store: FeedMonitorStore,
event: EventTypes.SETTINGS_FEED_MONITORS_FETCH_SUCCESS,
getValue: ({store}) => {
return {
feeds: store.getFeeds(),
rules: store.getRules(),
};
const ConnectedDownloadRulesTab = connectStores<Omit<DownloadRulesTabProps, 'intl'>, DownloadRulesTabStates>(
injectIntl(DownloadRulesTab),
() => {
return [
{
store: FeedsStore,
event: 'SETTINGS_FEED_MONITORS_FETCH_SUCCESS',
getValue: ({store}) => {
const storeFeeds = store as typeof FeedsStore;
return {
feeds: storeFeeds.getFeeds(),
rules: storeFeeds.getRules(),
};
},
},
},
];
});
];
},
);
export default ConnectedDownloadRulesTab;

View File

@@ -1,14 +1,18 @@
import {injectIntl} from 'react-intl';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import DownloadRulesTab from './DownloadRulesTab';
import FeedMonitorStore from '../../../stores/FeedMonitorStore';
import FeedsStore from '../../../stores/FeedsStore';
import FeedsTab from './FeedsTab';
import Modal from '../Modal';
class FeedsModal extends React.Component {
interface FeedsModalProps extends WrappedComponentProps {
dismiss(): void;
}
class FeedsModal extends React.Component<FeedsModalProps> {
componentDidMount() {
FeedMonitorStore.fetchFeedMonitors();
FeedsStore.fetchFeedMonitors();
}
render() {

View File

@@ -1,5 +1,5 @@
import _ from 'lodash';
import {defineMessages, FormattedMessage, injectIntl} from 'react-intl';
import {defineMessages, FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import formatUtil from '@shared/util/formatUtil';
import React from 'react';
@@ -17,13 +17,39 @@ import {
} from '../../../ui';
import Edit from '../../icons/Edit';
import Close from '../../icons/Close';
import EventTypes from '../../../constants/EventTypes';
import FeedMonitorStore from '../../../stores/FeedMonitorStore';
import FeedsStore from '../../../stores/FeedsStore';
import ModalFormSectionHeader from '../ModalFormSectionHeader';
import * as validators from '../../../util/validators';
import UIActions from '../../../actions/UIActions';
import connectStores from '../../../util/connectStores';
import type {Feed, Feeds, Items} from '../../../stores/FeedsStore';
interface IntervalMultiplier {
displayName: string;
value: number;
}
type ValidatedFields = 'url' | 'label' | 'interval';
interface FeedFormData extends Feed {
intervalMultiplier: number;
}
interface FeedsTabProps extends WrappedComponentProps {
feeds: Feeds;
items: Items;
}
interface FeedsTabStates {
errors?: {
[field in ValidatedFields]?: string;
};
intervalMultipliers: Array<IntervalMultiplier>;
currentlyEditingFeed: Feed | null;
selectedFeedID: string | null;
}
const MESSAGES = defineMessages({
mustSpecifyURL: {
id: 'feeds.validation.must.specify.valid.feed.url',
@@ -66,10 +92,10 @@ const defaultFeed = {
url: '',
};
class FeedsTab extends React.Component {
formRef = null;
class FeedsTab extends React.Component<FeedsTabProps, FeedsTabStates> {
formRef: Form | null = null;
manualAddingFormRef = null;
manualAddingFormRef: Form | null = null;
validatedFields = {
url: {
@@ -86,20 +112,24 @@ class FeedsTab extends React.Component {
},
};
checkFieldValidity = _.throttle((fieldName, fieldValue) => {
checkFieldValidity = _.throttle((fieldName: ValidatedFields, fieldValue) => {
const {errors} = this.state;
if (this.state.errors[fieldName] && this.validatedFields[fieldName].isValid(fieldValue)) {
if (errors == null) {
return;
}
if (errors[fieldName] && this.validatedFields[fieldName].isValid(fieldValue)) {
delete errors[fieldName];
this.setState({errors});
}
}, 150);
constructor(props) {
constructor(props: FeedsTabProps) {
super(props);
this.state = {
errors: {},
intervalmultipliers: [
intervalMultipliers: [
{
displayName: this.props.intl.formatMessage(MESSAGES.min),
value: 1,
@@ -114,20 +144,28 @@ class FeedsTab extends React.Component {
},
],
currentlyEditingFeed: null,
selectedFeed: null,
selectedFeedID: null,
};
}
getAmendedFormData() {
const formData = this.formRef.getFormData();
formData.interval = (formData.interval * formData.intervalMultiplier).toString();
getAmendedFormData(): Feed | null {
if (this.formRef == null) {
return null;
}
const formData = this.formRef.getFormData() as Partial<FeedFormData>;
if (formData.interval != null && formData.intervalMultiplier != null) {
formData.interval *= formData.intervalMultiplier;
}
delete formData.intervalMultiplier;
return formData;
return {...defaultFeed, ...formData};
}
getIntervalSelectOptions() {
return this.state.intervalmultipliers.map((interval) => (
return this.state.intervalMultipliers.map((interval: IntervalMultiplier) => (
<SelectItem key={interval.value} id={interval.value}>
{interval.displayName}
</SelectItem>
@@ -148,12 +186,17 @@ class FeedsTab extends React.Component {
}
return feeds.reduce(
(feedOptions, feed) =>
feedOptions.concat(
(feedOptions, feed) => {
if (feed._id == null) {
return feedOptions;
}
return feedOptions.concat(
<SelectItem key={feed._id} id={feed._id}>
{feed.label}
</SelectItem>,
),
);
},
[
<SelectItem key="select-feed" id="placeholder" placeholder>
<em>
@@ -164,7 +207,7 @@ class FeedsTab extends React.Component {
);
}
getModifyFeedForm(feed) {
getModifyFeedForm(feed: Feed) {
const isDayInterval = feed.interval % 1440;
const minutesDivisor = feed.interval % 60 ? 1 : 60;
const defaultIntervalTextValue = feed.interval / isDayInterval ? minutesDivisor : 1440;
@@ -212,7 +255,7 @@ class FeedsTab extends React.Component {
);
}
getFeedsListItem(feed) {
getFeedsListItem(feed: Feed) {
const matchedCount = feed.count || 0;
return (
<li className="interactive-list__item interactive-list__item--stacked-content feed-list__feed" key={feed._id}>
@@ -267,7 +310,7 @@ class FeedsTab extends React.Component {
);
}
getFeedAddForm(errors) {
getFeedAddForm(errors: Array<object | null> | null) {
return (
<Form
className="inverse"
@@ -341,7 +384,7 @@ class FeedsTab extends React.Component {
{this.renderSearchField()}
{this.renderDownloadButton()}
</FormRow>
{this.state.selectedFeed && <FormRow>{this.getFeedItemsList()}</FormRow>}
{this.state.selectedFeedID && <FormRow>{this.getFeedItemsList()}</FormRow>}
</Form>
);
}
@@ -364,7 +407,7 @@ class FeedsTab extends React.Component {
const itemsList = items.map((item, index) => (
<li className="interactive-list__item interactive-list__item--stacked-content feed-list__feed" key={item.title}>
<div className="interactive-list__label feed-list__feed-label">{item.title}</div>
<Checkbox id={index} />
<Checkbox id={`${index}`} />
</li>
));
@@ -380,22 +423,36 @@ class FeedsTab extends React.Component {
const currentFeed = this.state.currentlyEditingFeed;
const formData = this.getAmendedFormData();
if (currentFeed === defaultFeed) {
FeedMonitorStore.addFeed(formData);
} else {
FeedMonitorStore.modifyFeed(currentFeed._id, formData);
if (formData != null) {
if (currentFeed === defaultFeed) {
FeedsStore.addFeed(formData);
} else if (currentFeed != null && currentFeed._id != null) {
FeedsStore.modifyFeed(currentFeed._id, formData);
}
}
if (this.formRef != null) {
this.formRef.resetForm();
}
this.formRef.resetForm();
this.setState({currentlyEditingFeed: null});
}
};
handleFormChange = ({event, formData}) => {
this.checkFieldValidity(event.target.name, formData[event.target.name]);
handleFormChange = ({
event,
formData,
}: {
event: Event | React.FormEvent<HTMLFormElement>;
formData: Record<string, unknown>;
}) => {
const validatedField = (event.target as HTMLInputElement).name as ValidatedFields;
const feedForm = formData as Partial<Feed>;
this.checkFieldValidity(validatedField, feedForm[validatedField]);
};
handleRemoveFeedClick = (feed) => {
FeedMonitorStore.removeFeed(feed._id);
handleRemoveFeedClick = (feed: Feed) => {
if (feed._id != null) {
FeedsStore.removeFeed(feed._id);
}
if (feed === this.state.currentlyEditingFeed) {
this.setState({currentlyEditingFeed: null});
@@ -406,19 +463,27 @@ class FeedsTab extends React.Component {
this.setState({currentlyEditingFeed: defaultFeed});
};
handleModifyFeedClick = (feed) => {
handleModifyFeedClick = (feed: Feed) => {
this.setState({currentlyEditingFeed: feed});
};
handleBrowseFeedChange = (input) => {
if (input.event.target.type !== 'checkbox') {
this.setState({selectedFeed: input.formData.feedID});
FeedMonitorStore.fetchItems({params: {id: input.formData.feedID, search: input.formData.search}});
handleBrowseFeedChange = (input: {
event: Event | React.FormEvent<HTMLFormElement>;
formData: Record<string, unknown>;
}) => {
const feedBrowseForm = input.formData as {feedID: string; search: string};
if ((input.event.target as HTMLInputElement).type !== 'checkbox') {
this.setState({selectedFeedID: feedBrowseForm.feedID});
FeedsStore.fetchItems({params: {id: feedBrowseForm.feedID, search: feedBrowseForm.search}});
}
};
handleBrowseFeedSubmit = () => {
const formData = this.manualAddingFormRef.getFormData();
if (this.manualAddingFormRef == null) {
return;
}
const formData = this.manualAddingFormRef.getFormData() as Record<string, object>;
const downloadedTorrents = this.props.items
.filter((item, index) => formData[index])
@@ -427,25 +492,34 @@ class FeedsTab extends React.Component {
UIActions.displayModal({id: 'add-torrents', torrents: downloadedTorrents});
};
validateForm() {
const formData = this.formRef.getFormData();
const errors = Object.keys(this.validatedFields).reduce((memo, fieldName) => {
const fieldValue = formData[fieldName];
validateForm(): {errors?: FeedsTabStates['errors']; isValid: boolean} {
if (this.formRef == null) {
return {isValid: false};
}
if (!this.validatedFields[fieldName].isValid(fieldValue)) {
const formData = this.formRef.getFormData() as Record<string, object>;
const errors = Object.keys(this.validatedFields).reduce((memo: FeedsTabStates['errors'], field) => {
const fieldName = field as ValidatedFields;
const fieldValue = `${formData[fieldName]}`;
if (!this.validatedFields[fieldName].isValid(fieldValue) && memo != null) {
memo[fieldName] = this.validatedFields[fieldName].error;
}
return memo;
}, {});
if (errors == null) {
return {isValid: true};
}
return {errors, isValid: !Object.keys(errors).length};
}
renderSearchField = () => {
const {selectedFeed} = this.state;
const {selectedFeedID} = this.state;
if (selectedFeed == null) return null;
if (selectedFeedID == null) return null;
return (
<Textbox
@@ -459,9 +533,9 @@ class FeedsTab extends React.Component {
};
renderDownloadButton = () => {
const {selectedFeed} = this.state;
const {selectedFeedID} = this.state;
if (selectedFeed == null) return null;
if (selectedFeedID == null) return null;
return (
<Button key="button" type="submit" labelOffset>
@@ -471,11 +545,20 @@ class FeedsTab extends React.Component {
};
render() {
const errors = Object.keys(this.state.errors).map((errorID) => (
<FormRow key={errorID}>
<FormError>{this.state.errors[errorID]}</FormError>
</FormRow>
));
let errors = null;
if (this.state.errors != null) {
errors = Object.keys(this.state.errors).map((error) => {
const errorID = error as ValidatedFields;
if (this.state.errors == null || this.state.errors[errorID] == null) {
return null;
}
return (
<FormRow key={errorID}>
<FormError>{this.state.errors[errorID]}</FormError>
</FormRow>
);
});
}
return (
<div>
{this.getFeedAddForm(errors)}
@@ -485,23 +568,25 @@ class FeedsTab extends React.Component {
}
}
const ConnectedFeedsTab = connectStores(injectIntl(FeedsTab), () => {
const ConnectedFeedsTab = connectStores<Omit<FeedsTabProps, 'intl'>, FeedsTabStates>(injectIntl(FeedsTab), () => {
return [
{
store: FeedMonitorStore,
event: EventTypes.SETTINGS_FEED_MONITORS_FETCH_SUCCESS,
store: FeedsStore,
event: 'SETTINGS_FEED_MONITORS_FETCH_SUCCESS',
getValue: ({store}) => {
const storeFeeds = store as typeof FeedsStore;
return {
feeds: store.getFeeds(),
feeds: storeFeeds.getFeeds(),
};
},
},
{
store: FeedMonitorStore,
event: EventTypes.SETTINGS_FEED_MONITOR_ITEMS_FETCH_SUCCESS,
store: FeedsStore,
event: 'SETTINGS_FEED_MONITOR_ITEMS_FETCH_SUCCESS',
getValue: ({store}) => {
const storeFeeds = store as typeof FeedsStore;
return {
items: store.getItems() || [],
items: storeFeeds.getItems() || [],
};
},
},

View File

@@ -52,7 +52,7 @@ class RemoveTorrentsModal extends React.Component {
deleteDataContent = (
<FormRow>
<Checkbox id="deleteData" checked={SettingsStore.getFloodSettings('deleteTorrentData')}>
<Checkbox id="deleteData" checked={SettingsStore.getFloodSetting('deleteTorrentData')}>
<FormattedMessage id="torrents.remove.delete.data" />
</Checkbox>
</FormRow>

View File

@@ -8,11 +8,12 @@ import AuthActions from '../../../actions/AuthActions';
import AuthStore from '../../../stores/AuthStore';
import Close from '../../icons/Close';
import connectStores from '../../../util/connectStores';
import EventTypes from '../../../constants/EventTypes';
import ModalFormSectionHeader from '../ModalFormSectionHeader';
import RtorrentConnectionTypeSelection from '../../general/RtorrentConnectionTypeSelection';
import RTorrentConnectionTypeSelection from '../../general/RTorrentConnectionTypeSelection';
import SettingsTab from './SettingsTab';
import type {UserConfig} from '../../../stores/AuthStore';
class AuthTab extends SettingsTab {
state = {
addUserError: null,
@@ -20,9 +21,9 @@ class AuthTab extends SettingsTab {
isAddingUser: false,
};
formData = {};
formData?: Partial<UserConfig>;
formRef = null;
formRef?: Form | null = null;
componentDidMount() {
if (!this.props.isAdmin) return;
@@ -33,11 +34,11 @@ class AuthTab extends SettingsTab {
}
getUserList() {
const userList = this.props.users.sort((a, b) => a.username.localeCompare(b.username));
const userList = this.props.users.sort((a: UserConfig, b: UserConfig) => a.username.localeCompare(b.username));
const currentUsername = AuthStore.getCurrentUsername();
return userList.map((user) => {
return userList.map((user: UserConfig) => {
const isCurrentUser = user.username === currentUsername;
let badge = null;
let removeIcon = null;
@@ -74,16 +75,20 @@ class AuthTab extends SettingsTab {
});
}
handleDeleteUserClick(username) {
handleDeleteUserClick(username: UserConfig['username']) {
AuthActions.deleteUser(username).then(AuthActions.fetchUsers);
}
handleFormChange = ({formData}) => {
this.formData = formData;
handleFormChange = ({formData}: {formData: Record<string, unknown>}) => {
this.formData = formData as Partial<UserConfig>;
};
handleFormSubmit = () => {
if (this.formData.username === '') {
if (this.formData == null) {
return;
}
if (this.formData.username == null || this.formData.username === '') {
this.setState({
addUserError: this.props.intl.formatMessage({
id: 'auth.error.username.empty',
@@ -104,7 +109,9 @@ class AuthTab extends SettingsTab {
this.setState({addUserError: error.response.data.message, isAddingUser: false});
})
.then(() => {
this.formRef.resetForm();
if (this.formRef != null) {
this.formRef.resetForm();
}
this.setState({addUserError: null, isAddingUser: false});
});
}
@@ -192,9 +199,9 @@ class AuthTab extends SettingsTab {
<FormattedMessage id="auth.admin" />
</Checkbox>
</FormRow>
<RtorrentConnectionTypeSelection />
<RTorrentConnectionTypeSelection />
<FormRow justify="end">
<Button isLoading={this.state.isAddingUser} priority="primary" type="submit" width="auto">
<Button isLoading={this.state.isAddingUser} priority="primary" type="submit">
<FormattedMessage id="button.add" />
</Button>
</FormRow>
@@ -207,11 +214,12 @@ const ConnectedAuthTab = connectStores(injectIntl(AuthTab), () => {
return [
{
store: AuthStore,
event: EventTypes.AUTH_LIST_USERS_SUCCESS,
event: 'AUTH_LIST_USERS_SUCCESS',
getValue: ({store}) => {
const storeAuth = store as typeof AuthStore;
return {
users: store.getUsers(),
isAdmin: store.isAdmin(),
users: storeAuth.getUsers(),
isAdmin: storeAuth.isAdmin(),
};
},
},

View File

@@ -9,28 +9,16 @@ export default class ConnectivityTab extends SettingsTab {
state = {};
getDHTEnabledValue() {
if (this.state.dhtEnabled != null) {
return this.state.dhtEnabled;
}
return this.props.settings.dhtStats.dht === 'auto';
return this.props.settings.dhtStats.dht === 'auto' || this.props.settings.dhtStats.dht === 'on';
}
handleFormChange = ({event}) => {
if (event.target.name === 'dhtEnabled') {
const dhtEnabled = !this.getDHTEnabledValue();
const dhtEnabledString = dhtEnabled ? 'auto' : 'disable';
this.setState({dhtEnabled});
this.props.onCustomSettingsChange({
id: 'dht',
data: [dhtEnabledString],
overrideID: 'dhtStats',
overrideData: {dht: dhtEnabledString},
});
} else {
this.handleClientSettingFieldChange(event.target.name, event);
this.props.onClientSettingsChange({dht: event.target.checked ? 'auto' : 'disable'});
return;
}
this.handleClientSettingFieldChange(event.target.name, event);
};
render() {

View File

@@ -6,7 +6,6 @@ import AuthTab from './AuthTab';
import BandwidthTab from './BandwidthTab';
import ConnectivityTab from './ConnectivityTab';
import connectStores from '../../../util/connectStores';
import EventTypes from '../../../constants/EventTypes';
import Modal from '../Modal';
import ResourcesTab from './ResourcesTab';
import ConfigStore from '../../../stores/ConfigStore';
@@ -48,31 +47,16 @@ class SettingsModal extends React.Component {
];
}
handleCustomsSettingChange = (data) => {
this.setState((state) => {
return {
changedClientSettings: this.mergeObjects(state.changedClientSettings, {
[data.id]: {...data, overrideLocalSetting: true},
}),
};
});
};
handleSaveSettingsClick = () => {
const floodSettings = Object.keys(this.state.changedFloodSettings).map((settingsKey) => ({
id: settingsKey,
data: this.state.changedFloodSettings[settingsKey],
}));
const clientSettings = Object.keys(this.state.changedClientSettings).map((settingsKey) => {
const data = this.state.changedClientSettings[settingsKey];
if (data.overrideLocalSetting) {
return data;
}
return {id: settingsKey, data};
});
const clientSettings = Object.keys(this.state.changedClientSettings).map((settingsKey) => ({
id: settingsKey,
data: this.state.changedClientSettings[settingsKey],
}));
this.setState({isSavingSettings: true}, () => {
Promise.all([
@@ -160,7 +144,6 @@ class SettingsModal extends React.Component {
connectivity: {
content: ConnectivityTab,
props: {
onCustomSettingsChange: this.handleCustomsSettingChange,
onClientSettingsChange: this.handleClientSettingsChange,
settings: clientSettings,
},
@@ -236,11 +219,12 @@ const ConnectedSettingsModal = connectStores(injectIntl(SettingsModal), () => {
return [
{
store: SettingsStore,
event: EventTypes.SETTINGS_CHANGE,
event: 'SETTINGS_CHANGE',
getValue: ({store}) => {
const storeSettings = store;
return {
clientSettings: store.getClientSettings(),
floodSettings: store.getFloodSettings(),
clientSettings: storeSettings.getClientSettings(),
floodSettings: storeSettings.getFloodSettings(),
};
},
},

View File

@@ -14,8 +14,8 @@ class UITab extends SettingsTab {
super(props);
this.state = {
torrentListViewSize: SettingsStore.getFloodSettings('torrentListViewSize'),
selectedLanguage: SettingsStore.getFloodSettings('language'),
torrentListViewSize: SettingsStore.getFloodSetting('torrentListViewSize'),
selectedLanguage: SettingsStore.getFloodSetting('language'),
};
}

View File

@@ -10,7 +10,7 @@ class MountPointsList extends React.Component {
constructor(props) {
super(props);
const mountPoints = SettingsStore.getFloodSettings('mountPoints');
const mountPoints = SettingsStore.getFloodSetting('mountPoints');
const disks = DiskUsageStore.getDiskUsage().reduce((disksByTarget, disk) => {
disksByTarget[disk.target] = disk;
return disksByTarget;

View File

@@ -15,7 +15,7 @@ class TorrentContextMenuItemsList extends React.Component {
super(props);
this.state = {
torrentContextMenuItems: SettingsStore.getFloodSettings('torrentContextMenuItems'),
torrentContextMenuItems: SettingsStore.getFloodSetting('torrentContextMenuItems'),
};
}

View File

@@ -15,7 +15,7 @@ class TorrentDetailItemsList extends React.Component {
super(props);
this.state = {
torrentDetails: SettingsStore.getFloodSettings('torrentDetails'),
torrentDetails: SettingsStore.getFloodSetting('torrentDetails'),
};
}

View File

@@ -5,16 +5,20 @@ import Badge from '../../general/Badge';
import Size from '../../general/Size';
import Checkmark from '../../icons/Checkmark';
const checkmark = <Checkmark />;
import type {TorrentPeer} from '../../../stores/TorrentStore';
export default class TorrentPeers extends React.Component {
interface TorrentPeersProps {
peers: Array<TorrentPeer>;
}
export default class TorrentPeers extends React.Component<TorrentPeersProps> {
render() {
const {peers} = this.props;
if (peers) {
const peerList = peers.map((peer) => {
const {country: countryCode} = peer;
const encryptedIcon = peer.isEncrypted ? checkmark : null;
const encryptedIcon = peer.isEncrypted ? <Checkmark /> : null;
let peerCountry = null;
if (countryCode) {
@@ -26,8 +30,8 @@ export default class TorrentPeers extends React.Component {
className="peers-list__flag__image"
src={flagImageSrc}
onError={(e) => {
e.target.onerror = null;
e.target.style.display = 'none';
(e.target as HTMLImageElement).onerror = null;
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
<span className="peers-list__flag__text">{countryCode}</span>

View File

@@ -1,7 +1,6 @@
import {FormattedMessage} from 'react-intl';
import React from 'react';
import EventTypes from '../../constants/EventTypes';
import DiskUsageStore from '../../stores/DiskUsageStore';
import Size from '../general/Size';
import Tooltip from '../general/Tooltip';
@@ -9,7 +8,14 @@ import connectStores from '../../util/connectStores';
import ProgressBar from '../general/ProgressBar';
import SettingsStore from '../../stores/SettingsStore';
const DiskUsageTooltipItem = ({label, value}) => {
import type {Disk, Disks} from '../../stores/DiskUsageStore';
interface DiskUsageProps {
disks?: Disks;
mountPoints?: Array<string>;
}
const DiskUsageTooltipItem = ({label, value}: {label: object; value: number}) => {
return (
<li className="diskusage__details-list__item">
<label className="diskusage__details-list__label">{label}</label>
@@ -18,13 +24,19 @@ const DiskUsageTooltipItem = ({label, value}) => {
);
};
class DiskUsage extends React.Component {
class DiskUsage extends React.Component<DiskUsageProps> {
getDisks() {
const {disks, mountPoints} = this.props;
const diskMap = disks.reduce((disksByTarget, disk) => {
if (disks == null || mountPoints == null) {
return null;
}
const diskMap = disks.reduce((disksByTarget: Record<string, Disk>, disk: Disk) => {
disksByTarget[disk.target] = disk;
return disksByTarget;
}, {});
return mountPoints
.filter((target) => target in diskMap)
.map((target) => diskMap[target])
@@ -55,7 +67,7 @@ class DiskUsage extends React.Component {
render() {
const disks = this.getDisks();
if (disks.length === 0) {
if (disks == null || disks.length === 0) {
return null;
}
@@ -73,17 +85,21 @@ class DiskUsage extends React.Component {
export default connectStores(DiskUsage, () => [
{
store: DiskUsageStore,
event: EventTypes.DISK_USAGE_CHANGE,
getValue: ({store}) => ({
disks: store.getDiskUsage(),
}),
event: 'DISK_USAGE_CHANGE',
getValue: ({store}) => {
const storeDiskUsage = store as typeof DiskUsageStore;
return {
disks: storeDiskUsage.getDiskUsage(),
};
},
},
{
store: SettingsStore,
event: EventTypes.SETTINGS_CHANGE,
event: 'SETTINGS_CHANGE',
getValue: ({store}) => {
const storeSettings = store as typeof SettingsStore;
return {
mountPoints: store.getFloodSettings('mountPoints'),
mountPoints: storeSettings.getFloodSetting('mountPoints'),
};
},
},

View File

@@ -1,4 +1,4 @@
import {defineMessages, injectIntl} from 'react-intl';
import {defineMessages, injectIntl, WrappedComponentProps} from 'react-intl';
import React from 'react';
import FeedIcon from '../icons/FeedIcon';
@@ -11,17 +11,13 @@ const MESSAGES = defineMessages({
},
});
const METHODS_TO_BIND = ['handleFeedsButtonClick'];
class FeedsButton extends React.Component<WrappedComponentProps> {
tooltipRef: Tooltip | null = null;
class FeedsButton extends React.Component {
constructor() {
super();
constructor(props: WrappedComponentProps) {
super(props);
this.tooltipRef = null;
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
this.handleFeedsButtonClick = this.handleFeedsButtonClick.bind(this);
}
handleFeedsButtonClick() {

Some files were not shown because too many files have changed in this diff Show More