1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-24 22:36:19 -04:00
This commit is contained in:
Mark McDowall
2018-01-12 18:01:27 -08:00
committed by Taloth Saldono
parent 99feff549d
commit 5894b4fd95
1183 changed files with 91622 additions and 4978 deletions
@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal';
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,43 @@
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);
@@ -0,0 +1,12 @@
.body {
composes: modalBody from 'Components/Modal/ModalBody.css';
flex: 1 1 430px;
}
.deleteButton {
composes: button from 'Components/Link/Button.css';
margin-right: auto;
}
@@ -0,0 +1,152 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import { stringSettingShape } from 'Helpers/Props/Shapes/settingShape';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
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 ? 'Edit Remote Path Mapping' : 'Add Remote Path Mapping'}
</ModalHeader>
<ModalBody className={styles.body}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to add a new remote path mapping, please try again.</div>
}
{
!isFetching && !error &&
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Host</FormLabel>
<FormInputGroup
type={inputTypes.AUTO_COMPLETE}
name="host"
helpText="The same host you specified for the remote Download Client"
{...host}
values={downloadClientHosts}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Remote Path</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="remotePath"
helpText="Root path to the directory that the Download Client accesses"
{...remotePath}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Local Path</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
name="localPath"
helpText="Path that Sonarr should use to access the remote path locally"
{...localPath}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteRemotePathMappingPress}
>
Delete
</Button>
}
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
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.string).isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteRemotePathMappingPress: PropTypes.func
};
export default EditRemotePathMappingModalContent;
@@ -0,0 +1,138 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import selectSettings from 'Store/Selectors/selectSettings';
import { setRemotePathMappingValue, saveRemotePathMapping } from 'Store/Actions/settingsActions';
import EditRemotePathMappingModalContent from './EditRemotePathMappingModalContent';
const newRemotePathMapping = {
host: '',
remotePath: '',
localPath: ''
};
const selectDownloadClientHosts = createSelector(
(state) => state.settings.downloadClients.items,
(downloadClients) => {
return downloadClients.reduce((acc, downloadClient) => {
const host = downloadClient.fields.find((field) => {
return field.name === 'host';
});
if (host && !acc.includes(host.value)) {
acc.push(host.value);
}
return acc;
}, []);
}
);
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 ? _.find(items, { 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);
@@ -0,0 +1,23 @@
.remotePathMapping {
display: flex;
align-items: stretch;
margin-bottom: 10px;
height: 30px;
border-bottom: 1px solid $borderColor;
line-height: 30px;
}
.host {
flex: 0 0 300px;
}
.path {
flex: 0 0 400px;
}
.actions {
display: flex;
justify-content: flex-end;
flex: 1 0 auto;
padding-right: 10px;
}
@@ -0,0 +1,114 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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="Delete Delay Profile"
message="Are you sure you want to delete this remote path mapping?"
confirmLabel="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,23 @@
.remotePathMappingsHeader {
display: flex;
margin-bottom: 10px;
font-weight: bold;
}
.host {
flex: 0 0 300px;
}
.path {
flex: 0 0 400px;
}
.addRemotePathMapping {
display: flex;
justify-content: flex-end;
padding-right: 10px;
}
.addButton {
text-align: center;
}
@@ -0,0 +1,100 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import PageSectionContent from 'Components/Page/PageSectionContent';
import RemotePathMapping from './RemotePathMapping';
import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector';
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="Remote Path Mappings">
<PageSectionContent
errorMessage="Unable to load Remote Path Mappings"
{...otherProps}
>
<div className={styles.remotePathMappingsHeader}>
<div className={styles.host}>Host</div>
<div className={styles.path}>Remote Path</div>
<div className={styles.path}>Local Path</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,59 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchRemotePathMappings, deleteRemotePathMapping } 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);