diff --git a/client/src/javascript/components/general/Duration.js b/client/src/javascript/components/general/Duration.tsx similarity index 84% rename from client/src/javascript/components/general/Duration.js rename to client/src/javascript/components/general/Duration.tsx index cbabbe24..bfeae370 100644 --- a/client/src/javascript/components/general/Duration.js +++ b/client/src/javascript/components/general/Duration.tsx @@ -1,7 +1,14 @@ import {FormattedMessage} from 'react-intl'; import React from 'react'; -export default class Duration extends React.Component { +import type {Duration as DurationType} from '@shared/types/Torrent'; + +interface DurationProps { + suffix: React.ReactNode; + value: 'Infinity' | DurationType; +} + +export default class Duration extends React.Component { render() { let {suffix = null} = this.props; const {value: duration} = this.props; @@ -18,7 +25,7 @@ export default class Duration extends React.Component { if (duration === 'Infinity') { content = ; - } else if (duration.years > 0) { + } else if (duration.years != null && duration.years > 0) { content = [ {duration.years} @@ -33,7 +40,7 @@ export default class Duration extends React.Component { , ]; - } else if (duration.weeks > 0) { + } else if (duration.weeks != null && duration.weeks > 0) { content = [ {duration.weeks} @@ -48,7 +55,7 @@ export default class Duration extends React.Component { , ]; - } else if (duration.days > 0) { + } else if (duration.days != null && duration.days > 0) { content = [ {duration.days} @@ -63,7 +70,7 @@ export default class Duration extends React.Component { , ]; - } else if (duration.hours > 0) { + } else if (duration.hours != null && duration.hours > 0) { content = [ {duration.hours} @@ -78,7 +85,7 @@ export default class Duration extends React.Component { , ]; - } else if (duration.minutes > 0) { + } else if (duration.minutes != null && duration.minutes > 0) { content = [ {duration.minutes} diff --git a/client/src/javascript/components/general/LoadingIndicator.js b/client/src/javascript/components/general/LoadingIndicator.tsx similarity index 77% rename from client/src/javascript/components/general/LoadingIndicator.js rename to client/src/javascript/components/general/LoadingIndicator.tsx index bd41c901..8e59260c 100644 --- a/client/src/javascript/components/general/LoadingIndicator.js +++ b/client/src/javascript/components/general/LoadingIndicator.tsx @@ -1,7 +1,11 @@ import classnames from 'classnames'; import React from 'react'; -export default class LoadingIndicator extends React.Component { +interface LoadingIndicatorProps { + inverse?: boolean; +} + +export default class LoadingIndicator extends React.Component { render() { const classes = classnames('loading-indicator', { 'is-inverse': this.props.inverse, diff --git a/client/src/javascript/components/general/ProgressBar.js b/client/src/javascript/components/general/ProgressBar.tsx similarity index 69% rename from client/src/javascript/components/general/ProgressBar.js rename to client/src/javascript/components/general/ProgressBar.tsx index 714398b8..a5e964bb 100644 --- a/client/src/javascript/components/general/ProgressBar.js +++ b/client/src/javascript/components/general/ProgressBar.tsx @@ -1,9 +1,14 @@ import React from 'react'; -export default class ProgressBar extends React.PureComponent { +interface ProgressBarProps { + percent: number; + icon?: JSX.Element; +} + +export default class ProgressBar extends React.PureComponent { render() { const percent = Math.round(this.props.percent); - const style = {}; + const style: React.CSSProperties = {}; if (percent !== 100) { style.width = `${percent}%`; diff --git a/client/src/javascript/components/general/Ratio.js b/client/src/javascript/components/general/Ratio.tsx similarity index 67% rename from client/src/javascript/components/general/Ratio.js rename to client/src/javascript/components/general/Ratio.tsx index fa9ee28a..09d5eef6 100644 --- a/client/src/javascript/components/general/Ratio.js +++ b/client/src/javascript/components/general/Ratio.tsx @@ -1,7 +1,11 @@ import {FormattedNumber} from 'react-intl'; import React from 'react'; -export default class Ratio extends React.Component { +interface RatioProps { + value: number; +} + +export default class Ratio extends React.Component { render() { let ratio = this.props.value; @@ -14,7 +18,7 @@ export default class Ratio extends React.Component { precision = 0; } - ratio = ratio.toFixed(precision); + ratio = Number(ratio.toFixed(precision)); return ; } diff --git a/client/src/javascript/components/general/filesystem/TorrentDestination.tsx b/client/src/javascript/components/general/filesystem/TorrentDestination.tsx index cd6fc294..7380ba72 100644 --- a/client/src/javascript/components/general/filesystem/TorrentDestination.tsx +++ b/client/src/javascript/components/general/filesystem/TorrentDestination.tsx @@ -186,7 +186,7 @@ class TorrentDestination extends React.Component - + diff --git a/client/src/javascript/components/modals/Modal.js b/client/src/javascript/components/modals/Modal.tsx similarity index 68% rename from client/src/javascript/components/modals/Modal.js rename to client/src/javascript/components/modals/Modal.tsx index dbddbb27..a41732c6 100644 --- a/client/src/javascript/components/modals/Modal.js +++ b/client/src/javascript/components/modals/Modal.tsx @@ -4,9 +4,31 @@ import React from 'react'; import ModalActions from './ModalActions'; import ModalTabs from './ModalTabs'; -const METHODS_TO_BIND = ['handleTabChange']; +import type {Tab} from './ModalTabs'; + +interface ModalProps { + heading?: React.ReactNode; + content?: React.ReactNode; + className?: string | null; + alignment?: 'left' | 'center'; + size?: 'medium' | 'large'; + orientation?: 'horizontal' | 'vertical'; + tabsInBody?: boolean; + inverse?: boolean; + actions?: ModalActions['props']['actions']; + tabs?: Record; + onSetRef?: (id: string, ref: HTMLDivElement | null) => void; +} + +interface ModalStates { + activeTabId: string | null; +} + +const METHODS_TO_BIND = ['handleTabChange'] as const; + +export default class Modal extends React.Component { + domRefs: Record = {}; -export default class Modal extends React.Component { static defaultProps = { alignment: 'left', className: null, @@ -16,10 +38,9 @@ export default class Modal extends React.Component { tabsInBody: false, }; - constructor() { - super(); + constructor(props: ModalProps) { + super(props); - this.domRefs = {}; this.state = { activeTabId: null, }; @@ -29,19 +50,11 @@ export default class Modal extends React.Component { }); } - getActiveTabId() { - if (this.state.activeTabId) { - return this.state.activeTabId; - } - - return Object.keys(this.props.tabs)[0]; + handleTabChange(tab: Tab) { + this.setState({activeTabId: tab.id || null}); } - handleTabChange(tab) { - this.setState({activeTabId: tab.id}); - } - - setRef(id, ref) { + setRef(id: string, ref: HTMLDivElement | null) { this.domRefs[id] = ref; if (this.props.onSetRef) { @@ -74,11 +87,15 @@ export default class Modal extends React.Component { let headerTabs; if (this.props.tabs) { - const activeTabId = this.getActiveTabId(); + let {activeTabId} = this.state; + if (activeTabId == null) { + [activeTabId] = Object.keys(this.props.tabs); + } + const activeTab = this.props.tabs[activeTabId]; const contentClasses = classnames('modal__content', activeTab.modalContentClasses); - const ModalContentComponent = activeTab.content; + const ModalContentComponent = activeTab.content as React.FunctionComponent; const modalContentData = activeTab.props; const tabs = ( @@ -107,7 +124,7 @@ export default class Modal extends React.Component { if (this.props.actions) { footer = (
- +
); } diff --git a/client/src/javascript/components/modals/ModalActions.js b/client/src/javascript/components/modals/ModalActions.js deleted file mode 100644 index 26667b7e..00000000 --- a/client/src/javascript/components/modals/ModalActions.js +++ /dev/null @@ -1,59 +0,0 @@ -import classnames from 'classnames'; -import React from 'react'; - -import {Button, Checkbox} from '../../ui'; -import UIActions from '../../actions/UIActions'; - -export default class ModalActions extends React.Component { - static defaultProps = { - alignment: 'left', - }; - - getModalButtons(actions) { - const buttons = actions.map((action, index) => { - const classes = classnames('button', { - [action.supplementalClassName]: action.supplementalClassName, - }); - - if (action.type === 'checkbox') { - return ( - // eslint-disable-next-line react/no-array-index-key - - {action.content} - - ); - } - - return ( - - ); - }); - - return
{buttons}
; - } - - getClickHandler(action) { - return (event) => { - if (action.clickHandler) { - action.clickHandler(event); - } - - if (action.triggerDismiss) { - UIActions.dismissModal(); - } - }; - } - - render() { - return
{this.getModalButtons(this.props.actions)}
; - } -} diff --git a/client/src/javascript/components/modals/ModalActions.tsx b/client/src/javascript/components/modals/ModalActions.tsx new file mode 100644 index 00000000..ae88e3f4 --- /dev/null +++ b/client/src/javascript/components/modals/ModalActions.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +import {Button, Checkbox} from '../../ui'; +import UIActions from '../../actions/UIActions'; + +interface BaseAction { + content: React.ReactNode; + triggerDismiss?: boolean; +} + +interface CheckboxAction extends BaseAction { + type: 'checkbox'; + id?: Checkbox['props']['id']; + checked?: Checkbox['props']['checked']; + clickHandler?: ((event: React.MouseEvent | KeyboardEvent) => void) | null; +} + +interface ButtonAction extends BaseAction { + type: 'primary' | 'tertiary'; + isLoading?: Button['props']['isLoading']; + submit?: boolean; + clickHandler?: ((event: React.MouseEvent) => void) | null; +} + +interface ModalActionsProps { + actions: Array; +} + +export default class ModalActions extends React.Component { + render() { + const buttons = this.props.actions.map((action, index) => { + let dismissIfNeeded = () => { + // do nothing by default. + }; + + if (action.triggerDismiss) { + dismissIfNeeded = () => { + UIActions.dismissModal(); + }; + } + + if (action.type === 'checkbox') { + return ( + { + if (action.clickHandler != null) { + action.clickHandler(event); + } + dismissIfNeeded(); + }}> + {action.content} + + ); + } + + return ( + + ); + }); + + const buttonsGroup =
{buttons}
; + + return
{buttonsGroup}
; + } +} diff --git a/client/src/javascript/components/modals/ModalFormSectionHeader.js b/client/src/javascript/components/modals/ModalFormSectionHeader.tsx similarity index 100% rename from client/src/javascript/components/modals/ModalFormSectionHeader.js rename to client/src/javascript/components/modals/ModalFormSectionHeader.tsx diff --git a/client/src/javascript/components/modals/ModalTabs.js b/client/src/javascript/components/modals/ModalTabs.tsx similarity index 61% rename from client/src/javascript/components/modals/ModalTabs.js rename to client/src/javascript/components/modals/ModalTabs.tsx index 9435e56d..857bb63a 100644 --- a/client/src/javascript/components/modals/ModalTabs.js +++ b/client/src/javascript/components/modals/ModalTabs.tsx @@ -1,12 +1,26 @@ import classnames from 'classnames'; import React from 'react'; -export default class ModalTabs extends React.Component { +export interface Tab { + id?: string; + label: React.ReactNode; + content: React.ReactNode | React.FunctionComponent; + props?: Record; + modalContentClasses?: string; +} + +interface ModalTabsProps { + activeTabId: string | null; + tabs: Record; + onTabChange: (tab: Tab) => void; +} + +export default class ModalTabs extends React.Component { static defaultProps = { - tabs: [], + tabs: {}, }; - handleTabClick(tab) { + handleTabClick(tab: Tab) { if (this.props.onTabChange) { this.props.onTabChange(tab); } diff --git a/client/src/javascript/components/modals/Modals.js b/client/src/javascript/components/modals/Modals.tsx similarity index 61% rename from client/src/javascript/components/modals/Modals.js rename to client/src/javascript/components/modals/Modals.tsx index 2e8005c4..44fd475b 100644 --- a/client/src/javascript/components/modals/Modals.js +++ b/client/src/javascript/components/modals/Modals.tsx @@ -16,21 +16,44 @@ import TorrentDetailsModal from './torrent-details-modal/TorrentDetailsModal'; import UIActions from '../../actions/UIActions'; import UIStore from '../../stores/UIStore'; -class Modals extends React.Component { - constructor() { - super(); +import type {Modal} from '../../stores/UIStore'; - this.modals = { - 'add-torrents': AddTorrentsModal, - confirm: ConfirmModal, - feeds: FeedsModal, - 'move-torrents': MoveTorrentsModal, - 'remove-torrents': RemoveTorrentsModal, - 'set-taxonomy': SetTagsModal, - 'set-tracker': SetTrackerModal, - settings: SettingsModal, - 'torrent-details': TorrentDetailsModal, - }; +interface ModalsProps { + activeModal?: Modal | null; +} + +const createModal = (id: Modal['id'], options: Modal['options']): React.ReactNode => { + switch (id) { + case 'add-torrents': + return ; + case 'confirm': + return ; + case 'feeds': + return ; + case 'move-torrents': + return ; + case 'remove-torrents': + return ; + case 'set-taxonomy': + return ; + case 'set-tracker': + return ; + case 'settings': + return ; + case 'torrent-details': + return ; + default: + return null; + } +}; + +const dismissModal = () => { + UIActions.dismissModal(); +}; + +class Modals extends React.Component { + constructor(props: ModalsProps) { + super(props); this.handleKeyPress = throttle(this.handleKeyPress, 1000); } @@ -43,28 +66,14 @@ class Modals extends React.Component { window.removeEventListener('keydown', this.handleKeyPress); } - dismissModal() { - UIActions.dismissModal(); - } - - getModal() { - const ActiveModal = this.modals[this.props.activeModal.id]; - - return ; - } - - handleKeyPress = (event) => { + handleKeyPress = (event: KeyboardEvent) => { if (this.props.activeModal != null && event.keyCode === 27) { - this.dismissModal(); + dismissModal(); } }; - handleModalClick(event) { - event.stopPropagation(); - } - handleOverlayClick = () => { - this.dismissModal(); + dismissModal(); }; render() { @@ -75,7 +84,7 @@ class Modals extends React.Component {
- {this.getModal()} + {createModal(this.props.activeModal.id, this.props.activeModal.options)}
); @@ -90,9 +99,9 @@ const ConnectedModals = connectStores(Modals, () => { { store: UIStore, event: EventTypes.UI_MODAL_CHANGE, - getValue: ({store}) => { + getValue: () => { return { - activeModal: store.getActiveModal(), + activeModal: UIStore.getActiveModal(), }; }, }, diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx similarity index 63% rename from client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js rename to client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx index 36b15c7f..149fb469 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.js +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx @@ -1,16 +1,20 @@ -import {injectIntl} from 'react-intl'; +import {injectIntl, WrappedComponentProps} from 'react-intl'; import React, {PureComponent} from 'react'; import ModalActions from '../ModalActions'; import SettingsStore from '../../../stores/SettingsStore'; -class AddTorrentsActions extends PureComponent { - getActions() { - const startTorrentsOnLoad = SettingsStore.getFloodSetting('startTorrentsOnLoad'); +interface AddTorrentsActionsProps extends WrappedComponentProps { + isAddingTorrents: boolean; + onAddTorrentsClick: (event: React.MouseEvent) => void; +} + +class AddTorrentsActions extends PureComponent { + getActions(): ModalActions['props']['actions'] { return [ { - checked: startTorrentsOnLoad === 'true' || startTorrentsOnLoad === true, - clickHandler: this.handleStartTorrentsToggle, + checked: Boolean(SettingsStore.getFloodSetting('startTorrentsOnLoad')), + clickHandler: null, content: this.props.intl.formatMessage({ id: 'torrents.add.start.label', }), @@ -40,7 +44,7 @@ class AddTorrentsActions extends PureComponent { } render() { - return ; + return ; } } diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js index 323a2973..0e06cb63 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.js @@ -103,7 +103,7 @@ class AddTorrentsByFile extends React.Component { this.setState({isAddingTorrents: true}); const fileData = new FormData(); - const {destination, start, tags, useBasePath} = formData; + const {destination, start, tags, isBasePath} = formData; this.state.files.forEach((file) => { fileData.append('torrents', file); @@ -114,7 +114,7 @@ class AddTorrentsByFile extends React.Component { }); fileData.append('destination', destination); - fileData.append('isBasePath', useBasePath); + fileData.append('isBasePath', isBasePath); fileData.append('start', start); TorrentActions.addTorrentsByFiles(fileData, destination); @@ -145,7 +145,6 @@ class AddTorrentsByFile extends React.Component { /> diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx index 47423c56..135df750 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx @@ -17,25 +17,21 @@ type AddTorrentsByURLFormData = { [urls: string]: string; } & { destination: string; - useBasePath: boolean; + isBasePath: boolean; start: boolean; tags: string; }; -interface AddTorrentsByURLProps extends WrappedComponentProps { - dismissModal: () => void; -} - interface AddTorrentsByURLStates { isAddingTorrents: boolean; tags: string; urlTextboxes: Textboxes; } -class AddTorrentsByURL extends React.Component { +class AddTorrentsByURL extends React.Component { formRef: Form | null = null; - constructor(props: AddTorrentsByURLProps) { + constructor(props: WrappedComponentProps) { super(props); const activeModal = UIStore.getActiveModal(); @@ -81,7 +77,7 @@ class AddTorrentsByURL extends React.Component diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.js b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx similarity index 76% rename from client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.js rename to client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx index 9fd67817..d9f09b72 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.js +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx @@ -1,16 +1,11 @@ -import {injectIntl} from 'react-intl'; +import {injectIntl, WrappedComponentProps} from 'react-intl'; import React from 'react'; import AddTorrentsByFile from './AddTorrentsByFile'; import AddTorrentsByURL from './AddTorrentsByURL'; import Modal from '../Modal'; -import UIActions from '../../../actions/UIActions'; - -class AddTorrents extends React.Component { - dismissModal() { - UIActions.dismissModal(); - } +class AddTorrents extends React.Component { render() { const tabs = { 'by-url': { @@ -32,7 +27,6 @@ class AddTorrents extends React.Component { heading={this.props.intl.formatMessage({ id: 'torrents.add.heading', })} - dismiss={this.dismissModal} tabs={tabs} /> ); diff --git a/client/src/javascript/components/modals/confirm-modal/ConfirmModal.js b/client/src/javascript/components/modals/confirm-modal/ConfirmModal.tsx similarity index 61% rename from client/src/javascript/components/modals/confirm-modal/ConfirmModal.js rename to client/src/javascript/components/modals/confirm-modal/ConfirmModal.tsx index d9de240d..e1ecf8aa 100644 --- a/client/src/javascript/components/modals/confirm-modal/ConfirmModal.js +++ b/client/src/javascript/components/modals/confirm-modal/ConfirmModal.tsx @@ -2,7 +2,15 @@ import React from 'react'; import Modal from '../Modal'; -export default class ConfirmModal extends React.Component { +interface ConfirmModalProps { + options: { + content: React.ReactNode; + heading: React.ReactNode; + actions: Modal['props']['actions']; + }; +} + +export default class ConfirmModal extends React.Component { getContent() { return
{this.props.options.content}
; } @@ -13,7 +21,6 @@ export default class ConfirmModal extends React.Component { actions={this.props.options.actions} alignment="center" content={this.getContent()} - dismiss={this.props.dismiss} heading={this.props.options.heading} /> ); diff --git a/client/src/javascript/components/modals/feeds-modal/FeedsModal.tsx b/client/src/javascript/components/modals/feeds-modal/FeedsModal.tsx index 004a0923..5640f6b7 100644 --- a/client/src/javascript/components/modals/feeds-modal/FeedsModal.tsx +++ b/client/src/javascript/components/modals/feeds-modal/FeedsModal.tsx @@ -6,11 +6,7 @@ import {FeedsStoreClass} from '../../../stores/FeedsStore'; import FeedsTab from './FeedsTab'; import Modal from '../Modal'; -interface FeedsModalProps extends WrappedComponentProps { - dismiss(): void; -} - -class FeedsModal extends React.Component { +class FeedsModal extends React.Component { componentDidMount() { FeedsStoreClass.fetchFeedMonitors(); } @@ -33,7 +29,6 @@ class FeedsModal extends React.Component { return ( { + let directoryPath = path.substring(0, path.length - filename.length); + + if ( + directoryPath.charAt(directoryPath.length - 1) === '/' || + directoryPath.charAt(directoryPath.length - 1) === '\\' + ) { + directoryPath = directoryPath.substring(0, directoryPath.length - 1); + } + + return directoryPath; +}; + +class MoveTorrents extends React.Component { + constructor(props: WrappedComponentProps) { super(props); const filenames = TorrentStore.getSelectedTorrentsFilename(); const sources = TorrentStore.getSelectedTorrentsDownloadLocations(); this.state = { isSettingDownloadPath: false, - originalSource: sources.length === 1 ? this.removeTrailingFilename(sources[0], filenames[0]) : null, + originalSource: sources.length === 1 ? removeTrailingFilename(sources[0], filenames[0]) : undefined, }; } - getActions() { + getActions(): ModalActions['props']['actions'] { return [ { checked: false, @@ -56,18 +76,22 @@ class MoveTorrents extends React.Component { ]; } - getContent() { + getContent(): React.ReactNode { return (
-
+ { + return this.handleFormSubmit((formData as unknown) as MoveTorrentsOptions); + }}> - +
); } - handleFormSubmit = ({formData}) => { + handleFormSubmit = (formData: MoveTorrentsOptions) => { const filenames = TorrentStore.getSelectedTorrentsFilename(); const sourcePaths = TorrentStore.getSelectedTorrentsDownloadLocations(); @@ -75,7 +99,7 @@ class MoveTorrents extends React.Component { this.setState({isSettingDownloadPath: true}); TorrentActions.moveTorrents(TorrentStore.getSelectedTorrents(), { destination: formData.destination, - isBasePath: formData.useBasePath, + isBasePath: formData.isBasePath, filenames, moveFiles: formData.moveFiles, sourcePaths, @@ -86,24 +110,10 @@ class MoveTorrents extends React.Component { } }; - removeTrailingFilename(path, filename) { - let directoryPath = path.substring(0, path.length - filename.length); - - if ( - directoryPath.charAt(directoryPath.length - 1) === '/' || - directoryPath.charAt(directoryPath.length - 1) === '\\' - ) { - directoryPath = directoryPath.substring(0, directoryPath.length - 1); - } - - return directoryPath; - } - render() { return ( { + formRef?: Form | null; + + getActions(torrentCount: number): Modal['props']['actions'] { + if (torrentCount === 0) { return [ { clickHandler: null, @@ -22,7 +24,7 @@ class RemoveTorrentsModal extends React.Component { return [ { - clickHandler: this.handleRemoveTorrentDecline, + clickHandler: null, content: this.props.intl.formatMessage({ id: 'button.no', }), @@ -40,15 +42,14 @@ class RemoveTorrentsModal extends React.Component { ]; } - getContent(torrents) { + getContent(torrentCount: number) { let modalContent = null; let deleteDataContent = null; - const selectedTorrentCount = torrents.length; - if (selectedTorrentCount === 0) { + if (torrentCount === 0) { modalContent = ; } else { - modalContent = ; + modalContent = ; deleteDataContent = ( @@ -73,9 +74,14 @@ class RemoveTorrentsModal extends React.Component { } handleRemovalConfirmation = () => { + let deleteData = false; + if (this.formRef != null && this.formRef.getFormData().deleteData) { + deleteData = true; + } + TorrentActions.deleteTorrents({ hashes: TorrentStore.getSelectedTorrents(), - deleteData: this.formRef.getFormData().deleteData, + deleteData, }); }; @@ -87,10 +93,9 @@ class RemoveTorrentsModal extends React.Component { return ( ); diff --git a/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.js b/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx similarity index 80% rename from client/src/javascript/components/modals/set-tags-modal/SetTagsModal.js rename to client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx index 2ab08f58..20b5567e 100644 --- a/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.js +++ b/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx @@ -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'; @@ -6,10 +6,14 @@ import Modal from '../Modal'; import TorrentActions from '../../../actions/TorrentActions'; import TorrentStore from '../../../stores/TorrentStore'; -class SetTagsModal extends React.Component { - formRef = null; +interface SetTagsModalStates { + isSettingTags: boolean; +} - constructor(props) { +class SetTagsModal extends React.Component { + formRef: Form | null = null; + + constructor(props: WrappedComponentProps) { super(props); this.state = { isSettingTags: false, @@ -17,13 +21,17 @@ class SetTagsModal extends React.Component { } handleSetTagsClick = () => { - const formData = this.formRef.getFormData(); + if (this.formRef == null) { + return; + } + + const formData = this.formRef.getFormData() as {tags: string}; const tags = formData.tags ? formData.tags.split(',') : []; this.setState({isSettingTags: true}, () => TorrentActions.setTaxonomy(TorrentStore.getSelectedTorrents(), tags)); }; - getActions() { + getActions(): Modal['props']['actions'] { const primaryButtonText = this.props.intl.formatMessage({ id: 'torrents.set.tags.button.set', }); @@ -75,7 +83,6 @@ class SetTagsModal extends React.Component { { + formRef: Form | null = null; + + constructor(props: WrappedComponentProps) { super(props); this.state = { isSettingTracker: false, }; } - handleSetTrackerClick = () => { - const formData = this.formRef.getFormData(); + handleSetTrackerClick = (): void => { + if (this.formRef == null) { + return; + } + + const formData = this.formRef.getFormData() as {tracker: string}; const {tracker} = formData; this.setState({isSettingTracker: true}, () => @@ -25,7 +33,7 @@ class SetTrackerModal extends React.Component { ); }; - getActions() { + getActions(): Modal['props']['actions'] { const primaryButtonText = this.props.intl.formatMessage({ id: 'torrents.set.tracker.button.set', }); @@ -49,7 +57,7 @@ class SetTrackerModal extends React.Component { ]; } - getContent() { + getContent(): React.ReactNode { const trackerValue = TorrentStore.getSelectedTorrentsTrackerURIs()[0].join(', '); return ( @@ -77,7 +85,6 @@ class SetTrackerModal extends React.Component { ); diff --git a/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.js b/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.js index 16881879..75c4b3dc 100644 --- a/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.js +++ b/client/src/javascript/components/modals/torrent-details-modal/TorrentDetailsModal.js @@ -11,7 +11,6 @@ import TorrentHeading from './TorrentHeading'; import TorrentPeers from './TorrentPeers'; import TorrentStore from '../../../stores/TorrentStore'; import TorrentTrackers from './TorrentTrackers'; -import UIActions from '../../../actions/UIActions'; import UIStore from '../../../stores/UIStore'; class TorrentDetailsModal extends React.Component { @@ -23,10 +22,6 @@ class TorrentDetailsModal extends React.Component { TorrentStore.stopPollingTorrentDetails(); } - dismissModal() { - UIActions.dismissModal(); - } - getModalHeading() { return ; } @@ -80,7 +75,6 @@ class TorrentDetailsModal extends React.Component { return ( ; startOnLoad: boolean; - useBasePath?: boolean; + isBasePath?: boolean; count?: number; } diff --git a/client/src/javascript/stores/UIStore.ts b/client/src/javascript/stores/UIStore.ts index be1a0a15..be3073e6 100644 --- a/client/src/javascript/stores/UIStore.ts +++ b/client/src/javascript/stores/UIStore.ts @@ -29,8 +29,18 @@ export interface Dependency { export type Dependencies = Record; export interface Modal { - id: string; + id: + | 'add-torrents' + | 'confirm' + | 'feeds' + | 'move-torrents' + | 'remove-torrents' + | 'set-taxonomy' + | 'set-tracker' + | 'settings' + | 'torrent-details'; torrents?: unknown; + options?: unknown; } class UIStoreClass extends BaseStore { diff --git a/package-lock.json b/package-lock.json index ec633626..0f5a152f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2082,6 +2082,15 @@ "csstype": "^3.0.2" } }, + "@types/react-custom-scrollbars": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/react-custom-scrollbars/-/react-custom-scrollbars-4.0.7.tgz", + "integrity": "sha512-4QPZdwd+wmzWq9TyNSA/4MZFYvlQn1GlEFFkpFx8VSs13gR/L+hQne0vFnbzwlQmGG7OksthkoVpYxWJjzz95w==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "16.9.8", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", diff --git a/package.json b/package.json index 4d2c2d48..1901e44f 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@types/passport": "^1.0.4", "@types/passport-jwt": "^3.0.3", "@types/react": "^16.9.49", + "@types/react-custom-scrollbars": "^4.0.7", "@types/react-dom": "^16.9.8", "@types/react-measure": "^2.0.6", "@types/react-router-dom": "^5.1.5", diff --git a/shared/types/Torrent.ts b/shared/types/Torrent.ts index fdf61dbf..b966e455 100644 --- a/shared/types/Torrent.ts +++ b/shared/types/Torrent.ts @@ -1,6 +1,7 @@ import {TorrentStatus} from '../constants/torrentStatusMap'; export interface Duration { + years?: number; weeks?: number; days?: number; hours?: number;