mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-23 13:57:06 -04:00
Convert Download Client settings to TypeScript
This commit is contained in:
parent
6838f068bc
commit
92db4769be
43 changed files with 1555 additions and 2156 deletions
|
@ -11,7 +11,7 @@ import Switch from 'Components/Router/Switch';
|
|||
import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnector';
|
||||
import SeriesIndex from 'Series/Index/SeriesIndex';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings';
|
||||
import GeneralSettings from 'Settings/General/GeneralSettings';
|
||||
import ImportListSettings from 'Settings/ImportLists/ImportListSettings';
|
||||
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||
|
@ -113,7 +113,7 @@ function AppRoutes() {
|
|||
|
||||
<Route
|
||||
path="/settings/downloadclients"
|
||||
component={DownloadClientSettingsConnector}
|
||||
component={DownloadClientSettings}
|
||||
/>
|
||||
|
||||
<Route path="/settings/importlists" component={ImportListSettings} />
|
||||
|
|
|
@ -20,12 +20,14 @@ import IndexerFlag from 'typings/IndexerFlag';
|
|||
import Notification from 'typings/Notification';
|
||||
import QualityDefinition from 'typings/QualityDefinition';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import DownloadClientOptions from 'typings/Settings/DownloadClientOptions';
|
||||
import General from 'typings/Settings/General';
|
||||
import IndexerOptions from 'typings/Settings/IndexerOptions';
|
||||
import MediaManagement from 'typings/Settings/MediaManagement';
|
||||
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||
import NamingExample from 'typings/Settings/NamingExample';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import RemotePathMapping from 'typings/Settings/RemotePathMapping';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import MetadataAppState from './MetadataAppState';
|
||||
|
||||
|
@ -52,10 +54,15 @@ export interface DelayProfileAppState
|
|||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState<Presets<DownloadClient>> {
|
||||
isTestingAll: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadClientOptionsAppState
|
||||
extends AppSectionItemState<DownloadClientOptions>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface GeneralAppState
|
||||
extends AppSectionItemState<General>,
|
||||
AppSectionSaveState {}
|
||||
|
@ -131,6 +138,13 @@ export interface ImportListExclusionsSettingsAppState
|
|||
pendingChanges: Partial<ImportListExclusion>;
|
||||
}
|
||||
|
||||
export interface RemotePathMappingsAppState
|
||||
extends AppSectionState<RemotePathMapping>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
pendingChanges: Partial<RemotePathMapping>;
|
||||
}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
@ -142,6 +156,7 @@ interface SettingsAppState {
|
|||
customFormats: CustomFormatAppState;
|
||||
delayProfiles: DelayProfileAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
downloadClientOptions: DownloadClientOptionsAppState;
|
||||
general: GeneralAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
|
@ -158,6 +173,7 @@ interface SettingsAppState {
|
|||
qualityDefinitions: QualityDefinitionsAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
releaseProfiles: ReleaseProfilesAppState;
|
||||
remotePathMappings: RemotePathMappingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
|
|
39
frontend/src/DownloadClient/useDownloadClientHostOptions.ts
Normal file
39
frontend/src/DownloadClient/useDownloadClientHostOptions.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export default function useDownloadClientHostOptions() {
|
||||
return useSelector(
|
||||
createSelector(
|
||||
(state: AppState) => state.settings.downloadClients.items,
|
||||
(downloadClients) => {
|
||||
const hosts = downloadClients.reduce<Record<string, string[]>>(
|
||||
(acc, downloadClient) => {
|
||||
const name = downloadClient.name;
|
||||
const host = downloadClient.fields.find((field) => {
|
||||
return field.name === 'host';
|
||||
});
|
||||
|
||||
if (host) {
|
||||
const hostValue = host.value as string;
|
||||
|
||||
const group = (acc[hostValue] = acc[hostValue] || []);
|
||||
group.push(name);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return Object.keys(hosts).map((host) => {
|
||||
return {
|
||||
key: host,
|
||||
value: host,
|
||||
hint: `${hosts[host].join(', ')}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
|
||||
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
|
||||
import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector';
|
||||
import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector';
|
||||
|
||||
class DownloadClientSettings extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isManageDownloadClientsOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChildMounted = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
onManageDownloadClientsPress = () => {
|
||||
this.setState({ isManageDownloadClientsOpen: true });
|
||||
};
|
||||
|
||||
onManageDownloadClientsModalClose = () => {
|
||||
this.setState({ isManageDownloadClientsOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isTestingAll,
|
||||
dispatchTestAllDownloadClients
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
isManageDownloadClientsOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('DownloadClientSettings')}>
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllClients')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
onPress={dispatchTestAllDownloadClients}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageClients')}
|
||||
iconName={icons.MANAGE}
|
||||
onPress={this.onManageDownloadClientsPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<DownloadClientsConnector />
|
||||
|
||||
<DownloadClientOptionsConnector
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
|
||||
<RemotePathMappingsConnector />
|
||||
|
||||
<ManageDownloadClientsModal
|
||||
isOpen={isManageDownloadClientsOpen}
|
||||
onModalClose={this.onManageDownloadClientsModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClientSettings.propTypes = {
|
||||
isTestingAll: PropTypes.bool.isRequired,
|
||||
dispatchTestAllDownloadClients: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DownloadClientSettings;
|
109
frontend/src/Settings/DownloadClients/DownloadClientSettings.tsx
Normal file
109
frontend/src/Settings/DownloadClients/DownloadClientSettings.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbar from 'Settings/SettingsToolbar';
|
||||
import { testAllDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import {
|
||||
SaveCallback,
|
||||
SettingsStateChange,
|
||||
} from 'typings/Settings/SettingsState';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import DownloadClients from './DownloadClients/DownloadClients';
|
||||
import ManageDownloadClientsModal from './DownloadClients/Manage/ManageDownloadClientsModal';
|
||||
import DownloadClientOptions from './Options/DownloadClientOptions';
|
||||
import RemotePathMappings from './RemotePathMappings/RemotePathMappings';
|
||||
|
||||
function DownloadClientSettings() {
|
||||
const dispatch = useDispatch();
|
||||
const isTestingAll = useSelector(
|
||||
(state: AppState) => state.settings.downloadClients.isTestingAll
|
||||
);
|
||||
|
||||
const saveOptions = useRef<() => void>();
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [hasPendingChanges, setHasPendingChanges] = useState(false);
|
||||
const [
|
||||
isManageDownloadClientsModalOpen,
|
||||
setIsManageDownloadClientsModalOpen,
|
||||
] = useState(false);
|
||||
|
||||
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
|
||||
saveOptions.current = saveCallback;
|
||||
}, []);
|
||||
|
||||
const handleChildStateChange = useCallback(
|
||||
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
|
||||
setIsSaving(isSaving);
|
||||
setHasPendingChanges(hasPendingChanges);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleManageDownloadClientsPress = useCallback(() => {
|
||||
setIsManageDownloadClientsModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleManageDownloadClientsModalClose = useCallback(() => {
|
||||
setIsManageDownloadClientsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
saveOptions.current?.();
|
||||
}, []);
|
||||
|
||||
const handleTestAllIndexersPress = useCallback(() => {
|
||||
dispatch(testAllDownloadClients());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<PageContent title={translate('DownloadClientSettings')}>
|
||||
<SettingsToolbar
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('TestAllClients')}
|
||||
iconName={icons.TEST}
|
||||
isSpinning={isTestingAll}
|
||||
onPress={handleTestAllIndexersPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManageClients')}
|
||||
iconName={icons.MANAGE}
|
||||
onPress={handleManageDownloadClientsPress}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onSavePress={handleSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<DownloadClients />
|
||||
|
||||
<DownloadClientOptions
|
||||
setChildSave={handleSetChildSave}
|
||||
onChildStateChange={handleChildStateChange}
|
||||
/>
|
||||
|
||||
<RemotePathMappings />
|
||||
|
||||
<ManageDownloadClientsModal
|
||||
isOpen={isManageDownloadClientsModalOpen}
|
||||
onModalClose={handleManageDownloadClientsModalClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadClientSettings;
|
|
@ -1,21 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { testAllDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import DownloadClientSettings from './DownloadClientSettings';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.downloadClients.isTestingAll,
|
||||
(isTestingAll) => {
|
||||
return {
|
||||
isTestingAll
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchTestAllDownloadClients: testAllDownloadClients
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSettings);
|
|
@ -1,111 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientPresetMenuItem from './AddDownloadClientPresetMenuItem';
|
||||
import styles from './AddDownloadClientItem.css';
|
||||
|
||||
class AddDownloadClientItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDownloadClientSelect = () => {
|
||||
const {
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onDownloadClientSelect({ implementation });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onDownloadClientSelect
|
||||
} = this.props;
|
||||
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.downloadClient}
|
||||
>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
onPress={this.onDownloadClientSelect}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>
|
||||
{implementationName}
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{
|
||||
hasPresets &&
|
||||
<span>
|
||||
<Button
|
||||
size={sizes.SMALL}
|
||||
onPress={this.onDownloadClientSelect}
|
||||
>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button
|
||||
className={styles.presetsMenuButton}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
presets.map((preset) => {
|
||||
return (
|
||||
<AddDownloadClientPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
onPress={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
to={infoLink}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddDownloadClientItem.propTypes = {
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
infoLink: PropTypes.string.isRequired,
|
||||
presets: PropTypes.arrayOf(PropTypes.object),
|
||||
onDownloadClientSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddDownloadClientItem;
|
|
@ -0,0 +1,88 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { selectDownloadClientSchema } from 'Store/Actions/settingsActions';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientPresetMenuItem from './AddDownloadClientPresetMenuItem';
|
||||
import styles from './AddDownloadClientItem.css';
|
||||
|
||||
interface AddDownloadClientItemProps {
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
infoLink: string;
|
||||
presets?: DownloadClient[];
|
||||
onDownloadClientSelect: () => void;
|
||||
}
|
||||
|
||||
function AddDownloadClientItem({
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onDownloadClientSelect,
|
||||
}: AddDownloadClientItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
const handleDownloadClientSelect = useCallback(() => {
|
||||
dispatch(
|
||||
selectDownloadClientSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
})
|
||||
);
|
||||
|
||||
onDownloadClientSelect();
|
||||
}, [implementation, implementationName, dispatch, onDownloadClientSelect]);
|
||||
|
||||
return (
|
||||
<div className={styles.downloadClient}>
|
||||
<Link className={styles.underlay} onPress={handleDownloadClientSelect} />
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>{implementationName}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{hasPresets ? (
|
||||
<span>
|
||||
<Button size={sizes.SMALL} onPress={handleDownloadClientSelect}>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{presets.map((preset) => {
|
||||
return (
|
||||
<AddDownloadClientPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
implementationName={implementationName}
|
||||
onPress={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<Button to={infoLink} size={sizes.SMALL}>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddDownloadClientItem;
|
|
@ -1,25 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddDownloadClientModalContentConnector from './AddDownloadClientModalContentConnector';
|
||||
|
||||
function AddDownloadClientModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddDownloadClientModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddDownloadClientModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddDownloadClientModal;
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddDownloadClientModalContent, {
|
||||
AddDownloadClientModalContentProps,
|
||||
} from './AddDownloadClientModalContent';
|
||||
|
||||
interface AddDownloadClientModalProps
|
||||
extends AddDownloadClientModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function AddDownloadClientModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: AddDownloadClientModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<AddDownloadClientModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddDownloadClientModal;
|
|
@ -1,122 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientItem from './AddDownloadClientItem';
|
||||
import styles from './AddDownloadClientModalContent.css';
|
||||
|
||||
class AddDownloadClientModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
usenetDownloadClients,
|
||||
torrentDownloadClients,
|
||||
onDownloadClientSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('AddDownloadClient')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isSchemaFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isSchemaFetching && !!schemaError &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddDownloadClientError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isSchemaPopulated && !schemaError &&
|
||||
<div>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('SupportedDownloadClients')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('SupportedDownloadClientsMoreInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<FieldSet legend={translate('Usenet')}>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
usenetDownloadClients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
implementation={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Torrents')}>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
torrentDownloadClients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
implementation={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</FieldSet>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddDownloadClientModalContent.propTypes = {
|
||||
isSchemaFetching: PropTypes.bool.isRequired,
|
||||
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||
schemaError: PropTypes.object,
|
||||
usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDownloadClientSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddDownloadClientModalContent;
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { fetchDownloadClientSchema } from 'Store/Actions/settingsActions';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientItem from './AddDownloadClientItem';
|
||||
import styles from './AddDownloadClientModalContent.css';
|
||||
|
||||
export interface AddDownloadClientModalContentProps {
|
||||
onDownloadClientSelect: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function AddDownloadClientModalContent({
|
||||
onDownloadClientSelect,
|
||||
onModalClose,
|
||||
}: AddDownloadClientModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
|
||||
useSelector((state: AppState) => state.settings.downloadClients);
|
||||
|
||||
const { usenetDownloadClients, torrentDownloadClients } = useMemo(() => {
|
||||
return schema.reduce<{
|
||||
usenetDownloadClients: DownloadClient[];
|
||||
torrentDownloadClients: DownloadClient[];
|
||||
}>(
|
||||
(acc, item) => {
|
||||
if (item.protocol === 'usenet') {
|
||||
acc.usenetDownloadClients.push(item);
|
||||
} else if (item.protocol === 'torrent') {
|
||||
acc.torrentDownloadClients.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
usenetDownloadClients: [],
|
||||
torrentDownloadClients: [],
|
||||
}
|
||||
);
|
||||
}, [schema]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDownloadClientSchema());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('AddDownloadClient')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isSchemaFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isSchemaFetching && !!schemaError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddDownloadClientError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isSchemaPopulated && !schemaError ? (
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('SupportedDownloadClients')}</div>
|
||||
<div>{translate('SupportedDownloadClientsMoreInfo')}</div>
|
||||
</Alert>
|
||||
|
||||
<FieldSet legend={translate('Usenet')}>
|
||||
<div className={styles.downloadClients}>
|
||||
{usenetDownloadClients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
implementation={downloadClient.implementation}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Torrents')}>
|
||||
<div className={styles.downloadClients}>
|
||||
{torrentDownloadClients.map((downloadClient) => {
|
||||
return (
|
||||
<AddDownloadClientItem
|
||||
key={downloadClient.implementation}
|
||||
{...downloadClient}
|
||||
implementation={downloadClient.implementation}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</FieldSet>
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddDownloadClientModalContent;
|
|
@ -1,75 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClientSchema, selectDownloadClientSchema } from 'Store/Actions/settingsActions';
|
||||
import AddDownloadClientModalContent from './AddDownloadClientModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.downloadClients,
|
||||
(downloadClients) => {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
} = downloadClients;
|
||||
|
||||
const usenetDownloadClients = _.filter(schema, { protocol: 'usenet' });
|
||||
const torrentDownloadClients = _.filter(schema, { protocol: 'torrent' });
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
usenetDownloadClients,
|
||||
torrentDownloadClients
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchDownloadClientSchema,
|
||||
selectDownloadClientSchema
|
||||
};
|
||||
|
||||
class AddDownloadClientModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchDownloadClientSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDownloadClientSelect = ({ implementation }) => {
|
||||
this.props.selectDownloadClientSchema({ implementation });
|
||||
this.props.onModalClose({ downloadClientSelected: true });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddDownloadClientModalContent
|
||||
{...this.props}
|
||||
onDownloadClientSelect={this.onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddDownloadClientModalContentConnector.propTypes = {
|
||||
fetchDownloadClientSchema: PropTypes.func.isRequired,
|
||||
selectDownloadClientSchema: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddDownloadClientModalContentConnector);
|
|
@ -1,49 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class AddDownloadClientPresetMenuItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
name,
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onPress({
|
||||
name,
|
||||
implementation
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
implementation,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
{...otherProps}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddDownloadClientPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddDownloadClientPresetMenuItem;
|
|
@ -0,0 +1,41 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import { selectDownloadClientSchema } from 'Store/Actions/settingsActions';
|
||||
|
||||
interface AddDownloadClientPresetMenuItemProps {
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function AddDownloadClientPresetMenuItem({
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: AddDownloadClientPresetMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(
|
||||
selectDownloadClientSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
presetName: name,
|
||||
})
|
||||
);
|
||||
|
||||
onPress();
|
||||
}, [name, implementation, implementationName, dispatch, onPress]);
|
||||
|
||||
return (
|
||||
<MenuItem {...otherProps} onPress={handlePress}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddDownloadClientPresetMenuItem;
|
|
@ -1,136 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
|
||||
import styles from './DownloadClient.css';
|
||||
|
||||
class DownloadClient extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditDownloadClientModalOpen: false,
|
||||
isDeleteDownloadClientModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditDownloadClientPress = () => {
|
||||
this.setState({ isEditDownloadClientModalOpen: true });
|
||||
};
|
||||
|
||||
onEditDownloadClientModalClose = () => {
|
||||
this.setState({ isEditDownloadClientModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteDownloadClientPress = () => {
|
||||
this.setState({
|
||||
isEditDownloadClientModalOpen: false,
|
||||
isDeleteDownloadClientModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteDownloadClientModalClose = () => {
|
||||
this.setState({ isDeleteDownloadClientModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteDownloadClient = () => {
|
||||
this.props.onConfirmDeleteDownloadClient(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
enable,
|
||||
priority,
|
||||
tags,
|
||||
tagList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.downloadClient}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditDownloadClientPress}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{
|
||||
enable ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Enabled')}
|
||||
</Label> :
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
priority > 1 &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
{translate('PrioritySettings', { priority })}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<TagList
|
||||
tags={tags}
|
||||
tagList={tagList}
|
||||
/>
|
||||
|
||||
<EditDownloadClientModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditDownloadClientModalOpen}
|
||||
onModalClose={this.onEditDownloadClientModalClose}
|
||||
onDeleteDownloadClientPress={this.onDeleteDownloadClientPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteDownloadClientModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteDownloadClient')}
|
||||
message={translate('DeleteDownloadClientMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteDownloadClient}
|
||||
onCancel={this.onDeleteDownloadClientModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClient.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
enable: PropTypes.bool.isRequired,
|
||||
priority: PropTypes.number.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DownloadClient;
|
|
@ -0,0 +1,105 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TagList from 'Components/TagList';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { deleteDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import useTags from 'Tags/useTags';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditDownloadClientModal from './EditDownloadClientModal';
|
||||
import styles from './DownloadClient.css';
|
||||
|
||||
interface DownloadClientProps {
|
||||
id: number;
|
||||
name: string;
|
||||
enable: boolean;
|
||||
priority: number;
|
||||
tags: number[];
|
||||
}
|
||||
|
||||
function DownloadClient({
|
||||
id,
|
||||
name,
|
||||
enable,
|
||||
priority,
|
||||
tags,
|
||||
}: DownloadClientProps) {
|
||||
const dispatch = useDispatch();
|
||||
const tagList = useTags();
|
||||
|
||||
const [isEditDownloadClientModalOpen, setIsEditDownloadClientModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isDeleteDownloadClientModalOpen, setIsDeleteDownloadClientModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleEditDownloadClientPress = useCallback(() => {
|
||||
setIsEditDownloadClientModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditDownloadClientModalClose = useCallback(() => {
|
||||
setIsEditDownloadClientModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteDownloadClientPress = useCallback(() => {
|
||||
setIsEditDownloadClientModalOpen(false);
|
||||
setIsDeleteDownloadClientModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteDownloadClientModalClose = useCallback(() => {
|
||||
setIsDeleteDownloadClientModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteDownloadClient = useCallback(() => {
|
||||
dispatch(deleteDownloadClient({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.downloadClient}
|
||||
overlayContent={true}
|
||||
onPress={handleEditDownloadClientPress}
|
||||
>
|
||||
<div className={styles.name}>{name}</div>
|
||||
|
||||
<div className={styles.enabled}>
|
||||
{enable ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
|
||||
) : (
|
||||
<Label kind={kinds.DISABLED} outline={true}>
|
||||
{translate('Disabled')}
|
||||
</Label>
|
||||
)}
|
||||
|
||||
{priority > 1 ? (
|
||||
<Label kind={kinds.DISABLED} outline={true}>
|
||||
{translate('PrioritySettings', { priority })}
|
||||
</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<TagList tags={tags} tagList={tagList} />
|
||||
|
||||
<EditDownloadClientModal
|
||||
id={id}
|
||||
isOpen={isEditDownloadClientModalOpen}
|
||||
onModalClose={handleEditDownloadClientModalClose}
|
||||
onDeleteDownloadClientPress={handleDeleteDownloadClientPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteDownloadClientModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteDownloadClient')}
|
||||
message={translate('DeleteDownloadClientMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleConfirmDeleteDownloadClient}
|
||||
onCancel={handleDeleteDownloadClientModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadClient;
|
|
@ -1,118 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientModal from './AddDownloadClientModal';
|
||||
import DownloadClient from './DownloadClient';
|
||||
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
|
||||
import styles from './DownloadClients.css';
|
||||
|
||||
class DownloadClients extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddDownloadClientModalOpen: false,
|
||||
isEditDownloadClientModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddDownloadClientPress = () => {
|
||||
this.setState({ isAddDownloadClientModalOpen: true });
|
||||
};
|
||||
|
||||
onAddDownloadClientModalClose = ({ downloadClientSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddDownloadClientModalOpen: false,
|
||||
isEditDownloadClientModalOpen: downloadClientSelected
|
||||
});
|
||||
};
|
||||
|
||||
onEditDownloadClientModalClose = () => {
|
||||
this.setState({ isEditDownloadClientModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
onConfirmDeleteDownloadClient,
|
||||
tagList,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddDownloadClientModalOpen,
|
||||
isEditDownloadClientModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('DownloadClients')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('DownloadClientsLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.downloadClients}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<DownloadClient
|
||||
key={item.id}
|
||||
{...item}
|
||||
tagList={tagList}
|
||||
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addDownloadClient}
|
||||
onPress={this.onAddDownloadClientPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddDownloadClientModal
|
||||
isOpen={isAddDownloadClientModalOpen}
|
||||
onModalClose={this.onAddDownloadClientModalClose}
|
||||
/>
|
||||
|
||||
<EditDownloadClientModalConnector
|
||||
isOpen={isEditDownloadClientModalOpen}
|
||||
onModalClose={this.onEditDownloadClientModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClients.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DownloadClients;
|
|
@ -0,0 +1,94 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import Card from 'Components/Card';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import DownloadClientModel from 'typings/DownloadClient';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddDownloadClientModal from './AddDownloadClientModal';
|
||||
import DownloadClient from './DownloadClient';
|
||||
import EditDownloadClientModal from './EditDownloadClientModal';
|
||||
import styles from './DownloadClients.css';
|
||||
|
||||
function DownloadClients() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { error, isFetching, isPopulated, items } = useSelector(
|
||||
createSortedSectionSelector<DownloadClientModel, DownloadClientAppState>(
|
||||
'settings.downloadClients',
|
||||
sortByProp('name')
|
||||
)
|
||||
);
|
||||
|
||||
const [isAddDownloadClientModalOpen, setIsAddDownloadClientModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isEditDownloadClientModalOpen, setIsEditDownloadClientModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleAddDownloadClientPress = useCallback(() => {
|
||||
setIsAddDownloadClientModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDownloadClientSelect = useCallback(() => {
|
||||
setIsAddDownloadClientModalOpen(false);
|
||||
setIsEditDownloadClientModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddDownloadClientModalClose = useCallback(() => {
|
||||
setIsAddDownloadClientModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleEditDownloadClientModalClose = useCallback(() => {
|
||||
setIsEditDownloadClientModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDownloadClients());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('DownloadClients')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('DownloadClientsLoadError')}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
>
|
||||
<div className={styles.downloadClients}>
|
||||
{items.map((item) => {
|
||||
return <DownloadClient key={item.id} {...item} />;
|
||||
})}
|
||||
|
||||
<Card
|
||||
className={styles.addDownloadClient}
|
||||
onPress={handleAddDownloadClientPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<AddDownloadClientModal
|
||||
isOpen={isAddDownloadClientModalOpen}
|
||||
onDownloadClientSelect={handleDownloadClientSelect}
|
||||
onModalClose={handleAddDownloadClientModalClose}
|
||||
/>
|
||||
|
||||
<EditDownloadClientModal
|
||||
isOpen={isEditDownloadClientModalOpen}
|
||||
onModalClose={handleEditDownloadClientModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadClients;
|
|
@ -1,63 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import DownloadClients from './DownloadClients';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByProp('name')),
|
||||
createTagsSelector(),
|
||||
(downloadClients, tagList) => {
|
||||
return {
|
||||
...downloadClients,
|
||||
tagList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchDownloadClients,
|
||||
deleteDownloadClient
|
||||
};
|
||||
|
||||
class DownloadClientsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchDownloadClients();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteDownloadClient = (id) => {
|
||||
this.props.deleteDownloadClient({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DownloadClients
|
||||
{...this.props}
|
||||
onConfirmDeleteDownloadClient={this.onConfirmDeleteDownloadClient}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClientsConnector.propTypes = {
|
||||
fetchDownloadClients: PropTypes.func.isRequired,
|
||||
deleteDownloadClient: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientsConnector);
|
|
@ -1,27 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditDownloadClientModalContentConnector from './EditDownloadClientModalContentConnector';
|
||||
|
||||
function EditDownloadClientModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditDownloadClientModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditDownloadClientModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditDownloadClientModal;
|
|
@ -0,0 +1,46 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
cancelSaveDownloadClient,
|
||||
cancelTestDownloadClient,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import EditDownloadClientModalContent, {
|
||||
EditDownloadClientModalContentProps,
|
||||
} from './EditDownloadClientModalContent';
|
||||
|
||||
const section = 'settings.downloadClients';
|
||||
|
||||
interface EditDownloadClientModalProps
|
||||
extends EditDownloadClientModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditDownloadClientModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditDownloadClientModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
dispatch(cancelTestDownloadClient({ section }));
|
||||
dispatch(cancelSaveDownloadClient({ section }));
|
||||
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditDownloadClientModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDownloadClientModal;
|
|
@ -1,65 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { cancelSaveDownloadClient, cancelTestDownloadClient } from 'Store/Actions/settingsActions';
|
||||
import EditDownloadClientModal from './EditDownloadClientModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.downloadClients';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelTestDownloadClient() {
|
||||
dispatch(cancelTestDownloadClient({ section }));
|
||||
},
|
||||
|
||||
dispatchCancelSaveDownloadClient() {
|
||||
dispatch(cancelSaveDownloadClient({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditDownloadClientModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.dispatchCancelTestDownloadClient();
|
||||
this.props.dispatchCancelSaveDownloadClient();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchCancelTestDownloadClient,
|
||||
dispatchCancelSaveDownloadClient,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditDownloadClientModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditDownloadClientModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchCancelTestDownloadClient: PropTypes.func.isRequired,
|
||||
dispatchCancelSaveDownloadClient: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditDownloadClientModalConnector);
|
|
@ -1,250 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
class EditDownloadClientModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting,
|
||||
saveError,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onTestPress,
|
||||
onDeleteDownloadClientPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
enable,
|
||||
protocol,
|
||||
priority,
|
||||
removeCompletedDownloads,
|
||||
removeFailedDownloads,
|
||||
fields,
|
||||
tags,
|
||||
message
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? translate('EditDownloadClientImplementation', { implementationName }) : translate('AddDownloadClientImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddDownloadClientError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
{
|
||||
!!message &&
|
||||
<Alert
|
||||
className={styles.message}
|
||||
kind={message.value.type}
|
||||
>
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enable"
|
||||
{...enable}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={advancedSettings}
|
||||
provider="downloadClient"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ClientPriority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('DownloadClientPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('DownloadClientSeriesTagHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FieldSet
|
||||
size={sizes.SMALL}
|
||||
legend={translate('CompletedDownloadHandling')}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeCompletedDownloads"
|
||||
helpText={translate('RemoveCompletedDownloadsHelpText')}
|
||||
{...removeCompletedDownloads}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
protocol.value !== 'torrent' &&
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFailedDownloads"
|
||||
helpText={translate('RemoveFailedDownloadsHelpText')}
|
||||
{...removeFailedDownloads}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
</FieldSet>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteDownloadClientPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<AdvancedSettingsButton
|
||||
showLabel={false}
|
||||
/>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={onTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditDownloadClientModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isTesting: PropTypes.bool.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onTestPress: PropTypes.func.isRequired,
|
||||
onDeleteDownloadClientPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditDownloadClientModalContent;
|
|
@ -0,0 +1,274 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
|
||||
import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditDownloadClientModalContent.css';
|
||||
|
||||
export interface EditDownloadClientModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteDownloadClientPress?: () => void;
|
||||
}
|
||||
|
||||
function EditDownloadClientModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
onDeleteDownloadClientPress,
|
||||
}: EditDownloadClientModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
isTesting = false,
|
||||
saveError,
|
||||
item,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(
|
||||
createProviderSettingsSelectorHook<DownloadClient, DownloadClientAppState>(
|
||||
'downloadClients',
|
||||
id
|
||||
)
|
||||
);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const {
|
||||
implementationName,
|
||||
name,
|
||||
enable,
|
||||
protocol,
|
||||
priority,
|
||||
removeCompletedDownloads,
|
||||
removeFailedDownloads,
|
||||
fields,
|
||||
tags,
|
||||
message,
|
||||
} = item;
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setDownloadClientValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setDownloadClientFieldValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleTestPress = useCallback(() => {
|
||||
dispatch(testDownloadClient({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveDownloadClient({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id
|
||||
? translate('EditDownloadClientImplementation', {
|
||||
implementationName,
|
||||
})
|
||||
: translate('AddDownloadClientImplementation', {
|
||||
implementationName,
|
||||
})}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddDownloadClientError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error ? (
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
{!!message && (
|
||||
<Alert className={styles.message} kind={message.value.type}>
|
||||
{message.value.message}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enable"
|
||||
{...enable}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={showAdvancedSettings}
|
||||
provider="downloadClient"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>{translate('ClientPriority')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('DownloadClientPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('DownloadClientSeriesTagHelpText')}
|
||||
{...tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FieldSet
|
||||
size={sizes.SMALL}
|
||||
legend={translate('CompletedDownloadHandling')}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeCompletedDownloads"
|
||||
helpText={translate('RemoveCompletedDownloadsHelpText')}
|
||||
{...removeCompletedDownloads}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{protocol.value === 'torrent' ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="removeFailedDownloads"
|
||||
helpText={translate('RemoveFailedDownloadsHelpText')}
|
||||
{...removeFailedDownloads}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
</FieldSet>
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteDownloadClientPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<AdvancedSettingsButton showLabel={false} />
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isTesting}
|
||||
error={saveError}
|
||||
onPress={handleTestPress}
|
||||
>
|
||||
{translate('Test')}
|
||||
</SpinnerErrorButton>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditDownloadClientModalContent;
|
|
@ -1,93 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import {
|
||||
saveDownloadClient,
|
||||
setDownloadClientFieldValue,
|
||||
setDownloadClientValue,
|
||||
testDownloadClient
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditDownloadClientModalContent from './EditDownloadClientModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('downloadClients'),
|
||||
(advancedSettings, downloadClient) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...downloadClient
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setDownloadClientValue,
|
||||
setDownloadClientFieldValue,
|
||||
saveDownloadClient,
|
||||
testDownloadClient
|
||||
};
|
||||
|
||||
class EditDownloadClientModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setDownloadClientValue({ name, value });
|
||||
};
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setDownloadClientFieldValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
onTestPress = () => {
|
||||
this.props.testDownloadClient({ id: this.props.id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditDownloadClientModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onTestPress={this.onTestPress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditDownloadClientModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setDownloadClientValue: PropTypes.func.isRequired,
|
||||
setDownloadClientFieldValue: PropTypes.func.isRequired,
|
||||
saveDownloadClient: PropTypes.func.isRequired,
|
||||
testDownloadClient: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditDownloadClientModalContentConnector);
|
|
@ -1,115 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function DownloadClientOptions(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
isFetching,
|
||||
error,
|
||||
settings,
|
||||
hasSettings,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('DownloadClientOptionsLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
hasSettings && !isFetching && !error && advancedSettings &&
|
||||
<div>
|
||||
<FieldSet legend={translate('CompletedDownloadHandling')}>
|
||||
|
||||
<Form>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableCompletedDownloadHandling"
|
||||
helpText={translate('EnableCompletedDownloadHandlingHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.enableCompletedDownloadHandling}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailed"
|
||||
helpText={translate('AutoRedownloadFailedHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
settings.autoRedownloadFailed.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailedFromInteractiveSearch"
|
||||
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</Form>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('RemoveDownloadsAlert')}
|
||||
</Alert>
|
||||
</FieldSet>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
DownloadClientOptions.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
hasSettings: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DownloadClientOptions;
|
|
@ -0,0 +1,149 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
fetchDownloadClientOptions,
|
||||
saveDownloadClientOptions,
|
||||
setDownloadClientOptionsValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const SECTION = 'downloadClientOptions';
|
||||
|
||||
interface DownloadClientOptionsProps {
|
||||
setChildSave(saveCallback: () => void): void;
|
||||
onChildStateChange(payload: unknown): void;
|
||||
}
|
||||
|
||||
function DownloadClientOptions({
|
||||
setChildSave,
|
||||
onChildStateChange,
|
||||
}: DownloadClientOptionsProps) {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isSaving,
|
||||
error,
|
||||
hasPendingChanges,
|
||||
hasSettings,
|
||||
settings,
|
||||
} = useSelector(createSettingsSectionSelector(SECTION));
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions aren't typed
|
||||
dispatch(setDownloadClientOptionsValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchDownloadClientOptions());
|
||||
setChildSave(() => dispatch(saveDownloadClientOptions()));
|
||||
|
||||
return () => {
|
||||
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
|
||||
};
|
||||
}, [dispatch, setChildSave]);
|
||||
|
||||
useEffect(() => {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges,
|
||||
});
|
||||
}, [onChildStateChange, isSaving, hasPendingChanges]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('DownloadClientOptionsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{hasSettings && isPopulated && !error && showAdvancedSettings ? (
|
||||
<div>
|
||||
<FieldSet legend={translate('CompletedDownloadHandling')}>
|
||||
<Form>
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('Enable')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableCompletedDownloadHandling"
|
||||
helpText={translate(
|
||||
'EnableCompletedDownloadHandlingHelpText'
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
{...settings.enableCompletedDownloadHandling}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailed"
|
||||
helpText={translate('AutoRedownloadFailedHelpText')}
|
||||
onChange={handleInputChange}
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{settings.autoRedownloadFailed.value ? (
|
||||
<FormGroup
|
||||
advancedSettings={showAdvancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>
|
||||
{translate('AutoRedownloadFailedFromInteractiveSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailedFromInteractiveSearch"
|
||||
helpText={translate(
|
||||
'AutoRedownloadFailedFromInteractiveSearchHelpText'
|
||||
)}
|
||||
onChange={handleInputChange}
|
||||
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
</Form>
|
||||
|
||||
<Alert kind={kinds.INFO}>{translate('RemoveDownloadsAlert')}</Alert>
|
||||
</FieldSet>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DownloadClientOptions;
|
|
@ -1,101 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchDownloadClientOptions, saveDownloadClientOptions, setDownloadClientOptionsValue } from 'Store/Actions/settingsActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import DownloadClientOptions from './DownloadClientOptions';
|
||||
|
||||
const SECTION = 'downloadClientOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(advancedSettings, sectionSettings) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchDownloadClientOptions: fetchDownloadClientOptions,
|
||||
dispatchSetDownloadClientOptionsValue: setDownloadClientOptionsValue,
|
||||
dispatchSaveDownloadClientOptions: saveDownloadClientOptions,
|
||||
dispatchClearPendingChanges: clearPendingChanges
|
||||
};
|
||||
|
||||
class DownloadClientOptionsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
dispatchFetchDownloadClientOptions,
|
||||
dispatchSaveDownloadClientOptions,
|
||||
onChildMounted
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchDownloadClientOptions();
|
||||
onChildMounted(dispatchSaveDownloadClientOptions);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
hasPendingChanges,
|
||||
isSaving,
|
||||
onChildStateChange
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.isSaving !== isSaving ||
|
||||
prevProps.hasPendingChanges !== hasPendingChanges
|
||||
) {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetDownloadClientOptionsValue({ name, value });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DownloadClientOptions
|
||||
onInputChange={this.onInputChange}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DownloadClientOptionsConnector.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
hasPendingChanges: PropTypes.bool.isRequired,
|
||||
dispatchFetchDownloadClientOptions: PropTypes.func.isRequired,
|
||||
dispatchSetDownloadClientOptionsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveDownloadClientOptions: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
onChildMounted: PropTypes.func.isRequired,
|
||||
onChildStateChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientOptionsConnector);
|
|
@ -1,27 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditRemotePathMappingModalContentConnector from './EditRemotePathMappingModalContentConnector';
|
||||
|
||||
function EditRemotePathMappingModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditRemotePathMappingModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditRemotePathMappingModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditRemotePathMappingModal;
|
|
@ -0,0 +1,37 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditRemotePathMappingModalContent, {
|
||||
EditRemotePathMappingModalContentProps,
|
||||
} from './EditRemotePathMappingModalContent';
|
||||
|
||||
interface EditRemotePathMappingModalProps
|
||||
extends EditRemotePathMappingModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditRemotePathMappingModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditRemotePathMappingModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'settings.remotePathMappings' }));
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditRemotePathMappingModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRemotePathMappingModal;
|
|
@ -1,43 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditRemotePathMappingModal from './EditRemotePathMappingModal';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditRemotePathMappingModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.remotePathMappings' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditRemotePathMappingModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditRemotePathMappingModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditRemotePathMappingModalConnector);
|
|
@ -1,154 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { stringSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditRemotePathMappingModalContent.css';
|
||||
|
||||
function EditRemotePathMappingModalContent(props) {
|
||||
const {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
downloadClientHosts,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteRemotePathMappingPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
host,
|
||||
remotePath,
|
||||
localPath
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? translate('EditRemotePathMapping') : translate('AddRemotePathMapping')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody className={styles.body}>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddRemotePathMappingError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error &&
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Host')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="host"
|
||||
helpText={translate('RemotePathMappingHostHelpText')}
|
||||
{...host}
|
||||
values={downloadClientHosts}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemotePath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="remotePath"
|
||||
helpText={translate('RemotePathMappingRemotePathHelpText')}
|
||||
{...remotePath}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('LocalPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="localPath"
|
||||
helpText={translate('RemotePathMappingLocalPathHelpText')}
|
||||
{...localPath}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteRemotePathMappingPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
const remotePathMappingShape = {
|
||||
host: PropTypes.shape(stringSettingShape).isRequired,
|
||||
remotePath: PropTypes.shape(stringSettingShape).isRequired,
|
||||
localPath: PropTypes.shape(stringSettingShape).isRequired
|
||||
};
|
||||
|
||||
EditRemotePathMappingModalContent.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(remotePathMappingShape).isRequired,
|
||||
downloadClientHosts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteRemotePathMappingPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditRemotePathMappingModalContent;
|
|
@ -0,0 +1,214 @@
|
|||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import useDownloadClientHostOptions from 'DownloadClient/useDownloadClientHostOptions';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
saveRemotePathMapping,
|
||||
setRemotePathMappingValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import RemotePathMapping from 'typings/Settings/RemotePathMapping';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditRemotePathMappingModalContent.css';
|
||||
|
||||
const newRemotePathMapping: RemotePathMapping & { [key: string]: unknown } = {
|
||||
id: 0,
|
||||
host: '',
|
||||
remotePath: '',
|
||||
localPath: '',
|
||||
};
|
||||
|
||||
function createRemotePathMappingSelector(id?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.remotePathMappings,
|
||||
(remotePathMappings) => {
|
||||
const { isFetching, error, isSaving, saveError, pendingChanges, items } =
|
||||
remotePathMappings;
|
||||
|
||||
const mapping = id
|
||||
? items.find((i) => i.id === id)!
|
||||
: newRemotePathMapping;
|
||||
|
||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export interface EditRemotePathMappingModalContentProps {
|
||||
id?: number;
|
||||
onDeleteRemotePathMappingPress?: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function EditRemotePathMappingModalContent({
|
||||
id,
|
||||
onDeleteRemotePathMappingPress,
|
||||
onModalClose,
|
||||
}: EditRemotePathMappingModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const downloadClientHosts = useDownloadClientHostOptions();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(createRemotePathMappingSelector(id));
|
||||
|
||||
const { host, remotePath, localPath } = item;
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setRemotePathMappingValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveRemotePathMapping({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) {
|
||||
Object.keys(newRemotePathMapping).forEach((name) => {
|
||||
if (name === 'id') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setRemotePathMappingValue({
|
||||
name,
|
||||
value: newRemotePathMapping[name],
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id
|
||||
? translate('EditRemotePathMapping')
|
||||
: translate('AddRemotePathMapping')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody className={styles.body}>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddRemotePathMappingError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error ? (
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Host')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="host"
|
||||
helpText={translate('RemotePathMappingHostHelpText')}
|
||||
{...host}
|
||||
values={downloadClientHosts}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemotePath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="remotePath"
|
||||
helpText={translate('RemotePathMappingRemotePathHelpText')}
|
||||
{...remotePath}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('LocalPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="localPath"
|
||||
helpText={translate('RemotePathMappingLocalPathHelpText')}
|
||||
includeFiles={false}
|
||||
{...localPath}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteRemotePathMappingPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRemotePathMappingModalContent;
|
|
@ -1,147 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveRemotePathMapping, setRemotePathMappingValue } from 'Store/Actions/settingsActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import EditRemotePathMappingModalContent from './EditRemotePathMappingModalContent';
|
||||
|
||||
const newRemotePathMapping = {
|
||||
host: '',
|
||||
remotePath: '',
|
||||
localPath: ''
|
||||
};
|
||||
|
||||
const selectDownloadClientHosts = createSelector(
|
||||
(state) => state.settings.downloadClients.items,
|
||||
(downloadClients) => {
|
||||
const hosts = downloadClients.reduce((acc, downloadClient) => {
|
||||
const name = downloadClient.name;
|
||||
const host = downloadClient.fields.find((field) => {
|
||||
return field.name === 'host';
|
||||
});
|
||||
|
||||
if (host) {
|
||||
const group = acc[host.value] = acc[host.value] || [];
|
||||
group.push(name);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return Object.keys(hosts).map((host) => {
|
||||
return {
|
||||
key: host,
|
||||
value: host,
|
||||
hint: `${hosts[host].join(', ')}`
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
function createRemotePathMappingSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.settings.remotePathMappings,
|
||||
selectDownloadClientHosts,
|
||||
(id, remotePathMappings, downloadClientHosts) => {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges,
|
||||
items
|
||||
} = remotePathMappings;
|
||||
|
||||
const mapping = id ? items.find((i) => i.id === id) : newRemotePathMapping;
|
||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
id,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
...settings,
|
||||
downloadClientHosts
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createRemotePathMappingSelector(),
|
||||
(remotePathMapping) => {
|
||||
return {
|
||||
...remotePathMapping
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetRemotePathMappingValue: setRemotePathMappingValue,
|
||||
dispatchSaveRemotePathMapping: saveRemotePathMapping
|
||||
};
|
||||
|
||||
class EditRemotePathMappingModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.id) {
|
||||
Object.keys(newRemotePathMapping).forEach((name) => {
|
||||
this.props.dispatchSetRemotePathMappingValue({
|
||||
name,
|
||||
value: newRemotePathMapping[name]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetRemotePathMappingValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveRemotePathMapping({ id: this.props.id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditRemotePathMappingModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditRemotePathMappingModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
dispatchSetRemotePathMappingValue: PropTypes.func.isRequired,
|
||||
dispatchSaveRemotePathMapping: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditRemotePathMappingModalContentConnector);
|
|
@ -1,115 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
|
||||
import styles from './RemotePathMapping.css';
|
||||
|
||||
class RemotePathMapping extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditRemotePathMappingModalOpen: false,
|
||||
isDeleteRemotePathMappingModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditRemotePathMappingPress = () => {
|
||||
this.setState({ isEditRemotePathMappingModalOpen: true });
|
||||
};
|
||||
|
||||
onEditRemotePathMappingModalClose = () => {
|
||||
this.setState({ isEditRemotePathMappingModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteRemotePathMappingPress = () => {
|
||||
this.setState({
|
||||
isEditRemotePathMappingModalOpen: false,
|
||||
isDeleteRemotePathMappingModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteRemotePathMappingModalClose = () => {
|
||||
this.setState({ isDeleteRemotePathMappingModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteRemotePathMapping = () => {
|
||||
this.props.onConfirmDeleteRemotePathMapping(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
host,
|
||||
remotePath,
|
||||
localPath
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.remotePathMapping
|
||||
)}
|
||||
>
|
||||
<div className={styles.host}>{host}</div>
|
||||
<div className={styles.path}>{remotePath}</div>
|
||||
<div className={styles.path}>{localPath}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link
|
||||
onPress={this.onEditRemotePathMappingPress}
|
||||
>
|
||||
<Icon name={icons.EDIT} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<EditRemotePathMappingModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditRemotePathMappingModalOpen}
|
||||
onModalClose={this.onEditRemotePathMappingModalClose}
|
||||
onDeleteRemotePathMappingPress={this.onDeleteRemotePathMappingPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteRemotePathMappingModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteRemotePathMapping')}
|
||||
message={translate('DeleteRemotePathMappingMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteRemotePathMapping}
|
||||
onCancel={this.onDeleteRemotePathMappingModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemotePathMapping.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
host: PropTypes.string.isRequired,
|
||||
remotePath: PropTypes.string.isRequired,
|
||||
localPath: PropTypes.string.isRequired,
|
||||
onConfirmDeleteRemotePathMapping: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
RemotePathMapping.defaultProps = {
|
||||
// The drag preview will not connect the drag handle.
|
||||
connectDragSource: (node) => node
|
||||
};
|
||||
|
||||
export default RemotePathMapping;
|
|
@ -0,0 +1,91 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { deleteRemotePathMapping } from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditRemotePathMappingModal from './EditRemotePathMappingModal';
|
||||
import styles from './RemotePathMapping.css';
|
||||
|
||||
interface RemotePathMappingProps {
|
||||
id: number;
|
||||
host: string;
|
||||
remotePath: string;
|
||||
localPath: string;
|
||||
}
|
||||
|
||||
function RemotePathMapping({
|
||||
id,
|
||||
host,
|
||||
remotePath,
|
||||
localPath,
|
||||
}: RemotePathMappingProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [
|
||||
isEditRemotePathMappingModalOpen,
|
||||
setIsEditRemotePathMappingModalOpen,
|
||||
] = useState(false);
|
||||
|
||||
const [
|
||||
isDeleteRemotePathMappingModalOpen,
|
||||
setIsDeleteRemotePathMappingModalOpen,
|
||||
] = useState(false);
|
||||
|
||||
const handleEditRemotePathMappingPress = useCallback(() => {
|
||||
setIsEditRemotePathMappingModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditRemotePathMappingModalClose = useCallback(() => {
|
||||
setIsEditRemotePathMappingModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteRemotePathMappingPress = useCallback(() => {
|
||||
setIsEditRemotePathMappingModalOpen(false);
|
||||
setIsDeleteRemotePathMappingModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteRemotePathMappingModalClose = useCallback(() => {
|
||||
setIsDeleteRemotePathMappingModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteRemotePathMapping = useCallback(() => {
|
||||
dispatch(deleteRemotePathMapping({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.remotePathMapping)}>
|
||||
<div className={styles.host}>{host}</div>
|
||||
<div className={styles.path}>{remotePath}</div>
|
||||
<div className={styles.path}>{localPath}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<Link onPress={handleEditRemotePathMappingPress}>
|
||||
<Icon name={icons.EDIT} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<EditRemotePathMappingModal
|
||||
id={id}
|
||||
isOpen={isEditRemotePathMappingModalOpen}
|
||||
onModalClose={handleEditRemotePathMappingModalClose}
|
||||
onDeleteRemotePathMappingPress={handleDeleteRemotePathMappingPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteRemotePathMappingModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteRemotePathMapping')}
|
||||
message={translate('DeleteRemotePathMappingMessageText')}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleConfirmDeleteRemotePathMapping}
|
||||
onCancel={handleDeleteRemotePathMappingModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemotePathMapping;
|
|
@ -1,114 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
|
||||
import RemotePathMapping from './RemotePathMapping';
|
||||
import styles from './RemotePathMappings.css';
|
||||
|
||||
class RemotePathMappings extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddRemotePathMappingModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddRemotePathMappingPress = () => {
|
||||
this.setState({ isAddRemotePathMappingModalOpen: true });
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isAddRemotePathMappingModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
onConfirmDeleteRemotePathMapping,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('RemotePathMappings')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('RemotePathMappingsLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<InlineMarkdown data={translate('RemotePathMappingsInfo', { wikiLink: 'https://wiki.servarr.com/sonarr/settings#remote-path-mappings' })} />
|
||||
</Alert>
|
||||
|
||||
<div className={styles.remotePathMappingsHeader}>
|
||||
<div className={styles.host}>
|
||||
{translate('Host')}
|
||||
</div>
|
||||
<div className={styles.path}>
|
||||
{translate('RemotePath')}
|
||||
</div>
|
||||
<div className={styles.path}>
|
||||
{translate('LocalPath')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<RemotePathMapping
|
||||
key={item.id}
|
||||
{...item}
|
||||
{...otherProps}
|
||||
index={index}
|
||||
onConfirmDeleteRemotePathMapping={onConfirmDeleteRemotePathMapping}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.addRemotePathMapping}>
|
||||
<Link
|
||||
className={styles.addButton}
|
||||
onPress={this.onAddRemotePathMappingPress}
|
||||
>
|
||||
<Icon name={icons.ADD} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<EditRemotePathMappingModalConnector
|
||||
isOpen={this.state.isAddRemotePathMappingModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemotePathMappings.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteRemotePathMapping: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemotePathMappings;
|
|
@ -0,0 +1,86 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { fetchRemotePathMappings } from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditRemotePathMappingModal from './EditRemotePathMappingModal';
|
||||
import RemotePathMapping from './RemotePathMapping';
|
||||
import styles from './RemotePathMappings.css';
|
||||
|
||||
function RemotePathMappings() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.settings.remotePathMappings
|
||||
);
|
||||
|
||||
const [isAddRemotePathMappingModalOpen, setIsAddRemotePathMappingModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleAddRemotePathMappingPress = useCallback(() => {
|
||||
setIsAddRemotePathMappingModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddRemotePathMappingModalClose = useCallback(() => {
|
||||
setIsAddRemotePathMappingModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRemotePathMappings());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('RemotePathMappings')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('RemotePathMappingsLoadError')}
|
||||
error={error}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<InlineMarkdown
|
||||
data={translate('RemotePathMappingsInfo', {
|
||||
wikiLink:
|
||||
'https://wiki.servarr.com/sonarr/settings#remote-path-mappings',
|
||||
})}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.remotePathMappingsHeader}>
|
||||
<div className={styles.host}>{translate('Host')}</div>
|
||||
<div className={styles.path}>{translate('RemotePath')}</div>
|
||||
<div className={styles.path}>{translate('LocalPath')}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{items.map((item) => {
|
||||
return <RemotePathMapping key={item.id} {...item} />;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={styles.addRemotePathMapping}>
|
||||
<Link
|
||||
className={styles.addButton}
|
||||
onPress={handleAddRemotePathMappingPress}
|
||||
>
|
||||
<Icon name={icons.ADD} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<EditRemotePathMappingModal
|
||||
isOpen={isAddRemotePathMappingModalOpen}
|
||||
onModalClose={handleAddRemotePathMappingModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemotePathMappings;
|
|
@ -1,59 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteRemotePathMapping, fetchRemotePathMappings } from 'Store/Actions/settingsActions';
|
||||
import RemotePathMappings from './RemotePathMappings';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.remotePathMappings,
|
||||
(remotePathMappings) => {
|
||||
return {
|
||||
...remotePathMappings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchRemotePathMappings: fetchRemotePathMappings,
|
||||
dispatchDeleteRemotePathMapping: deleteRemotePathMapping
|
||||
};
|
||||
|
||||
class RemotePathMappingsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRemotePathMappings();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteRemotePathMapping = (id) => {
|
||||
this.props.dispatchDeleteRemotePathMapping({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RemotePathMappings
|
||||
{...this.state}
|
||||
{...this.props}
|
||||
onConfirmDeleteRemotePathMapping={this.onConfirmDeleteRemotePathMapping}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemotePathMappingsConnector.propTypes = {
|
||||
dispatchFetchRemotePathMappings: PropTypes.func.isRequired,
|
||||
dispatchDeleteRemotePathMapping: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(RemotePathMappingsConnector);
|
|
@ -210,6 +210,10 @@ function EditDelayProfileModalContent({
|
|||
useEffect(() => {
|
||||
if (!id) {
|
||||
Object.keys(newDelayProfile).forEach((name) => {
|
||||
if (name === 'id') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setDelayProfileValue({
|
||||
|
|
6
frontend/src/typings/Settings/DownloadClientOptions.ts
Normal file
6
frontend/src/typings/Settings/DownloadClientOptions.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default interface DownloadClientOptions {
|
||||
downloadClientWorkingFolders: string;
|
||||
enableCompletedDownloadHandling: boolean;
|
||||
autoRedownloadFailed: boolean;
|
||||
autoRedownloadFailedFromInteractiveSearch: boolean;
|
||||
}
|
9
frontend/src/typings/Settings/RemotePathMapping.ts
Normal file
9
frontend/src/typings/Settings/RemotePathMapping.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface RemotePathMapping extends ModelBase {
|
||||
host: string;
|
||||
localPath: string;
|
||||
remotePath: string;
|
||||
}
|
||||
|
||||
export default RemotePathMapping;
|
Loading…
Add table
Add a link
Reference in a new issue