diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index e507fcf57..d06892896 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -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, + AppSectionDeleteState, + AppSectionSaveState, + AppSectionSchemaState> {} + export interface ImportListOptionsSettingsAppState extends AppSectionItemState, AppSectionSaveState {} @@ -156,6 +163,7 @@ interface SettingsAppState { autoTaggings: AutoTaggingAppState; autoTaggingSpecifications: AutoTaggingSpecificationAppState; customFormats: CustomFormatAppState; + customFormatSpecifications: CustomFormatSpecificationAppState; delayProfiles: DelayProfileAppState; downloadClients: DownloadClientAppState; downloadClientOptions: DownloadClientOptionsAppState; diff --git a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx index 3010c3628..faa91c5f4 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormatSettingsPage.tsx @@ -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() { /> - {/* TODO: Upgrade react-dnd to get typings, we're 2 major versions behind */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} - {/* @ts-ignore */} - + diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js deleted file mode 100644 index 0f72228bb..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.js +++ /dev/null @@ -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 ( - -
-
- {name} -
- -
- - - -
-
- -
- { - 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 ( - - ); - }) - } -
- - - - - - -
- ); - } -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx new file mode 100644 index 000000000..335a7a555 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormat.tsx @@ -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 ( + +
+
{name}
+ +
+ + + +
+
+ +
+ {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 ( + + ); + })} +
+ + + + + + +
+ ); +} + +export default CustomFormat; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js deleted file mode 100644 index 8036a4a25..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.js +++ /dev/null @@ -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 ( -
- -
- { - items.map((item) => { - return ( - - ); - }) - } - - -
- -
-
-
- - - -
-
- ); - } -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.tsx new file mode 100644 index 000000000..22a28dcbb --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormats.tsx @@ -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( + 'settings.customFormats', + sortByProp('name') + ) + ); + + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [clonedId, setClonedId] = useState(); + + 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 ( +
+ +
+ {items.map((item) => { + return ( + + ); + })} + + +
+ +
+
+
+ + +
+
+ ); +} + +export default CustomFormats; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js deleted file mode 100644 index 0417d9b21..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/CustomFormatsConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -CustomFormatsConnector.propTypes = { - dispatchFetchCustomFormats: PropTypes.func.isRequired, - dispatchDeleteCustomFormat: PropTypes.func.isRequired, - dispatchCloneCustomFormat: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(CustomFormatsConnector); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.js deleted file mode 100644 index cf15d32c6..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.js +++ /dev/null @@ -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 ( - - - - ); - } -} - -EditCustomFormatModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditCustomFormatModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.tsx new file mode 100644 index 000000000..107bf488a --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModal.tsx @@ -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 ( + + + + ); +} + +export default EditCustomFormatModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js deleted file mode 100644 index 3e79425cd..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -EditCustomFormatModalConnector.propTypes = { - ...EditCustomFormatModalContentConnector.propTypes, - onModalClose: PropTypes.func.isRequired, - clearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(mapStateToProps, mapDispatchToProps)(EditCustomFormatModalConnector); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js deleted file mode 100644 index 44fa9c5ca..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.js +++ /dev/null @@ -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 ( - - - - {id ? translate('EditCustomFormat') : translate('AddCustomFormat')} - - - -
- { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('AddCustomFormatError')} - - } - - { - !isFetching && !error && specificationsPopulated && -
-
- - - {translate('Name')} - - - - - - - {translate('IncludeCustomFormatWhenRenaming')} - - - -
- -
- -
- {translate('CustomFormatsSettingsTriggerInfo')} -
-
-
- { - specifications.map((tag) => { - return ( - - ); - }) - } - - -
- -
-
-
-
- - - - - - - -
- } -
-
- -
- { - id && - - } - - -
- - - - - {translate('Save')} - -
-
- ); - } -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.tsx new file mode 100644 index 000000000..9316e20e3 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContent.tsx @@ -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 ( + + + {id ? translate('EditCustomFormat') : translate('AddCustomFormat')} + + + +
+ {isFetching ? : null} + + {!isFetching && error ? ( + + {translate('AddCustomFormatError')} + + ) : null} + + {!isFetching && !error && isSpecificationsPopulated ? ( +
+
+ + {translate('Name')} + + + + + + + {translate('IncludeCustomFormatWhenRenaming')} + + + + +
+ +
+ +
{translate('CustomFormatsSettingsTriggerInfo')}
+
+ +
+ {specifications.map((tag) => { + return ; + })} + + +
+ +
+
+
+
+ + + + + + +
+ ) : null} +
+
+ + +
+ {id ? ( + + ) : null} + + +
+ + + + + {translate('Save')} + +
+
+ ); +} + +export default EditCustomFormatModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js deleted file mode 100644 index 5d22126aa..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/EditCustomFormatModalContentConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.js deleted file mode 100644 index 17105f67f..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.js +++ /dev/null @@ -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 ( - - - - ); - } -} - -ExportCustomFormatModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default ExportCustomFormatModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.tsx new file mode 100644 index 000000000..48b305fac --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModal.tsx @@ -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 ( + + + + ); +} + +export default ExportCustomFormatModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js deleted file mode 100644 index 4527cf662..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.js +++ /dev/null @@ -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 ( - - - - {translate('ExportCustomFormat')} - - - -
- { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('CustomFormatsLoadError')} - - } - - { - !isFetching && !error && specificationsPopulated && -
-
-                    {json}
-                  
-
- } -
-
- - - - -
- ); - } -} - -ExportCustomFormatModalContent.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - json: PropTypes.string.isRequired, - specificationsPopulated: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default ExportCustomFormatModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.tsx new file mode 100644 index 000000000..2e5b24794 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContent.tsx @@ -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>((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 ( + + {translate('ExportCustomFormat')} + + +
+ {isFetching ? : null} + + {!isFetching && error ? ( + + {translate('CustomFormatsLoadError')} + + ) : null} + + {!isFetching && !error ? ( +
+
{json}
+
+ ) : null} +
+
+ + + + + +
+ ); +} + +export default ExportCustomFormatModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContentConnector.js deleted file mode 100644 index 521d83d41..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ExportCustomFormatModalContentConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -ExportCustomFormatModalContentConnector.propTypes = { - id: PropTypes.number, - fetchCustomFormatSpecifications: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(ExportCustomFormatModalContentConnector); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.js deleted file mode 100644 index 4cd8cb1d4..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.js +++ /dev/null @@ -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 ( - - - - ); - } -} - -ImportCustomFormatModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default ImportCustomFormatModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.tsx new file mode 100644 index 000000000..83d7d4e99 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModal.tsx @@ -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 ( + + + + ); +} + +export default ImportCustomFormatModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js deleted file mode 100644 index b9c0590d1..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.js +++ /dev/null @@ -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 ( - - - - {translate('ImportCustomFormat')} - - - -
- { - isFetching && - - } - - { - !isFetching && !!error && - - {translate('CustomFormatsLoadError')} - - } - - { - !isFetching && !error && specificationsPopulated && -
- - - {translate('CustomFormatJson')} - - - -
- } -
-
- - - - {translate('Import')} - - -
- ); - } -} - -ImportCustomFormatModalContent.propTypes = { - isFetching: PropTypes.bool.isRequired, - error: PropTypes.object, - specificationsPopulated: PropTypes.bool.isRequired, - onImportPress: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default ImportCustomFormatModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.tsx new file mode 100644 index 000000000..eda7d107b --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContent.tsx @@ -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>(); + const [json, setJson] = useState(''); + const [isSpinning, setIsSpinning] = useState(false); + const [parseError, setParseError] = useState(); + + const handleChange = useCallback(({ value }: InputChanged) => { + 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 ( + + {translate('ImportCustomFormat')} + + +
+ {isFetching ? : null} + + {!isFetching && error ? ( + + {translate('CustomFormatsLoadError')} + + ) : null} + + {!isFetching && !error && isSpecificationsPopulated ? ( +
+ + {translate('CustomFormatJson')} + + +
+ ) : null} +
+
+ + + + + {translate('Import')} + + +
+ ); +} + +export default ImportCustomFormatModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContentConnector.js deleted file mode 100644 index 53124e9e9..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/ImportCustomFormatModalContentConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx index 57bb7fda0..db1f0cb36 100644 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Manage/ManageCustomFormatsModalRow.tsx @@ -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) { /> - { - const { - implementation - } = this.props; - - this.props.onSpecificationSelect({ implementation }); - }; - - // - // Render - - render() { - const { - implementation, - implementationName, - infoLink, - presets, - onSpecificationSelect - } = this.props; - - const hasPresets = !!presets && !!presets.length; - - return ( -
- - -
-
- {implementationName} -
- -
- { - hasPresets && - - - - - - - - { - presets.map((preset, index) => { - return ( - - ); - }) - } - - - - } - - -
-
-
- ); - } -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.tsx new file mode 100644 index 000000000..de5867478 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationItem.tsx @@ -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 ( +
+ + +
+
{implementationName}
+ +
+ {hasPresets ? ( + + + + + + + + {presets.map((preset) => ( + + ))} + + + + ) : null} + + +
+
+
+ ); +} + +export default AddSpecificationItem; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js deleted file mode 100644 index 19d8a4335..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.js +++ /dev/null @@ -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 ( - - - - ); -} - -AddSpecificationModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default AddSpecificationModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.tsx new file mode 100644 index 000000000..d5a2a525a --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModal.tsx @@ -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 ( + + + + ); +} + +export default AddSpecificationModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js deleted file mode 100644 index f06764719..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.js +++ /dev/null @@ -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 ( - - - Add Condition - - - - { - isSchemaFetching && - - } - - { - !isSchemaFetching && !!schemaError && - - {translate('AddConditionError')} - - } - - { - isSchemaPopulated && !schemaError && -
- - -
- {translate('SupportedCustomConditions')} -
-
- {translate('VisitTheWikiForMoreDetails')} - {translate('Wiki')} -
-
- -
- { - schema.map((specification) => { - return ( - - ); - }) - } -
- -
- } -
- - - -
- ); - } -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.tsx new file mode 100644 index 000000000..15f6ffc4f --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContent.tsx @@ -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 ( + + Add Condition + + + {isSchemaFetching ? : null} + + {!isSchemaFetching && !!schemaError ? ( + {translate('AddConditionError')} + ) : null} + + {isSchemaPopulated && !schemaError ? ( +
+ +
{translate('SupportedCustomConditions')}
+
+ {translate('VisitTheWikiForMoreDetails')} + + {translate('Wiki')} + +
+
+ +
+ {schema.map((specification) => ( + + ))} +
+
+ ) : null} +
+ + + + +
+ ); +} + +export default AddSpecificationModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js deleted file mode 100644 index 72b524b33..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationModalContentConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -AddSpecificationModalContentConnector.propTypes = { - fetchCustomFormatSpecificationSchema: PropTypes.func.isRequired, - selectCustomFormatSpecificationSchema: PropTypes.func.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default connect(createMapStateToProps, mapDispatchToProps)(AddSpecificationModalContentConnector); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js deleted file mode 100644 index 6189de040..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.js +++ /dev/null @@ -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 ( - - {name} - - ); - } -} - -AddSpecificationPresetMenuItem.propTypes = { - name: PropTypes.string.isRequired, - implementation: PropTypes.string.isRequired, - onPress: PropTypes.func.isRequired -}; - -export default AddSpecificationPresetMenuItem; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.tsx new file mode 100644 index 000000000..dc2fc0b15 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/AddSpecificationPresetMenuItem.tsx @@ -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 ( + + {name} + + ); +} + +export default AddSpecificationPresetMenuItem; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js deleted file mode 100644 index b5c287c35..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.js +++ /dev/null @@ -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 ( - - - - ); -} - -EditSpecificationModal.propTypes = { - isOpen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired -}; - -export default EditSpecificationModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.tsx new file mode 100644 index 000000000..09e6c09e2 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModal.tsx @@ -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 ( + + + + ); +} + +export default EditSpecificationModal; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js deleted file mode 100644 index a2865444b..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -EditSpecificationModalConnector.propTypes = { - onModalClose: PropTypes.func.isRequired, - dispatchClearPendingChanges: PropTypes.func.isRequired -}; - -export default connect(null, createMapDispatchToProps)(EditSpecificationModalConnector); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js deleted file mode 100644 index 71f3cffa9..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.js +++ /dev/null @@ -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 ( - - - {id ? translate('EditConditionImplementation', { implementationName }) : translate('AddConditionImplementation', { implementationName })} - - - -
- { - fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) && - -
- -
-
- -
-
- -
-
- } - - - - {translate('Name')} - - - - - - { - fields && fields.map((field) => { - return ( - - ); - }) - } - - - - {translate('Negate')} - - - - - - - - {translate('Required')} - - - - - -
- - { - id && - - } - - - - - {translate('Save')} - - -
- ); -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.tsx new file mode 100644 index 000000000..1a5d6cba1 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContent.tsx @@ -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 ( + + + {id + ? translate('EditConditionImplementation', { implementationName }) + : translate('AddConditionImplementation', { implementationName })} + + + +
+ {fields?.some( + (x) => + x.label === + translate('CustomFormatsSpecificationRegularExpression') + ) ? ( + +
+ +
+
+ +
+
+ +
+
+ ) : null} + + + {translate('Name')} + + + + + {fields + ? fields.map((field) => { + return ( + + ); + }) + : null} + + + {translate('Negate')} + + + + + + {translate('Required')} + + + + +
+ + {id && ( + + )} + + + + + {translate('Save')} + + +
+ ); +} + +export default EditSpecificationModalContent; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js deleted file mode 100644 index b56993dfb..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/EditSpecificationModalContentConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js deleted file mode 100644 index 0b59f09d5..000000000 --- a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.js +++ /dev/null @@ -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 ( - -
-
- {name} -
- - -
- -
- - - { - negate && - - } - - { - required && - - } -
- - - - -
- ); - } -} - -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; diff --git a/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx new file mode 100644 index 000000000..afb91aa97 --- /dev/null +++ b/frontend/src/Settings/CustomFormats/CustomFormats/Specifications/Specification.tsx @@ -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 ( + +
+
{name}
+ + +
+ +
+ + + {negate ? ( + + ) : null} + + {required ? ( + + ) : null} +
+ + + + +
+ ); +} + +export default Specification; diff --git a/frontend/src/typings/CustomFormat.ts b/frontend/src/typings/CustomFormat.ts index b3cc2a845..aceb37482 100644 --- a/frontend/src/typings/CustomFormat.ts +++ b/frontend/src/typings/CustomFormat.ts @@ -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; diff --git a/frontend/src/typings/CustomFormatSpecification.ts b/frontend/src/typings/CustomFormatSpecification.ts new file mode 100644 index 000000000..708b02e49 --- /dev/null +++ b/frontend/src/typings/CustomFormatSpecification.ts @@ -0,0 +1,8 @@ +import Provider from './Provider'; + +interface CustomFormatSpecification extends Provider { + negate: boolean; + required: boolean; +} + +export default CustomFormatSpecification;