Convert Custom Format settings to TypeScript

pull/7640/head
Mark McDowall 4 months ago
parent 1765feac03
commit 4c6d6b726e
No known key found for this signature in database

@ -10,6 +10,7 @@ import AppSectionState, {
import Language from 'Language/Language';
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
import CustomFormat from 'typings/CustomFormat';
import CustomFormatSpecification from 'typings/CustomFormatSpecification';
import DelayProfile from 'typings/DelayProfile';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
@ -128,6 +129,12 @@ export interface CustomFormatAppState
AppSectionDeleteState,
AppSectionSaveState {}
export interface CustomFormatSpecificationAppState
extends AppSectionState<CustomFormatSpecification>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<Presets<CustomFormatSpecification>> {}
export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
@ -156,6 +163,7 @@ interface SettingsAppState {
autoTaggings: AutoTaggingAppState;
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
customFormats: CustomFormatAppState;
customFormatSpecifications: CustomFormatSpecificationAppState;
delayProfiles: DelayProfileAppState;
downloadClients: DownloadClientAppState;
downloadClientOptions: DownloadClientOptionsAppState;

@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import ParseToolbarButton from 'Parse/ParseToolbarButton';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
import CustomFormats from './CustomFormats/CustomFormats';
import ManageCustomFormatsToolbarButton from './CustomFormats/Manage/ManageCustomFormatsToolbarButton';
function CustomFormatSettingsPage() {
@ -27,13 +27,8 @@ function CustomFormatSettingsPage() {
/>
<PageContentBody>
{/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */}
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<DndProvider backend={HTML5Backend}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<CustomFormatsConnector />
<CustomFormats />
</DndProvider>
</PageContentBody>
</PageContent>

@ -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);

@ -12,7 +12,7 @@ import { icons } from 'Helpers/Props';
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector';
import EditCustomFormatModal from '../EditCustomFormatModal';
import styles from './ManageCustomFormatsModalRow.css';
interface ManageCustomFormatsModalRowProps {
@ -102,7 +102,7 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
/>
</TableRowCell>
<EditCustomFormatModalConnector
<EditCustomFormatModal
id={id}
isOpen={isEditCustomFormatModalOpen}
onModalClose={handleEditCustomFormatModalClose}

@ -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;

@ -1,4 +1,5 @@
import ModelBase from 'App/ModelBase';
import CustomFormatSpecification from './CustomFormatSpecification';
export interface QualityProfileFormatItem {
format: number;
@ -9,6 +10,7 @@ export interface QualityProfileFormatItem {
interface CustomFormat extends ModelBase {
name: string;
includeCustomFormatWhenRenaming: boolean;
specifications: CustomFormatSpecification[];
}
export default CustomFormat;

@ -0,0 +1,8 @@
import Provider from './Provider';
interface CustomFormatSpecification extends Provider {
negate: boolean;
required: boolean;
}
export default CustomFormatSpecification;
Loading…
Cancel
Save