mirror of
https://github.com/zoriya/flood.git
synced 2025-12-06 07:16:18 +00:00
client: partially migrate to TypeScript
This commit is contained in:
27
.eslintrc.js
27
.eslintrc.js
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
@@ -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(),
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -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'],
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -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(),
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 '—';
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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',
|
||||
@@ -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';
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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});
|
||||
@@ -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;
|
||||
@@ -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() {
|
||||
@@ -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() || [],
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -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>
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,7 @@ class TorrentContextMenuItemsList extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
torrentContextMenuItems: SettingsStore.getFloodSettings('torrentContextMenuItems'),
|
||||
torrentContextMenuItems: SettingsStore.getFloodSetting('torrentContextMenuItems'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class TorrentDetailItemsList extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
torrentDetails: SettingsStore.getFloodSettings('torrentDetails'),
|
||||
torrentDetails: SettingsStore.getFloodSetting('torrentDetails'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -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
Reference in New Issue
Block a user