parent
1765feac03
commit
4c6d6b726e
@ -1,175 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import ExportCustomFormatModal from './ExportCustomFormatModal';
|
||||
import styles from './CustomFormat.css';
|
||||
|
||||
class CustomFormat extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditCustomFormatModalOpen: false,
|
||||
isExportCustomFormatModalOpen: false,
|
||||
isDeleteCustomFormatModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditCustomFormatPress = () => {
|
||||
this.setState({ isEditCustomFormatModalOpen: true });
|
||||
};
|
||||
|
||||
onEditCustomFormatModalClose = () => {
|
||||
this.setState({ isEditCustomFormatModalOpen: false });
|
||||
};
|
||||
|
||||
onExportCustomFormatPress = () => {
|
||||
this.setState({ isExportCustomFormatModalOpen: true });
|
||||
};
|
||||
|
||||
onExportCustomFormatModalClose = () => {
|
||||
this.setState({ isExportCustomFormatModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteCustomFormatPress = () => {
|
||||
this.setState({
|
||||
isEditCustomFormatModalOpen: false,
|
||||
isDeleteCustomFormatModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteCustomFormatModalClose = () => {
|
||||
this.setState({ isDeleteCustomFormatModalOpen: false });
|
||||
};
|
||||
|
||||
onConfirmDeleteCustomFormat = () => {
|
||||
this.props.onConfirmDeleteCustomFormat(this.props.id);
|
||||
};
|
||||
|
||||
onCloneCustomFormatPress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneCustomFormatPress
|
||||
} = this.props;
|
||||
|
||||
onCloneCustomFormatPress(id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
specifications,
|
||||
isDeleting
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.customFormat}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditCustomFormatPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneCustomFormat')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneCustomFormatPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('ExportCustomFormat')}
|
||||
name={icons.EXPORT}
|
||||
onPress={this.onExportCustomFormatPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{
|
||||
specifications.map((item, index) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
if (item.required) {
|
||||
kind = kinds.SUCCESS;
|
||||
}
|
||||
if (item.negate) {
|
||||
kind = kinds.DANGER;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={styles.label}
|
||||
key={index}
|
||||
kind={kind}
|
||||
>
|
||||
{item.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditCustomFormatModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditCustomFormatModalOpen}
|
||||
onModalClose={this.onEditCustomFormatModalClose}
|
||||
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
||||
/>
|
||||
|
||||
<ExportCustomFormatModal
|
||||
id={id}
|
||||
isOpen={this.state.isExportCustomFormatModalOpen}
|
||||
onModalClose={this.onExportCustomFormatModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCustomFormat')}
|
||||
message={translate('DeleteCustomFormatMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||
onCancel={this.onDeleteCustomFormatModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFormat.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
specifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFormat;
|
@ -0,0 +1,149 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
|
||||
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||
import ExportCustomFormatModal from './ExportCustomFormatModal';
|
||||
import styles from './CustomFormat.css';
|
||||
|
||||
interface CustomFormatProps {
|
||||
id: number;
|
||||
name: string;
|
||||
specifications: CustomFormatSpecification[];
|
||||
isDeleting: boolean;
|
||||
onCloneCustomFormatPress: (id: number) => void;
|
||||
}
|
||||
|
||||
function CustomFormat({
|
||||
id,
|
||||
name,
|
||||
specifications,
|
||||
isDeleting,
|
||||
onCloneCustomFormatPress,
|
||||
}: CustomFormatProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] =
|
||||
useState(false);
|
||||
const [isExportCustomFormatModalOpen, setIsExportCustomFormatModalOpen] =
|
||||
useState(false);
|
||||
const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const onEditCustomFormatPress = useCallback(() => {
|
||||
setIsEditCustomFormatModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditCustomFormatModalClose = useCallback(() => {
|
||||
setIsEditCustomFormatModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteCustomFormatPress = useCallback(() => {
|
||||
setIsEditCustomFormatModalOpen(false);
|
||||
setIsDeleteCustomFormatModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteCustomFormatModalClose = useCallback(() => {
|
||||
setIsDeleteCustomFormatModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDeleteCustomFormatHandler = useCallback(() => {
|
||||
dispatch(deleteCustomFormat({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleCloneCustomFormatPressHandler = useCallback(() => {
|
||||
onCloneCustomFormatPress(id);
|
||||
}, [id, onCloneCustomFormatPress]);
|
||||
|
||||
const handleExportCustomFormatPress = useCallback(() => {
|
||||
setIsExportCustomFormatModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleExportCustomFormatModalClose = useCallback(() => {
|
||||
setIsExportCustomFormatModalOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.customFormat}
|
||||
overlayContent={true}
|
||||
onPress={onEditCustomFormatPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneCustomFormat')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneCustomFormatPressHandler}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('ExportCustomFormat')}
|
||||
name={icons.EXPORT}
|
||||
onPress={handleExportCustomFormatPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{specifications.map((item, index) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let kind: Kind = kinds.DEFAULT;
|
||||
|
||||
if (item.required) {
|
||||
kind = kinds.SUCCESS;
|
||||
}
|
||||
if (item.negate) {
|
||||
kind = kinds.DANGER;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label key={index} className={styles.label} kind={kind}>
|
||||
{item.name}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<EditCustomFormatModal
|
||||
id={id}
|
||||
isOpen={isEditCustomFormatModalOpen}
|
||||
onModalClose={handleEditCustomFormatModalClose}
|
||||
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
|
||||
/>
|
||||
|
||||
<ExportCustomFormatModal
|
||||
id={id}
|
||||
isOpen={isExportCustomFormatModalOpen}
|
||||
onModalClose={handleExportCustomFormatModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteCustomFormatModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCustomFormat')}
|
||||
message={translate('DeleteCustomFormatMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={handleConfirmDeleteCustomFormatHandler}
|
||||
onCancel={handleDeleteCustomFormatModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomFormat;
|
@ -1,116 +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 CustomFormat from './CustomFormat';
|
||||
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import styles from './CustomFormats.css';
|
||||
|
||||
class CustomFormats extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isCustomFormatModalOpen: false,
|
||||
tagsFromId: undefined
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCloneCustomFormatPress = (id) => {
|
||||
this.props.onCloneCustomFormatPress(id);
|
||||
this.setState({
|
||||
isCustomFormatModalOpen: true,
|
||||
tagsFromId: id
|
||||
});
|
||||
};
|
||||
|
||||
onEditCustomFormatPress = () => {
|
||||
this.setState({ isCustomFormatModalOpen: true });
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({
|
||||
isCustomFormatModalOpen: false,
|
||||
tagsFromId: undefined
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isDeleting,
|
||||
onConfirmDeleteCustomFormat,
|
||||
onCloneCustomFormatPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('CustomFormats')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('CustomFormatsLoadError')}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<CustomFormat
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteCustomFormat={onConfirmDeleteCustomFormat}
|
||||
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addCustomFormat}
|
||||
onPress={this.onEditCustomFormatPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditCustomFormatModalConnector
|
||||
isOpen={this.state.isCustomFormatModalOpen}
|
||||
tagsFromId={this.state.tagsFromId}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFormats.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFormats;
|
@ -0,0 +1,97 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CustomFormatAppState } 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 {
|
||||
cloneCustomFormat,
|
||||
fetchCustomFormats,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import CustomFormatModel from 'typings/CustomFormat';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFormat from './CustomFormat';
|
||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||
import styles from './CustomFormats.css';
|
||||
|
||||
function CustomFormats() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { error, isFetching, isPopulated, isDeleting, items } = useSelector(
|
||||
createSortedSectionSelector<CustomFormatModel, CustomFormatAppState>(
|
||||
'settings.customFormats',
|
||||
sortByProp('name')
|
||||
)
|
||||
);
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [clonedId, setClonedId] = useState<number>();
|
||||
|
||||
const handleAddCustomFormatPress = useCallback(() => {
|
||||
setIsEditModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleCloneCustomFormatPress = useCallback(
|
||||
(id: number) => {
|
||||
dispatch(cloneCustomFormat({ id }));
|
||||
|
||||
setIsEditModalOpen(true);
|
||||
setClonedId(id);
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleEditModalClose = useCallback(() => {
|
||||
setIsEditModalOpen(false);
|
||||
setClonedId(undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCustomFormats());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<FieldSet legend={translate('CustomFormats')}>
|
||||
<PageSectionContent
|
||||
errorMessage={translate('CustomFormatsLoadError')}
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
error={error}
|
||||
>
|
||||
<div className={styles.customFormats}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<CustomFormat
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onCloneCustomFormatPress={handleCloneCustomFormatPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Card
|
||||
className={styles.addCustomFormat}
|
||||
onPress={handleAddCustomFormatPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<EditCustomFormatModal
|
||||
isOpen={isEditModalOpen}
|
||||
clonedId={clonedId}
|
||||
onModalClose={handleEditModalClose}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomFormats;
|
@ -1,63 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneCustomFormat, deleteCustomFormat, fetchCustomFormats } from 'Store/Actions/settingsActions';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import CustomFormats from './CustomFormats';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.customFormats', sortByProp('name')),
|
||||
(customFormats) => customFormats
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchCustomFormats: fetchCustomFormats,
|
||||
dispatchDeleteCustomFormat: deleteCustomFormat,
|
||||
dispatchCloneCustomFormat: cloneCustomFormat
|
||||
};
|
||||
|
||||
class CustomFormatsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchCustomFormats();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteCustomFormat = (id) => {
|
||||
this.props.dispatchDeleteCustomFormat({ id });
|
||||
};
|
||||
|
||||
onCloneCustomFormatPress = (id) => {
|
||||
this.props.dispatchCloneCustomFormat({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CustomFormats
|
||||
onConfirmDeleteCustomFormat={this.onConfirmDeleteCustomFormat}
|
||||
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFormatsConnector.propTypes = {
|
||||
dispatchFetchCustomFormats: PropTypes.func.isRequired,
|
||||
dispatchDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
dispatchCloneCustomFormat: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFormatsConnector);
|
@ -1,61 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||
|
||||
class EditCustomFormatModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
height: 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onContentHeightChange = (height) => {
|
||||
if (this.state.height === 'auto' || height > this.state.height) {
|
||||
this.setState({ height });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ height: `${this.state.height}px` }}
|
||||
isOpen={isOpen}
|
||||
size={sizes.LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditCustomFormatModalContentConnector
|
||||
{...otherProps}
|
||||
onContentHeightChange={this.onContentHeightChange}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCustomFormatModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditCustomFormatModal;
|
@ -0,0 +1,36 @@
|
||||
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 EditCustomFormatModalContent, {
|
||||
EditCustomFormatModalContentProps,
|
||||
} from './EditCustomFormatModalContent';
|
||||
|
||||
interface EditCustomFormatModalProps extends EditCustomFormatModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditCustomFormatModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditCustomFormatModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'settings.customFormats' }));
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={handleModalClose}>
|
||||
<EditCustomFormatModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditCustomFormatModal;
|
@ -1,45 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||
|
||||
function mapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditCustomFormatModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.customFormats' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditCustomFormatModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCustomFormatModalConnector.propTypes = {
|
||||
...EditCustomFormatModalContentConnector.propTypes,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(EditCustomFormatModalConnector);
|
@ -1,263 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Card from 'Components/Card';
|
||||
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 Icon from 'Components/Icon';
|
||||
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 { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportCustomFormatModal from './ImportCustomFormatModal';
|
||||
import AddSpecificationModal from './Specifications/AddSpecificationModal';
|
||||
import EditSpecificationModalConnector from './Specifications/EditSpecificationModalConnector';
|
||||
import Specification from './Specifications/Specification';
|
||||
import styles from './EditCustomFormatModalContent.css';
|
||||
|
||||
class EditCustomFormatModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddSpecificationModalOpen: false,
|
||||
isEditSpecificationModalOpen: false,
|
||||
isImportCustomFormatModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAddSpecificationPress = () => {
|
||||
this.setState({ isAddSpecificationModalOpen: true });
|
||||
};
|
||||
|
||||
onAddSpecificationModalClose = ({ specificationSelected = false } = {}) => {
|
||||
this.setState({
|
||||
isAddSpecificationModalOpen: false,
|
||||
isEditSpecificationModalOpen: specificationSelected
|
||||
});
|
||||
};
|
||||
|
||||
onEditSpecificationModalClose = () => {
|
||||
this.setState({ isEditSpecificationModalOpen: false });
|
||||
};
|
||||
|
||||
onImportPress = () => {
|
||||
this.setState({ isImportCustomFormatModalOpen: true });
|
||||
};
|
||||
|
||||
onImportCustomFormatModalClose = () => {
|
||||
this.setState({ isImportCustomFormatModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
specificationsPopulated,
|
||||
specifications,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteCustomFormatPress,
|
||||
onCloneSpecificationPress,
|
||||
onConfirmDeleteSpecification,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isAddSpecificationModalOpen,
|
||||
isEditSpecificationModalOpen,
|
||||
isImportCustomFormatModalOpen
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
includeCustomFormatWhenRenaming
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
||||
<ModalHeader>
|
||||
{id ? translate('EditCustomFormat') : translate('AddCustomFormat')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddCustomFormatError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && specificationsPopulated &&
|
||||
<div>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Name')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('IncludeCustomFormatWhenRenaming')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeCustomFormatWhenRenaming"
|
||||
helpText={translate('IncludeCustomFormatWhenRenamingHelpText')}
|
||||
{...includeCustomFormatWhenRenaming}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('CustomFormatsSettingsTriggerInfo')}
|
||||
</div>
|
||||
</Alert>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
specifications.map((tag) => {
|
||||
return (
|
||||
<Specification
|
||||
key={tag.id}
|
||||
{...tag}
|
||||
onCloneSpecificationPress={onCloneSpecificationPress}
|
||||
onConfirmDeleteSpecification={onConfirmDeleteSpecification}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addSpecification}
|
||||
onPress={this.onAddSpecificationPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<AddSpecificationModal
|
||||
isOpen={isAddSpecificationModalOpen}
|
||||
onModalClose={this.onAddSpecificationModalClose}
|
||||
/>
|
||||
|
||||
<EditSpecificationModalConnector
|
||||
isOpen={isEditSpecificationModalOpen}
|
||||
onModalClose={this.onEditSpecificationModalClose}
|
||||
/>
|
||||
|
||||
<ImportCustomFormatModal
|
||||
isOpen={isImportCustomFormatModalOpen}
|
||||
onModalClose={this.onImportCustomFormatModalClose}
|
||||
/>
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div className={styles.rightButtons}>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteCustomFormatPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
onPress={this.onImportPress}
|
||||
>
|
||||
{translate('Import')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCustomFormatModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
specificationsPopulated: PropTypes.bool.isRequired,
|
||||
specifications: PropTypes.arrayOf(PropTypes.object),
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onContentHeightChange: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteCustomFormatPress: PropTypes.func,
|
||||
onCloneSpecificationPress: PropTypes.func.isRequired,
|
||||
onConfirmDeleteSpecification: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditCustomFormatModalContent;
|
@ -0,0 +1,242 @@
|
||||
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 Card from 'Components/Card';
|
||||
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 Icon from 'Components/Icon';
|
||||
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 { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
fetchCustomFormatSpecifications,
|
||||
saveCustomFormat,
|
||||
setCustomFormatValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportCustomFormatModal from './ImportCustomFormatModal';
|
||||
import AddSpecificationModal from './Specifications/AddSpecificationModal';
|
||||
import EditSpecificationModal from './Specifications/EditSpecificationModal';
|
||||
import Specification from './Specifications/Specification';
|
||||
import styles from './EditCustomFormatModalContent.css';
|
||||
|
||||
export interface EditCustomFormatModalContentProps {
|
||||
id?: number;
|
||||
clonedId?: number;
|
||||
onDeleteCustomFormatPress?: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function EditCustomFormatModalContent({
|
||||
id,
|
||||
clonedId,
|
||||
onDeleteCustomFormatPress,
|
||||
onModalClose,
|
||||
}: EditCustomFormatModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
} = useSelector(createProviderSettingsSelectorHook('customFormats', id));
|
||||
|
||||
const { isPopulated: isSpecificationsPopulated, items: specifications } =
|
||||
useSelector((state: AppState) => state.settings.customFormatSpecifications);
|
||||
|
||||
const [isAddSpecificationModalOpen, setIsAddSpecificationModalOpen] =
|
||||
useState(false);
|
||||
const [isEditSpecificationModalOpen, setIsEditSpecificationModalOpen] =
|
||||
useState(false);
|
||||
const [isImportCustomFormatModalOpen, setIsImportCustomFormatModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const { name, includeCustomFormatWhenRenaming } = item;
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const handleAddSpecificationPress = useCallback(() => {
|
||||
setIsAddSpecificationModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAddSpecificationModalClose = useCallback(() => {
|
||||
setIsAddSpecificationModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSpecificationSelect = useCallback(() => {
|
||||
setIsAddSpecificationModalOpen(false);
|
||||
setIsEditSpecificationModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditSpecificationModalClose = useCallback(() => {
|
||||
setIsEditSpecificationModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleImportPress = useCallback(() => {
|
||||
setIsImportCustomFormatModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleImportCustomFormatModalClose = useCallback(() => {
|
||||
setIsImportCustomFormatModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setCustomFormatValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveCustomFormat({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCustomFormatSpecifications({ id: clonedId || id }));
|
||||
}, [id, clonedId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && wasSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? translate('EditCustomFormat') : translate('AddCustomFormat')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddCustomFormatError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error && isSpecificationsPopulated ? (
|
||||
<div>
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('IncludeCustomFormatWhenRenaming')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeCustomFormatWhenRenaming"
|
||||
helpText={translate(
|
||||
'IncludeCustomFormatWhenRenamingHelpText'
|
||||
)}
|
||||
{...includeCustomFormatWhenRenaming}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
|
||||
<FieldSet legend={translate('Conditions')}>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('CustomFormatsSettingsTriggerInfo')}</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.customFormats}>
|
||||
{specifications.map((tag) => {
|
||||
return <Specification key={tag.id} {...tag} />;
|
||||
})}
|
||||
|
||||
<Card
|
||||
className={styles.addSpecification}
|
||||
onPress={handleAddSpecificationPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon name={icons.ADD} size={45} />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<AddSpecificationModal
|
||||
isOpen={isAddSpecificationModalOpen}
|
||||
onSpecificationSelect={handleSpecificationSelect}
|
||||
onModalClose={handleAddSpecificationModalClose}
|
||||
/>
|
||||
|
||||
<EditSpecificationModal
|
||||
isOpen={isEditSpecificationModalOpen}
|
||||
onModalClose={handleEditSpecificationModalClose}
|
||||
/>
|
||||
|
||||
<ImportCustomFormatModal
|
||||
isOpen={isImportCustomFormatModalOpen}
|
||||
onModalClose={handleImportCustomFormatModalClose}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<div className={styles.rightButtons}>
|
||||
{id ? (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteCustomFormatPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
<Button className={styles.deleteButton} onPress={handleImportPress}>
|
||||
{translate('Import')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditCustomFormatModalContent;
|
@ -1,102 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cloneCustomFormatSpecification, deleteCustomFormatSpecification, fetchCustomFormatSpecifications, saveCustomFormat, setCustomFormatValue } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditCustomFormatModalContent from './EditCustomFormatModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('customFormats'),
|
||||
(state) => state.settings.customFormatSpecifications,
|
||||
(advancedSettings, customFormat, specifications) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...customFormat,
|
||||
specificationsPopulated: specifications.isPopulated,
|
||||
specifications: specifications.items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setCustomFormatValue,
|
||||
saveCustomFormat,
|
||||
fetchCustomFormatSpecifications,
|
||||
cloneCustomFormatSpecification,
|
||||
deleteCustomFormatSpecification
|
||||
};
|
||||
|
||||
class EditCustomFormatModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
tagsFromId
|
||||
} = this.props;
|
||||
this.props.fetchCustomFormatSpecifications({ id: tagsFromId || id });
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setCustomFormatValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveCustomFormat({ id: this.props.id });
|
||||
};
|
||||
|
||||
onCloneSpecificationPress = (id) => {
|
||||
this.props.cloneCustomFormatSpecification({ id });
|
||||
};
|
||||
|
||||
onConfirmDeleteSpecification = (id) => {
|
||||
this.props.deleteCustomFormatSpecification({ id });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditCustomFormatModalContent
|
||||
{...this.props}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onCloneSpecificationPress={this.onCloneSpecificationPress}
|
||||
onConfirmDeleteSpecification={this.onConfirmDeleteSpecification}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCustomFormatModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
tagsFromId: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.object.isRequired,
|
||||
setCustomFormatValue: PropTypes.func.isRequired,
|
||||
saveCustomFormat: PropTypes.func.isRequired,
|
||||
fetchCustomFormatSpecifications: PropTypes.func.isRequired,
|
||||
cloneCustomFormatSpecification: PropTypes.func.isRequired,
|
||||
deleteCustomFormatSpecification: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditCustomFormatModalContentConnector);
|
@ -1,61 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ExportCustomFormatModalContentConnector from './ExportCustomFormatModalContentConnector';
|
||||
|
||||
class ExportCustomFormatModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
height: 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onContentHeightChange = (height) => {
|
||||
if (this.state.height === 'auto' || height > this.state.height) {
|
||||
this.setState({ height });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ height: `${this.state.height}px` }}
|
||||
isOpen={isOpen}
|
||||
size={sizes.LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ExportCustomFormatModalContentConnector
|
||||
{...otherProps}
|
||||
onContentHeightChange={this.onContentHeightChange}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExportCustomFormatModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ExportCustomFormatModal;
|
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ExportCustomFormatModalContent, {
|
||||
ExportCustomFormatModalContentProps,
|
||||
} from './ExportCustomFormatModalContent';
|
||||
|
||||
interface ExportCustomFormatModalProps
|
||||
extends ExportCustomFormatModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function ExportCustomFormatModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: ExportCustomFormatModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||
<ExportCustomFormatModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportCustomFormatModal;
|
@ -1,86 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
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 styles from './ExportCustomFormatModalContent.css';
|
||||
|
||||
class ExportCustomFormatModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
json,
|
||||
specificationsPopulated,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
||||
<ModalHeader>
|
||||
{translate('ExportCustomFormat')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('CustomFormatsLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && specificationsPopulated &&
|
||||
<div>
|
||||
<pre>
|
||||
{json}
|
||||
</pre>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<ClipboardButton
|
||||
className={styles.button}
|
||||
value={json}
|
||||
title={translate('CopyToClipboard')}
|
||||
kind={kinds.DEFAULT}
|
||||
/>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExportCustomFormatModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
json: PropTypes.string.isRequired,
|
||||
specificationsPopulated: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ExportCustomFormatModalContent;
|
@ -0,0 +1,108 @@
|
||||
import React, { 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 Button from 'Components/Link/Button';
|
||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||
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 { fetchCustomFormatSpecifications } from 'Store/Actions/settingsActions';
|
||||
import Field from 'typings/Field';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ExportCustomFormatModalContent.css';
|
||||
|
||||
const omittedProperties = ['id', 'implementationName', 'infoLink'];
|
||||
|
||||
function replacer(key: string, value: unknown) {
|
||||
if (omittedProperties.includes(key)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (key === 'fields') {
|
||||
return (value as Field[]).reduce<Record<string, unknown>>((acc, cur) => {
|
||||
acc[cur.name] = cur.value;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function createCustomFormatJsonSelector(id: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.customFormats,
|
||||
(customFormats) => {
|
||||
const customFormat = customFormats.items.find((i) => i.id === id);
|
||||
|
||||
const json = customFormat
|
||||
? JSON.stringify(customFormat, replacer, 2)
|
||||
: '';
|
||||
|
||||
return json;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExportCustomFormatModalContentProps {
|
||||
id: number;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function ExportCustomFormatModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
}: ExportCustomFormatModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, error } = useSelector(
|
||||
(state: AppState) => state.settings.customFormats
|
||||
);
|
||||
|
||||
const json = useSelector(createCustomFormatJsonSelector(id));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCustomFormatSpecifications({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('ExportCustomFormat')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('CustomFormatsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error ? (
|
||||
<div>
|
||||
<pre>{json}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<ClipboardButton
|
||||
className={styles.button}
|
||||
value={json}
|
||||
title={translate('CopyToClipboard')}
|
||||
kind={kinds.DEFAULT}
|
||||
/>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExportCustomFormatModalContent;
|
@ -1,83 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCustomFormatSpecifications } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import ExportCustomFormatModalContent from './ExportCustomFormatModalContent';
|
||||
|
||||
const omittedProperties = ['id', 'implementationName', 'infoLink'];
|
||||
|
||||
function replacer(key, value) {
|
||||
if (omittedProperties.includes(key)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// provider fields
|
||||
if (key === 'fields') {
|
||||
return value.reduce((acc, cur) => {
|
||||
acc[cur.name] = cur.value;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// regular setting values
|
||||
if (value.hasOwnProperty('value')) {
|
||||
return value.value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('customFormats'),
|
||||
(state) => state.settings.customFormatSpecifications,
|
||||
(advancedSettings, customFormat, specifications) => {
|
||||
const json = customFormat.item ? JSON.stringify(customFormat.item, replacer, 2) : '';
|
||||
return {
|
||||
advancedSettings,
|
||||
...customFormat,
|
||||
json,
|
||||
specificationsPopulated: specifications.isPopulated,
|
||||
specifications: specifications.items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchCustomFormatSpecifications
|
||||
};
|
||||
|
||||
class ExportCustomFormatModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id
|
||||
} = this.props;
|
||||
this.props.fetchCustomFormatSpecifications({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ExportCustomFormatModalContent
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExportCustomFormatModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
fetchCustomFormatSpecifications: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ExportCustomFormatModalContentConnector);
|
@ -1,61 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ImportCustomFormatModalContentConnector from './ImportCustomFormatModalContentConnector';
|
||||
|
||||
class ImportCustomFormatModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
height: 'auto'
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onContentHeightChange = (height) => {
|
||||
if (this.state.height === 'auto' || height > this.state.height) {
|
||||
this.setState({ height });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ height: `${this.state.height}px` }}
|
||||
isOpen={isOpen}
|
||||
size={sizes.LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ImportCustomFormatModalContentConnector
|
||||
{...otherProps}
|
||||
onContentHeightChange={this.onContentHeightChange}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportCustomFormatModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportCustomFormatModal;
|
@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ImportCustomFormatModalContent from './ImportCustomFormatModalContent';
|
||||
|
||||
interface ImportCustomFormatModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function ImportCustomFormatModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
}: ImportCustomFormatModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||
<ImportCustomFormatModalContent onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportCustomFormatModal;
|
@ -1,153 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } 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, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportCustomFormatModalContent.css';
|
||||
|
||||
class ImportCustomFormatModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._importTimeout = null;
|
||||
|
||||
this.state = {
|
||||
json: '',
|
||||
isSpinning: false,
|
||||
parseError: null
|
||||
};
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._importTimeout) {
|
||||
clearTimeout(this._importTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
onChange = (event) => {
|
||||
this.setState({ json: event.value });
|
||||
};
|
||||
|
||||
onImportPress = () => {
|
||||
this.setState({ isSpinning: true });
|
||||
// this is a bodge as we need to register a isSpinning: true to get the spinner button to update
|
||||
this._importTimeout = setTimeout(this.doImport, 250);
|
||||
};
|
||||
|
||||
doImport = () => {
|
||||
const parseError = this.props.onImportPress(this.state.json);
|
||||
this.setState({
|
||||
parseError,
|
||||
isSpinning: false
|
||||
});
|
||||
|
||||
if (!parseError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
error,
|
||||
specificationsPopulated,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
json,
|
||||
isSpinning,
|
||||
parseError
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
||||
<ModalHeader>
|
||||
{translate('ImportCustomFormat')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('CustomFormatsLoadError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && specificationsPopulated &&
|
||||
<Form>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('CustomFormatJson')}
|
||||
</FormLabel>
|
||||
<FormInputGroup
|
||||
key={0}
|
||||
inputClassName={styles.input}
|
||||
type={inputTypes.TEXT_AREA}
|
||||
name="customFormatJson"
|
||||
value={json}
|
||||
onChange={this.onChange}
|
||||
placeholder={'{\n "name": "Custom Format"\n}'}
|
||||
errors={parseError ? [parseError] : []}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
<SpinnerErrorButton
|
||||
onPress={this.onImportPress}
|
||||
isSpinning={isSpinning}
|
||||
error={parseError}
|
||||
>
|
||||
{translate('Import')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportCustomFormatModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
specificationsPopulated: PropTypes.bool.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportCustomFormatModalContent;
|
@ -0,0 +1,224 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import {
|
||||
clearCustomFormatSpecificationPending,
|
||||
deleteAllCustomFormatSpecification,
|
||||
fetchCustomFormatSpecificationSchema,
|
||||
saveCustomFormatSpecification,
|
||||
selectCustomFormatSpecificationSchema,
|
||||
setCustomFormatSpecificationFieldValue,
|
||||
setCustomFormatSpecificationValue,
|
||||
setCustomFormatValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
|
||||
import Field from 'typings/Field';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import { ValidationError } from 'typings/pending';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportCustomFormatModalContent.css';
|
||||
|
||||
interface ImportCustomFormatModalContentProps {
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function ImportCustomFormatModalContent({
|
||||
onModalClose,
|
||||
}: ImportCustomFormatModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, error } = useSelector(
|
||||
createProviderSettingsSelectorHook('customFormats', undefined)
|
||||
);
|
||||
|
||||
const {
|
||||
isPopulated: isSpecificationsPopulated,
|
||||
schema: specificationsSchema,
|
||||
} = useSelector(
|
||||
(state: AppState) => state.settings.customFormatSpecifications
|
||||
);
|
||||
|
||||
const importTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const [json, setJson] = useState('');
|
||||
const [isSpinning, setIsSpinning] = useState(false);
|
||||
const [parseError, setParseError] = useState<ValidationError>();
|
||||
|
||||
const handleChange = useCallback(({ value }: InputChanged<string>) => {
|
||||
setJson(value);
|
||||
}, []);
|
||||
|
||||
const clearPending = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'settings.customFormats' }));
|
||||
dispatch(clearCustomFormatSpecificationPending());
|
||||
dispatch(deleteAllCustomFormatSpecification());
|
||||
}, [dispatch]);
|
||||
|
||||
const parseFields = useCallback(
|
||||
(fields: Field[], schema: CustomFormatSpecification) => {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
const field = schema.fields.find((field) => field.name === key);
|
||||
if (!field) {
|
||||
throw new Error(
|
||||
translate('CustomFormatUnknownConditionOption', {
|
||||
key,
|
||||
implementation: schema.implementationName,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setCustomFormatSpecificationFieldValue({ name: key, value }));
|
||||
}
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const parseSpecification = useCallback(
|
||||
(spec: CustomFormatSpecification) => {
|
||||
const selectedImplementation = specificationsSchema.find((s) => {
|
||||
return s.implementation === spec.implementation;
|
||||
});
|
||||
|
||||
if (!selectedImplementation) {
|
||||
throw new Error(
|
||||
translate('CustomFormatUnknownCondition', {
|
||||
implementation: spec.implementation,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
dispatch(
|
||||
selectCustomFormatSpecificationSchema({
|
||||
implementation: spec.implementation,
|
||||
})
|
||||
);
|
||||
|
||||
for (const [key, value] of Object.entries(spec)) {
|
||||
if (key === 'fields') {
|
||||
parseFields(value, selectedImplementation);
|
||||
} else if (key !== 'id') {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setCustomFormatSpecificationValue({ name: key, value }));
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(saveCustomFormatSpecification());
|
||||
},
|
||||
[specificationsSchema, dispatch, parseFields]
|
||||
);
|
||||
|
||||
const handleImportPress = useCallback(() => {
|
||||
setIsSpinning(true);
|
||||
|
||||
importTimeout.current = setTimeout(() => {
|
||||
clearPending();
|
||||
|
||||
try {
|
||||
const cf = JSON.parse(json);
|
||||
|
||||
for (const [key, value] of Object.entries(cf)) {
|
||||
if (key === 'specifications') {
|
||||
for (const spec of value as CustomFormatSpecification[]) {
|
||||
parseSpecification(spec);
|
||||
}
|
||||
} else if (key !== 'id') {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setCustomFormatValue({ name: key, value }));
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
clearPending();
|
||||
|
||||
setParseError({
|
||||
isWarning: false,
|
||||
errorMessage: err.message,
|
||||
detailedDescription: err.stack,
|
||||
propertyName: 'customFormatJson',
|
||||
severity: 'error',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}, 250);
|
||||
}, [json, clearPending, dispatch, parseSpecification, onModalClose]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCustomFormatSpecificationSchema());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (importTimeout.current) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
clearTimeout(importTimeout.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('ImportCustomFormat')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('CustomFormatsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && !error && isSpecificationsPopulated ? (
|
||||
<Form>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('CustomFormatJson')}</FormLabel>
|
||||
<FormInputGroup
|
||||
key={0}
|
||||
inputClassName={styles.input}
|
||||
type={inputTypes.TEXT_AREA}
|
||||
name="customFormatJson"
|
||||
value={json}
|
||||
placeholder={'{\n "name": "Custom Format"\n}'}
|
||||
errors={parseError ? [parseError] : []}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
) : null}
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSpinning}
|
||||
error={parseError?.errorMessage}
|
||||
onPress={handleImportPress}
|
||||
>
|
||||
{translate('Import')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportCustomFormatModalContent;
|
@ -1,151 +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 { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { clearCustomFormatSpecificationPending, deleteAllCustomFormatSpecification, fetchCustomFormatSpecificationSchema, saveCustomFormatSpecification, selectCustomFormatSpecificationSchema, setCustomFormatSpecificationFieldValue, setCustomFormatSpecificationValue, setCustomFormatValue } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportCustomFormatModalContent from './ImportCustomFormatModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('customFormats'),
|
||||
(state) => state.settings.customFormatSpecifications,
|
||||
(advancedSettings, customFormat, specifications) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...customFormat,
|
||||
specificationsPopulated: specifications.isPopulated,
|
||||
specificationSchema: specifications.schema
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
deleteAllCustomFormatSpecification,
|
||||
clearCustomFormatSpecificationPending,
|
||||
clearPendingChanges,
|
||||
saveCustomFormatSpecification,
|
||||
selectCustomFormatSpecificationSchema,
|
||||
setCustomFormatSpecificationFieldValue,
|
||||
setCustomFormatSpecificationValue,
|
||||
setCustomFormatValue,
|
||||
fetchCustomFormatSpecificationSchema
|
||||
};
|
||||
|
||||
class ImportCustomFormatModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchCustomFormatSpecificationSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
clearPending = () => {
|
||||
this.props.clearPendingChanges({ section: 'settings.customFormats' });
|
||||
this.props.clearCustomFormatSpecificationPending();
|
||||
this.props.deleteAllCustomFormatSpecification();
|
||||
};
|
||||
|
||||
onImportPress = (payload) => {
|
||||
|
||||
this.clearPending();
|
||||
|
||||
try {
|
||||
const cf = JSON.parse(payload);
|
||||
this.parseCf(cf);
|
||||
} catch (err) {
|
||||
this.clearPending();
|
||||
return {
|
||||
message: err.message,
|
||||
detailedMessage: err.stack
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
parseCf = (cf) => {
|
||||
for (const [key, value] of Object.entries(cf)) {
|
||||
if (key === 'specifications') {
|
||||
for (const spec of value) {
|
||||
this.parseSpecification(spec);
|
||||
}
|
||||
} else if (key !== 'id') {
|
||||
this.props.setCustomFormatValue({ name: key, value });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
parseSpecification = (spec) => {
|
||||
const selectedImplementation = _.find(this.props.specificationSchema, { implementation: spec.implementation });
|
||||
|
||||
if (!selectedImplementation) {
|
||||
throw new Error(translate('CustomFormatUnknownCondition', {
|
||||
implementation: spec.implementation
|
||||
}));
|
||||
}
|
||||
|
||||
this.props.selectCustomFormatSpecificationSchema({ implementation: spec.implementation });
|
||||
|
||||
for (const [key, value] of Object.entries(spec)) {
|
||||
if (key === 'fields') {
|
||||
this.parseFields(value, selectedImplementation);
|
||||
} else if (key !== 'id') {
|
||||
this.props.setCustomFormatSpecificationValue({ name: key, value });
|
||||
}
|
||||
}
|
||||
|
||||
this.props.saveCustomFormatSpecification();
|
||||
};
|
||||
|
||||
parseFields = (fields, schema) => {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
const field = _.find(schema.fields, { name: key });
|
||||
if (!field) {
|
||||
throw new Error(translate('CustomFormatUnknownConditionOption', {
|
||||
key,
|
||||
implementation: schema.implementationName
|
||||
}));
|
||||
}
|
||||
|
||||
this.props.setCustomFormatSpecificationFieldValue({ name: key, value });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportCustomFormatModalContent
|
||||
{...this.props}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportCustomFormatModalContentConnector.propTypes = {
|
||||
specificationSchema: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired,
|
||||
deleteAllCustomFormatSpecification: PropTypes.func.isRequired,
|
||||
clearCustomFormatSpecificationPending: PropTypes.func.isRequired,
|
||||
saveCustomFormatSpecification: PropTypes.func.isRequired,
|
||||
fetchCustomFormatSpecificationSchema: PropTypes.func.isRequired,
|
||||
selectCustomFormatSpecificationSchema: PropTypes.func.isRequired,
|
||||
setCustomFormatSpecificationValue: PropTypes.func.isRequired,
|
||||
setCustomFormatSpecificationFieldValue: PropTypes.func.isRequired,
|
||||
setCustomFormatValue: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportCustomFormatModalContentConnector);
|
@ -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 AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem';
|
||||
import styles from './AddSpecificationItem.css';
|
||||
|
||||
class AddSpecificationItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSpecificationSelect = () => {
|
||||
const {
|
||||
implementation
|
||||
} = this.props;
|
||||
|
||||
this.props.onSpecificationSelect({ implementation });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onSpecificationSelect
|
||||
} = this.props;
|
||||
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.specification}
|
||||
>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
onPress={this.onSpecificationSelect}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>
|
||||
{implementationName}
|
||||
</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{
|
||||
hasPresets &&
|
||||
<span>
|
||||
<Button
|
||||
size={sizes.SMALL}
|
||||
onPress={this.onSpecificationSelect}
|
||||
>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button
|
||||
className={styles.presetsMenuButton}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{
|
||||
presets.map((preset, index) => {
|
||||
return (
|
||||
<AddSpecificationPresetMenuItem
|
||||
key={index}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
onPress={onSpecificationSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
to={infoLink}
|
||||
size={sizes.SMALL}
|
||||
>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddSpecificationItem.propTypes = {
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
infoLink: PropTypes.string.isRequired,
|
||||
presets: PropTypes.arrayOf(PropTypes.object),
|
||||
onSpecificationSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddSpecificationItem;
|
@ -0,0 +1,85 @@
|
||||
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 { selectCustomFormatSpecificationSchema } from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddSpecificationPresetMenuItem from './AddSpecificationPresetMenuItem';
|
||||
import styles from './AddSpecificationItem.css';
|
||||
|
||||
interface AddSpecificationItemProps {
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
infoLink: string;
|
||||
presets?: { name: string }[];
|
||||
onSpecificationSelect: () => void;
|
||||
}
|
||||
|
||||
function AddSpecificationItem({
|
||||
implementation,
|
||||
implementationName,
|
||||
infoLink,
|
||||
presets,
|
||||
onSpecificationSelect,
|
||||
}: AddSpecificationItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
const hasPresets = !!presets && !!presets.length;
|
||||
|
||||
const handleSpecificationSelect = useCallback(() => {
|
||||
dispatch(
|
||||
selectCustomFormatSpecificationSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
})
|
||||
);
|
||||
|
||||
onSpecificationSelect();
|
||||
}, [implementation, implementationName, dispatch, onSpecificationSelect]);
|
||||
|
||||
return (
|
||||
<div className={styles.specification}>
|
||||
<Link className={styles.underlay} onPress={handleSpecificationSelect} />
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.name}>{implementationName}</div>
|
||||
|
||||
<div className={styles.actions}>
|
||||
{hasPresets ? (
|
||||
<span>
|
||||
<Button size={sizes.SMALL} onPress={handleSpecificationSelect}>
|
||||
{translate('Custom')}
|
||||
</Button>
|
||||
|
||||
<Menu className={styles.presetsMenu}>
|
||||
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
|
||||
{translate('Presets')}
|
||||
</Button>
|
||||
|
||||
<MenuContent>
|
||||
{presets.map((preset) => (
|
||||
<AddSpecificationPresetMenuItem
|
||||
key={preset.name}
|
||||
name={preset.name}
|
||||
implementation={implementation}
|
||||
implementationName={implementationName}
|
||||
onPress={handleSpecificationSelect}
|
||||
/>
|
||||
))}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<Button to={infoLink} size={sizes.SMALL}>
|
||||
{translate('MoreInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddSpecificationItem;
|
@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddSpecificationModalContentConnector from './AddSpecificationModalContentConnector';
|
||||
|
||||
function AddSpecificationModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddSpecificationModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddSpecificationModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddSpecificationModal;
|
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddSpecificationModalContent, {
|
||||
AddSpecificationModalContentProps,
|
||||
} from './AddSpecificationModalContent';
|
||||
|
||||
interface AddSpecificationModalProps extends AddSpecificationModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function AddSpecificationModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: AddSpecificationModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<AddSpecificationModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddSpecificationModal;
|
@ -1,102 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
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 AddSpecificationItem from './AddSpecificationItem';
|
||||
import styles from './AddSpecificationModalContent.css';
|
||||
|
||||
class AddSpecificationModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema,
|
||||
onSpecificationSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Add Condition
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isSchemaFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isSchemaFetching && !!schemaError &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddConditionError')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isSchemaPopulated && !schemaError &&
|
||||
<div>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('SupportedCustomConditions')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('VisitTheWikiForMoreDetails')}
|
||||
<Link to="https://wiki.servarr.com/sonarr/settings#custom-formats-2">{translate('Wiki')}</Link>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.specifications}>
|
||||
{
|
||||
schema.map((specification) => {
|
||||
return (
|
||||
<AddSpecificationItem
|
||||
key={specification.implementation}
|
||||
{...specification}
|
||||
onSpecificationSelect={onSpecificationSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddSpecificationModalContent.propTypes = {
|
||||
isSchemaFetching: PropTypes.bool.isRequired,
|
||||
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||
schemaError: PropTypes.object,
|
||||
schema: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSpecificationSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddSpecificationModalContent;
|
@ -0,0 +1,79 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
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 { fetchCustomFormatSpecificationSchema } from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddSpecificationItem from './AddSpecificationItem';
|
||||
import styles from './AddSpecificationModalContent.css';
|
||||
|
||||
export interface AddSpecificationModalContentProps {
|
||||
onSpecificationSelect: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function AddSpecificationModalContent({
|
||||
onSpecificationSelect,
|
||||
onModalClose,
|
||||
}: AddSpecificationModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
|
||||
useSelector((state: AppState) => state.settings.customFormatSpecifications);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCustomFormatSpecificationSchema());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>Add Condition</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isSchemaFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isSchemaFetching && !!schemaError ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('AddConditionError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isSchemaPopulated && !schemaError ? (
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>{translate('SupportedCustomConditions')}</div>
|
||||
<div>
|
||||
{translate('VisitTheWikiForMoreDetails')}
|
||||
<Link to="https://wiki.servarr.com/sonarr/settings#custom-formats-2">
|
||||
{translate('Wiki')}
|
||||
</Link>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.specifications}>
|
||||
{schema.map((specification) => (
|
||||
<AddSpecificationItem
|
||||
key={specification.implementation}
|
||||
{...specification}
|
||||
onSpecificationSelect={onSpecificationSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddSpecificationModalContent;
|
@ -1,70 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCustomFormatSpecificationSchema, selectCustomFormatSpecificationSchema } from 'Store/Actions/settingsActions';
|
||||
import AddSpecificationModalContent from './AddSpecificationModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.customFormatSpecifications,
|
||||
(specifications) => {
|
||||
const {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
} = specifications;
|
||||
|
||||
return {
|
||||
isSchemaFetching,
|
||||
isSchemaPopulated,
|
||||
schemaError,
|
||||
schema
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchCustomFormatSpecificationSchema,
|
||||
selectCustomFormatSpecificationSchema
|
||||
};
|
||||
|
||||
class AddSpecificationModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchCustomFormatSpecificationSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSpecificationSelect = ({ implementation, name }) => {
|
||||
this.props.selectCustomFormatSpecificationSchema({ implementation, presetName: name });
|
||||
this.props.onModalClose({ specificationSelected: true });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddSpecificationModalContent
|
||||
{...this.props}
|
||||
onSpecificationSelect={this.onSpecificationSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddSpecificationModalContentConnector.propTypes = {
|
||||
fetchCustomFormatSpecificationSchema: PropTypes.func.isRequired,
|
||||
selectCustomFormatSpecificationSchema: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddSpecificationModalContentConnector);
|
@ -1,49 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
||||
class AddSpecificationPresetMenuItem 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddSpecificationPresetMenuItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddSpecificationPresetMenuItem;
|
@ -0,0 +1,41 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
import { selectCustomFormatSpecificationSchema } from 'Store/Actions/settingsActions';
|
||||
|
||||
interface AddSpecificationPresetMenuItemProps {
|
||||
name: string;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function AddSpecificationPresetMenuItem({
|
||||
name,
|
||||
implementation,
|
||||
implementationName,
|
||||
onPress,
|
||||
...otherProps
|
||||
}: AddSpecificationPresetMenuItemProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
dispatch(
|
||||
selectCustomFormatSpecificationSchema({
|
||||
implementation,
|
||||
implementationName,
|
||||
presetName: name,
|
||||
})
|
||||
);
|
||||
|
||||
onPress();
|
||||
}, [name, implementation, implementationName, dispatch, onPress]);
|
||||
|
||||
return (
|
||||
<MenuItem {...otherProps} onPress={handlePress}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddSpecificationPresetMenuItem;
|
@ -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 EditSpecificationModalContentConnector from './EditSpecificationModalContentConnector';
|
||||
|
||||
function EditSpecificationModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditSpecificationModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditSpecificationModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditSpecificationModal;
|
@ -0,0 +1,40 @@
|
||||
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 EditSpecificationModalContent, {
|
||||
EditSpecificationModalContentProps,
|
||||
} from './EditSpecificationModalContent';
|
||||
|
||||
const section = 'settings.customFormatSpecifications';
|
||||
|
||||
interface EditSpecificationModalProps
|
||||
extends EditSpecificationModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditSpecificationModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditSpecificationModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditSpecificationModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditSpecificationModal;
|
@ -1,50 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditSpecificationModal from './EditSpecificationModal';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
const section = 'settings.customFormatSpecifications';
|
||||
|
||||
return {
|
||||
dispatchClearPendingChanges() {
|
||||
dispatch(clearPendingChanges({ section }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class EditSpecificationModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPendingChanges();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<EditSpecificationModal
|
||||
{...otherProps}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditSpecificationModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, createMapDispatchToProps)(EditSpecificationModalConnector);
|
@ -1,162 +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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
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 translate from 'Utilities/String/translate';
|
||||
import styles from './EditSpecificationModalContent.css';
|
||||
|
||||
function EditSpecificationModalContent(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
item,
|
||||
onInputChange,
|
||||
onFieldChange,
|
||||
onCancelPress,
|
||||
onSavePress,
|
||||
onDeleteSpecificationPress,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
negate,
|
||||
required,
|
||||
fields
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onCancelPress}>
|
||||
<ModalHeader>
|
||||
{id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Name')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
fields && fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={advancedSettings}
|
||||
provider="specifications"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={onFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Negate')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="negate"
|
||||
{...negate}
|
||||
helpText={translate('NegateHelpText', { implementationName })}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Required')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="required"
|
||||
{...required}
|
||||
helpText={translate('RequiredHelpText', { implementationName })}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{
|
||||
id &&
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteSpecificationPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onCancelPress}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={false}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
EditSpecificationModalContent.propTypes = {
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onFieldChange: PropTypes.func.isRequired,
|
||||
onCancelPress: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onDeleteSpecificationPress: PropTypes.func
|
||||
};
|
||||
|
||||
export default EditSpecificationModalContent;
|
@ -0,0 +1,188 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { CustomFormatSpecificationAppState } from 'App/State/SettingsAppState';
|
||||
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
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 useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
saveCustomFormatSpecification,
|
||||
setCustomFormatSpecificationFieldValue,
|
||||
setCustomFormatSpecificationValue,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditSpecificationModalContent.css';
|
||||
|
||||
export interface EditSpecificationModalContentProps {
|
||||
id?: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteSpecificationPress?: () => void;
|
||||
}
|
||||
|
||||
function EditSpecificationModalContent({
|
||||
id,
|
||||
onModalClose,
|
||||
onDeleteSpecificationPress,
|
||||
}: EditSpecificationModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const showAdvancedSettings = useShowAdvancedSettings();
|
||||
|
||||
const { item, validationErrors, validationWarnings } = useSelector(
|
||||
createProviderSettingsSelectorHook<
|
||||
CustomFormatSpecification,
|
||||
CustomFormatSpecificationAppState
|
||||
>('customFormatSpecifications', id)
|
||||
);
|
||||
|
||||
const { implementationName, name, negate, required, fields } = item;
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setCustomFormatSpecificationValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleFieldChange = useCallback(
|
||||
(change: InputChanged) => {
|
||||
// @ts-expect-error - actions are not typed
|
||||
dispatch(setCustomFormatSpecificationFieldValue(change));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
dispatch(saveCustomFormatSpecification({ id }));
|
||||
|
||||
onModalClose();
|
||||
}, [id, dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id
|
||||
? translate('EditConditionImplementation', { implementationName })
|
||||
: translate('AddConditionImplementation', { implementationName })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
validationErrors={validationErrors}
|
||||
validationWarnings={validationWarnings}
|
||||
>
|
||||
{fields?.some(
|
||||
(x) =>
|
||||
x.label ===
|
||||
translate('CustomFormatsSpecificationRegularExpression')
|
||||
) ? (
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('ConditionUsingRegularExpressions')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('RegularExpressionsTutorialLink', {
|
||||
url: 'https://www.regular-expressions.info/tutorial.html',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('RegularExpressionsCanBeTested', {
|
||||
url: 'http://regexstorm.net/tester',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Name')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="name"
|
||||
{...name}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{fields
|
||||
? fields.map((field) => {
|
||||
return (
|
||||
<ProviderFieldFormGroup
|
||||
key={field.name}
|
||||
advancedSettings={showAdvancedSettings}
|
||||
provider="specifications"
|
||||
providerData={item}
|
||||
{...field}
|
||||
onChange={handleFieldChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Negate')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="negate"
|
||||
{...negate}
|
||||
helpText={translate('NegateHelpText', { implementationName })}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Required')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="required"
|
||||
{...required}
|
||||
helpText={translate('RequiredHelpText', { implementationName })}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
{id && (
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteSpecificationPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton isSpinning={false} onPress={handleSavePress}>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditSpecificationModalContent;
|
@ -1,78 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearCustomFormatSpecificationPending, saveCustomFormatSpecification, setCustomFormatSpecificationFieldValue, setCustomFormatSpecificationValue } from 'Store/Actions/settingsActions';
|
||||
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||
import EditSpecificationModalContent from './EditSpecificationModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.advancedSettings,
|
||||
createProviderSettingsSelector('customFormatSpecifications'),
|
||||
(advancedSettings, specification) => {
|
||||
return {
|
||||
advancedSettings,
|
||||
...specification
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setCustomFormatSpecificationValue,
|
||||
setCustomFormatSpecificationFieldValue,
|
||||
saveCustomFormatSpecification,
|
||||
clearCustomFormatSpecificationPending
|
||||
};
|
||||
|
||||
class EditSpecificationModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setCustomFormatSpecificationValue({ name, value });
|
||||
};
|
||||
|
||||
onFieldChange = ({ name, value }) => {
|
||||
this.props.setCustomFormatSpecificationFieldValue({ name, value });
|
||||
};
|
||||
|
||||
onCancelPress = () => {
|
||||
this.props.clearCustomFormatSpecificationPending();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveCustomFormatSpecification({ id: this.props.id });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditSpecificationModalContent
|
||||
{...this.props}
|
||||
onCancelPress={this.onCancelPress}
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onFieldChange={this.onFieldChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditSpecificationModalContentConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
item: PropTypes.object.isRequired,
|
||||
setCustomFormatSpecificationValue: PropTypes.func.isRequired,
|
||||
setCustomFormatSpecificationFieldValue: PropTypes.func.isRequired,
|
||||
clearCustomFormatSpecificationPending: PropTypes.func.isRequired,
|
||||
saveCustomFormatSpecification: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditSpecificationModalContentConnector);
|
@ -1,140 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditSpecificationModalConnector from './EditSpecificationModal';
|
||||
import styles from './Specification.css';
|
||||
|
||||
class Specification extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditSpecificationModalOpen: false,
|
||||
isDeleteSpecificationModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditSpecificationPress = () => {
|
||||
this.setState({ isEditSpecificationModalOpen: true });
|
||||
};
|
||||
|
||||
onEditSpecificationModalClose = () => {
|
||||
this.setState({ isEditSpecificationModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteSpecificationPress = () => {
|
||||
this.setState({
|
||||
isEditSpecificationModalOpen: false,
|
||||
isDeleteSpecificationModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteSpecificationModalClose = () => {
|
||||
this.setState({ isDeleteSpecificationModalOpen: false });
|
||||
};
|
||||
|
||||
onCloneSpecificationPress = () => {
|
||||
this.props.onCloneSpecificationPress(this.props.id);
|
||||
};
|
||||
|
||||
onConfirmDeleteSpecification = () => {
|
||||
this.props.onConfirmDeleteSpecification(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
required,
|
||||
negate
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.customFormat}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditSpecificationPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneCondition')}
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneSpecificationPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.labels}>
|
||||
<Label kind={kinds.DEFAULT}>
|
||||
{implementationName}
|
||||
</Label>
|
||||
|
||||
{
|
||||
negate &&
|
||||
<Label kind={kinds.DANGER}>
|
||||
{translate('Negated')}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
required &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Required')}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<EditSpecificationModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditSpecificationModalOpen}
|
||||
onModalClose={this.onEditSpecificationModalClose}
|
||||
onDeleteSpecificationPress={this.onDeleteSpecificationPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteSpecificationModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCondition')}
|
||||
message={translate('DeleteConditionMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={this.onConfirmDeleteSpecification}
|
||||
onCancel={this.onDeleteSpecificationModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Specification.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
implementationName: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
negate: PropTypes.bool.isRequired,
|
||||
required: PropTypes.bool.isRequired,
|
||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteSpecification: PropTypes.func.isRequired,
|
||||
onCloneSpecificationPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Specification;
|
@ -0,0 +1,113 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
cloneCustomFormatSpecification,
|
||||
deleteCustomFormatSpecification,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EditSpecificationModal from './EditSpecificationModal';
|
||||
import styles from './Specification.css';
|
||||
|
||||
interface SpecificationProps {
|
||||
id: number;
|
||||
implementationName: string;
|
||||
name: string;
|
||||
negate: boolean;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
function Specification({
|
||||
id,
|
||||
implementationName,
|
||||
name,
|
||||
required,
|
||||
negate,
|
||||
}: SpecificationProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isEditSpecificationModalOpen, setIsEditSpecificationModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isDeleteSpecificationModalOpen, setIsDeleteSpecificationModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const handleEditSpecificationPress = useCallback(() => {
|
||||
setIsEditSpecificationModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEditSpecificationModalClose = useCallback(() => {
|
||||
setIsEditSpecificationModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleDeleteSpecificationPress = useCallback(() => {
|
||||
setIsEditSpecificationModalOpen(false);
|
||||
setIsDeleteSpecificationModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleDeleteSpecificationModalClose = useCallback(() => {
|
||||
setIsDeleteSpecificationModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleCloneSpecificationPress = useCallback(() => {
|
||||
dispatch(cloneCustomFormatSpecification({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
const handleConfirmDeleteSpecification = useCallback(() => {
|
||||
dispatch(deleteCustomFormatSpecification({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.customFormat}
|
||||
overlayContent={true}
|
||||
onPress={handleEditSpecificationPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title={translate('CloneCondition')}
|
||||
name={icons.CLONE}
|
||||
onPress={handleCloneSpecificationPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.labels}>
|
||||
<Label kind={kinds.DEFAULT}>{implementationName}</Label>
|
||||
|
||||
{negate ? (
|
||||
<Label kind={kinds.DANGER}>{translate('Negated')}</Label>
|
||||
) : null}
|
||||
|
||||
{required ? (
|
||||
<Label kind={kinds.SUCCESS}>{translate('Required')}</Label>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<EditSpecificationModal
|
||||
id={id}
|
||||
isOpen={isEditSpecificationModalOpen}
|
||||
onModalClose={handleEditSpecificationModalClose}
|
||||
onDeleteSpecificationPress={handleDeleteSpecificationPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isDeleteSpecificationModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DeleteCondition')}
|
||||
message={translate('DeleteConditionMessageText', { name })}
|
||||
confirmLabel={translate('Delete')}
|
||||
onConfirm={handleConfirmDeleteSpecification}
|
||||
onCancel={handleDeleteSpecificationModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default Specification;
|
@ -0,0 +1,8 @@
|
||||
import Provider from './Provider';
|
||||
|
||||
interface CustomFormatSpecification extends Provider {
|
||||
negate: boolean;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export default CustomFormatSpecification;
|
Loading…
Reference in new issue