Convert Notifications to TypeScript

This commit is contained in:
Mark McDowall 2025-01-04 11:53:10 -08:00
parent 92db4769be
commit 1765feac03
No known key found for this signature in database
24 changed files with 835 additions and 1228 deletions

View file

@ -99,7 +99,9 @@ export interface IndexerAppState
export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<Presets<Notification>> {}
export interface QualityDefinitionsAppState
extends AppSectionState<QualityDefinition>,

View file

@ -3,17 +3,15 @@ import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import NotificationsConnector from './Notifications/NotificationsConnector';
import Notifications from './Notifications/Notifications';
function NotificationSettings() {
return (
<PageContent title={translate('ConnectSettings')}>
<SettingsToolbar
showSave={false}
/>
<SettingsToolbar showSave={false} />
<PageContentBody>
<NotificationsConnector />
<Notifications />
</PageContentBody>
</PageContent>
);

View file

@ -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 AddNotificationPresetMenuItem from './AddNotificationPresetMenuItem';
import styles from './AddNotificationItem.css';
class AddNotificationItem extends Component {
//
// Listeners
onNotificationSelect = () => {
const {
implementation
} = this.props;
this.props.onNotificationSelect({ implementation });
};
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onNotificationSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.notification}
>
<Link
className={styles.underlay}
onPress={this.onNotificationSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onNotificationSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddNotificationPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
onPress={onNotificationSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddNotificationItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onNotificationSelect: PropTypes.func.isRequired
};
export default AddNotificationItem;

View file

@ -0,0 +1,88 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import { selectNotificationSchema } from 'Store/Actions/settingsActions';
import Notification from 'typings/Notification';
import translate from 'Utilities/String/translate';
import AddNotificationPresetMenuItem from './AddNotificationPresetMenuItem';
import styles from './AddNotificationItem.css';
interface AddNotificationItemProps {
implementation: string;
implementationName: string;
infoLink: string;
presets?: Notification[];
onNotificationSelect: () => void;
}
function AddNotificationItem({
implementation,
implementationName,
infoLink,
presets,
onNotificationSelect,
}: AddNotificationItemProps) {
const dispatch = useDispatch();
const hasPresets = !!presets && !!presets.length;
const handleNotificationSelect = useCallback(() => {
dispatch(
selectNotificationSchema({
implementation,
implementationName,
})
);
onNotificationSelect();
}, [implementation, implementationName, dispatch, onNotificationSelect]);
return (
<div className={styles.notification}>
<Link className={styles.underlay} onPress={handleNotificationSelect} />
<div className={styles.overlay}>
<div className={styles.name}>{implementationName}</div>
<div className={styles.actions}>
{hasPresets ? (
<span>
<Button size={sizes.SMALL} onPress={handleNotificationSelect}>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
{translate('Presets')}
</Button>
<MenuContent>
{presets.map((preset) => {
return (
<AddNotificationPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onNotificationSelect}
/>
);
})}
</MenuContent>
</Menu>
</span>
) : null}
<Button to={infoLink} size={sizes.SMALL}>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
export default AddNotificationItem;

View file

@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddNotificationModalContentConnector from './AddNotificationModalContentConnector';
function AddNotificationModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddNotificationModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddNotificationModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddNotificationModal;

View file

@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddNotificationModalContent, {
AddNotificationModalContentProps,
} from './AddNotificationModalContent';
interface AddNotificationModalProps extends AddNotificationModalContentProps {
isOpen: boolean;
}
function AddNotificationModal({
isOpen,
onModalClose,
...otherProps
}: AddNotificationModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<AddNotificationModalContent
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default AddNotificationModal;

View file

@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddNotificationItem from './AddNotificationItem';
import styles from './AddNotificationModalContent.css';
class AddNotificationModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema,
onNotificationSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddConnection')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<Alert kind={kinds.DANGER}>
{translate('AddNotificationError')}
</Alert>
}
{
isSchemaPopulated && !schemaError &&
<div>
<div className={styles.notifications}>
{
schema.map((notification) => {
return (
<AddNotificationItem
key={notification.implementation}
implementation={notification.implementation}
{...notification}
onNotificationSelect={onNotificationSelect}
/>
);
})
}
</div>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddNotificationModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
schema: PropTypes.arrayOf(PropTypes.object).isRequired,
onNotificationSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddNotificationModalContent;

View file

@ -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 FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import { fetchNotificationSchema } from 'Store/Actions/settingsActions';
import translate from 'Utilities/String/translate';
import AddNotificationItem from './AddNotificationItem';
import styles from './AddNotificationModalContent.css';
export interface AddNotificationModalContentProps {
onNotificationSelect: () => void;
onModalClose: () => void;
}
function AddNotificationModalContent({
onNotificationSelect,
onModalClose,
}: AddNotificationModalContentProps) {
const dispatch = useDispatch();
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
useSelector((state: AppState) => state.settings.notifications);
useEffect(() => {
dispatch(fetchNotificationSchema());
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddNotification')}</ModalHeader>
<ModalBody>
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isSchemaFetching && !!schemaError ? (
<Alert kind={kinds.DANGER}>{translate('AddNotificationError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedNotifications')}</div>
<div>{translate('SupportedNotificationsMoreInfo')}</div>
</Alert>
<FieldSet legend={translate('Email')}>
<div className={styles.notifications}>
{schema.map((notification) => {
return (
<AddNotificationItem
key={notification.implementation}
{...notification}
implementation={notification.implementation}
onNotificationSelect={onNotificationSelect}
/>
);
})}
</div>
</FieldSet>
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default AddNotificationModalContent;

View file

@ -1,70 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchNotificationSchema, selectNotificationSchema } from 'Store/Actions/settingsActions';
import AddNotificationModalContent from './AddNotificationModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.notifications,
(notifications) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = notifications;
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
};
}
);
}
const mapDispatchToProps = {
fetchNotificationSchema,
selectNotificationSchema
};
class AddNotificationModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchNotificationSchema();
}
//
// Listeners
onNotificationSelect = ({ implementation, name }) => {
this.props.selectNotificationSchema({ implementation, presetName: name });
this.props.onModalClose({ notificationSelected: true });
};
//
// Render
render() {
return (
<AddNotificationModalContent
{...this.props}
onNotificationSelect={this.onNotificationSelect}
/>
);
}
}
AddNotificationModalContentConnector.propTypes = {
fetchNotificationSchema: PropTypes.func.isRequired,
selectNotificationSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNotificationModalContentConnector);

View file

@ -1,49 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddNotificationPresetMenuItem 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>
);
}
}
AddNotificationPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddNotificationPresetMenuItem;

View file

@ -0,0 +1,41 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MenuItem from 'Components/Menu/MenuItem';
import { selectNotificationSchema } from 'Store/Actions/settingsActions';
interface AddNotificationPresetMenuItemProps {
name: string;
implementation: string;
implementationName: string;
onPress: () => void;
}
function AddNotificationPresetMenuItem({
name,
implementation,
implementationName,
onPress,
...otherProps
}: AddNotificationPresetMenuItemProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(
selectNotificationSchema({
implementation,
implementationName,
presetName: name,
})
);
onPress();
}, [name, implementation, implementationName, dispatch, onPress]);
return (
<MenuItem {...otherProps} onPress={handlePress}>
{name}
</MenuItem>
);
}
export default AddNotificationPresetMenuItem;

View file

@ -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 EditNotificationModalContentConnector from './EditNotificationModalContentConnector';
function EditNotificationModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditNotificationModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditNotificationModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditNotificationModal;

View file

@ -0,0 +1,44 @@
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 {
cancelSaveNotification,
cancelTestNotification,
} from 'Store/Actions/settingsActions';
import EditNotificationModalContent, {
EditNotificationModalContentProps,
} from './EditNotificationModalContent';
const section = 'settings.notifications';
interface EditNotificationModalProps extends EditNotificationModalContentProps {
isOpen: boolean;
}
function EditNotificationModal({
isOpen,
onModalClose,
...otherProps
}: EditNotificationModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section }));
dispatch(cancelTestNotification({ section }));
dispatch(cancelSaveNotification({ section }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditNotificationModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditNotificationModal;

View file

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveNotification, cancelTestNotification } from 'Store/Actions/settingsActions';
import EditNotificationModal from './EditNotificationModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.notifications';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestNotification() {
dispatch(cancelTestNotification({ section }));
},
dispatchCancelSaveNotification() {
dispatch(cancelSaveNotification({ section }));
}
};
}
class EditNotificationModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestNotification();
this.props.dispatchCancelSaveNotification();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestNotification,
dispatchCancelSaveNotification,
...otherProps
} = this.props;
return (
<EditNotificationModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditNotificationModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestNotification: PropTypes.func.isRequired,
dispatchCancelSaveNotification: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditNotificationModalConnector);

View file

@ -1,186 +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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import NotificationEventItems from './NotificationEventItems';
import styles from './EditNotificationModalContent.css';
function EditNotificationModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteNotificationPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
tags,
fields,
message
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditConnectionImplementation', { implementationName }) : translate('AddConnectionImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('AddNotificationError')}
</Alert>
}
{
!isFetching && !error &&
<Form {...otherProps}>
{
!!message &&
<Alert
className={styles.message}
kind={message.value.type}
>
{message.value.message}
</Alert>
}
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<NotificationEventItems
item={item}
onInputChange={onInputChange}
/>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('NotificationsTagsSeriesHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="notification"
providerData={item}
section="settings.notifications"
{...field}
onChange={onFieldChange}
/>
);
})
}
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteNotificationPress}
>
{translate('Delete')}
</Button>
}
<AdvancedSettingsButton
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditNotificationModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteNotificationPress: PropTypes.func
};
export default EditNotificationModalContent;

View file

@ -0,0 +1,203 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { NotificationAppState } 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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import {
saveNotification,
setNotificationFieldValue,
setNotificationValue,
testNotification,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import { InputChanged } from 'typings/inputs';
import Notification from 'typings/Notification';
import translate from 'Utilities/String/translate';
import NotificationEventItems from './NotificationEventItems';
import styles from './EditNotificationModalContent.css';
export interface EditNotificationModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteNotificationPress?: () => void;
}
function EditNotificationModalContent({
id,
onModalClose,
onDeleteNotificationPress,
}: EditNotificationModalContentProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isFetching,
error,
isSaving,
isTesting = false,
saveError,
item,
validationErrors,
validationWarnings,
} = useSelector(
createProviderSettingsSelectorHook<Notification, NotificationAppState>(
'notifications',
id
)
);
const wasSaving = usePrevious(isSaving);
const { implementationName, name, fields, tags, message } = item;
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setNotificationValue(change));
},
[dispatch]
);
const handleFieldChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setNotificationFieldValue(change));
},
[dispatch]
);
const handleTestPress = useCallback(() => {
dispatch(testNotification({ id }));
}, [id, dispatch]);
const handleSavePress = useCallback(() => {
dispatch(saveNotification({ id }));
}, [id, dispatch]);
useEffect(() => {
if (wasSaving && !isSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id
? translate('EditConnectionImplementation', { implementationName })
: translate('AddConnectionImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('AddNotificationError')}</Alert>
) : null}
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
{message ? (
<Alert className={styles.message} kind={message.value.type}>
{message.value.message}
</Alert>
) : null}
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
<NotificationEventItems
item={item}
onInputChange={handleInputChange}
/>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('NotificationsTagsSeriesHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
{fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
{...field}
advancedSettings={showAdvancedSettings}
provider="notification"
providerData={item}
onChange={handleFieldChange}
/>
);
})}
</Form>
) : null}
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteNotificationPress}
>
{translate('Delete')}
</Button>
) : null}
<AdvancedSettingsButton showLabel={false} />
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={handleTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditNotificationModalContent;

View file

@ -1,93 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
saveNotification,
setNotificationFieldValues,
setNotificationValue,
testNotification
} from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditNotificationModalContent from './EditNotificationModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('notifications'),
(advancedSettings, notification) => {
return {
advancedSettings,
...notification
};
}
);
}
const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValues,
saveNotification,
testNotification
};
class EditNotificationModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setNotificationValue({ name, value });
};
onFieldChange = ({ name, value, additionalProperties = {} }) => {
this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } });
};
onSavePress = () => {
this.props.saveNotification({ id: this.props.id });
};
onTestPress = () => {
this.props.testNotification({ id: this.props.id });
};
//
// Render
render() {
return (
<EditNotificationModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditNotificationModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setNotificationValue: PropTypes.func.isRequired,
setNotificationFieldValues: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditNotificationModalContentConnector);

View file

@ -1,274 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditNotificationModalConnector from './EditNotificationModalConnector';
import styles from './Notification.css';
class Notification extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditNotificationModalOpen: false,
isDeleteNotificationModalOpen: false
};
}
//
// Listeners
onEditNotificationPress = () => {
this.setState({ isEditNotificationModalOpen: true });
};
onEditNotificationModalClose = () => {
this.setState({ isEditNotificationModalOpen: false });
};
onDeleteNotificationPress = () => {
this.setState({
isEditNotificationModalOpen: false,
isDeleteNotificationModalOpen: true
});
};
onDeleteNotificationModalClose = () => {
this.setState({ isDeleteNotificationModalOpen: false });
};
onConfirmDeleteNotification = () => {
this.props.onConfirmDeleteNotification(this.props.id);
};
//
// Render
render() {
const {
id,
name,
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
onEpisodeFileDelete,
onEpisodeFileDeleteForUpgrade,
onHealthIssue,
onHealthRestored,
onApplicationUpdate,
onManualInteractionRequired,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnImportComplete,
supportsOnRename,
supportsOnSeriesAdd,
supportsOnSeriesDelete,
supportsOnEpisodeFileDelete,
supportsOnEpisodeFileDeleteForUpgrade,
supportsOnHealthIssue,
supportsOnHealthRestored,
supportsOnApplicationUpdate,
supportsOnManualInteractionRequired,
tags,
tagList
} = this.props;
return (
<Card
className={styles.notification}
overlayContent={true}
onPress={this.onEditNotificationPress}
>
<div className={styles.name}>
{name}
</div>
{
supportsOnGrab && onGrab ?
<Label kind={kinds.SUCCESS}>
{translate('OnGrab')}
</Label> :
null
}
{
supportsOnDownload && onDownload ?
<Label kind={kinds.SUCCESS}>
{translate('OnFileImport')}
</Label> :
null
}
{
supportsOnUpgrade && onDownload && onUpgrade ?
<Label kind={kinds.SUCCESS}>
{translate('OnFileUpgrade')}
</Label> :
null
}
{
supportsOnImportComplete && onImportComplete ?
<Label kind={kinds.SUCCESS}>
{translate('OnImportComplete')}
</Label> :
null
}
{
supportsOnRename && onRename ?
<Label kind={kinds.SUCCESS}>
{translate('OnRename')}
</Label> :
null
}
{
supportsOnHealthIssue && onHealthIssue ?
<Label kind={kinds.SUCCESS}>
{translate('OnHealthIssue')}
</Label> :
null
}
{
supportsOnHealthRestored && onHealthRestored ?
<Label kind={kinds.SUCCESS}>
{translate('OnHealthRestored')}
</Label> :
null
}
{
supportsOnApplicationUpdate && onApplicationUpdate ?
<Label kind={kinds.SUCCESS}>
{translate('OnApplicationUpdate')}
</Label> :
null
}
{
supportsOnSeriesAdd && onSeriesAdd ?
<Label kind={kinds.SUCCESS}>
{translate('OnSeriesAdd')}
</Label> :
null
}
{
supportsOnSeriesDelete && onSeriesDelete ?
<Label kind={kinds.SUCCESS}>
{translate('OnSeriesDelete')}
</Label> :
null
}
{
supportsOnEpisodeFileDelete && onEpisodeFileDelete ?
<Label kind={kinds.SUCCESS}>
{translate('OnEpisodeFileDelete')}
</Label> :
null
}
{
supportsOnEpisodeFileDeleteForUpgrade && onEpisodeFileDelete && onEpisodeFileDeleteForUpgrade ?
<Label kind={kinds.SUCCESS}>
{translate('OnEpisodeFileDeleteForUpgrade')}
</Label> :
null
}
{
supportsOnManualInteractionRequired && onManualInteractionRequired ?
<Label kind={kinds.SUCCESS}>
{translate('OnManualInteractionRequired')}
</Label> :
null
}
{
!onGrab && !onDownload && !onRename && !onImportComplete && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label> :
null
}
<TagList
tags={tags}
tagList={tagList}
/>
<EditNotificationModalConnector
id={id}
isOpen={this.state.isEditNotificationModalOpen}
onModalClose={this.onEditNotificationModalClose}
onDeleteNotificationPress={this.onDeleteNotificationPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose}
/>
</Card>
);
}
}
Notification.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
onGrab: PropTypes.bool.isRequired,
onDownload: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired,
onImportComplete: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired,
onSeriesAdd: PropTypes.bool.isRequired,
onSeriesDelete: PropTypes.bool.isRequired,
onEpisodeFileDelete: PropTypes.bool.isRequired,
onEpisodeFileDeleteForUpgrade: PropTypes.bool.isRequired,
onHealthIssue: PropTypes.bool.isRequired,
onHealthRestored: PropTypes.bool.isRequired,
onApplicationUpdate: PropTypes.bool.isRequired,
onManualInteractionRequired: PropTypes.bool.isRequired,
supportsOnGrab: PropTypes.bool.isRequired,
supportsOnDownload: PropTypes.bool.isRequired,
supportsOnImportComplete: PropTypes.bool.isRequired,
supportsOnSeriesAdd: PropTypes.bool.isRequired,
supportsOnSeriesDelete: PropTypes.bool.isRequired,
supportsOnEpisodeFileDelete: PropTypes.bool.isRequired,
supportsOnEpisodeFileDeleteForUpgrade: PropTypes.bool.isRequired,
supportsOnUpgrade: PropTypes.bool.isRequired,
supportsOnRename: PropTypes.bool.isRequired,
supportsOnHealthIssue: PropTypes.bool.isRequired,
supportsOnHealthRestored: PropTypes.bool.isRequired,
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
supportsOnManualInteractionRequired: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired
};
export default Notification;

View file

@ -0,0 +1,179 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import { deleteNotification } from 'Store/Actions/settingsActions';
import useTags from 'Tags/useTags';
import NotificationModel from 'typings/Notification';
import translate from 'Utilities/String/translate';
import EditNotificationModal from './EditNotificationModal';
import styles from './Notification.css';
function Notification({
id,
name,
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
onEpisodeFileDelete,
onEpisodeFileDeleteForUpgrade,
onHealthIssue,
onHealthRestored,
onApplicationUpdate,
onManualInteractionRequired,
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnImportComplete,
supportsOnRename,
supportsOnSeriesAdd,
supportsOnSeriesDelete,
supportsOnEpisodeFileDelete,
supportsOnEpisodeFileDeleteForUpgrade,
supportsOnHealthIssue,
supportsOnHealthRestored,
supportsOnApplicationUpdate,
supportsOnManualInteractionRequired,
tags,
}: NotificationModel) {
const dispatch = useDispatch();
const tagList = useTags();
const [isEditNotificationModalOpen, setIsEditNotificationModalOpen] =
useState(false);
const [isDeleteNotificationModalOpen, setIsDeleteNotificationModalOpen] =
useState(false);
const handleEditNotificationPress = useCallback(() => {
setIsEditNotificationModalOpen(true);
}, []);
const handleEditNotificationModalClose = useCallback(() => {
setIsEditNotificationModalOpen(false);
}, []);
const handleDeleteNotificationPress = useCallback(() => {
setIsEditNotificationModalOpen(false);
setIsDeleteNotificationModalOpen(true);
}, []);
const handleDeleteNotificationModalClose = useCallback(() => {
setIsDeleteNotificationModalOpen(false);
}, []);
const handleConfirmDeleteNotification = useCallback(() => {
dispatch(deleteNotification({ id }));
}, [id, dispatch]);
return (
<Card
className={styles.notification}
overlayContent={true}
onPress={handleEditNotificationPress}
>
<div className={styles.name}>{name}</div>
{supportsOnGrab && onGrab ? (
<Label kind={kinds.SUCCESS}>{translate('OnGrab')}</Label>
) : null}
{supportsOnDownload && onDownload ? (
<Label kind={kinds.SUCCESS}>{translate('OnFileImport')}</Label>
) : null}
{supportsOnUpgrade && onDownload && onUpgrade ? (
<Label kind={kinds.SUCCESS}>{translate('OnFileUpgrade')}</Label>
) : null}
{supportsOnImportComplete && onImportComplete ? (
<Label kind={kinds.SUCCESS}>{translate('OnImportComplete')}</Label>
) : null}
{supportsOnRename && onRename ? (
<Label kind={kinds.SUCCESS}>{translate('OnRename')}</Label>
) : null}
{supportsOnHealthIssue && onHealthIssue ? (
<Label kind={kinds.SUCCESS}>{translate('OnHealthIssue')}</Label>
) : null}
{supportsOnHealthRestored && onHealthRestored ? (
<Label kind={kinds.SUCCESS}>{translate('OnHealthRestored')}</Label>
) : null}
{supportsOnApplicationUpdate && onApplicationUpdate ? (
<Label kind={kinds.SUCCESS}>{translate('OnApplicationUpdate')}</Label>
) : null}
{supportsOnSeriesAdd && onSeriesAdd ? (
<Label kind={kinds.SUCCESS}>{translate('OnSeriesAdd')}</Label>
) : null}
{supportsOnSeriesDelete && onSeriesDelete ? (
<Label kind={kinds.SUCCESS}>{translate('OnSeriesDelete')}</Label>
) : null}
{supportsOnEpisodeFileDelete && onEpisodeFileDelete ? (
<Label kind={kinds.SUCCESS}>{translate('OnEpisodeFileDelete')}</Label>
) : null}
{supportsOnEpisodeFileDeleteForUpgrade &&
onEpisodeFileDelete &&
onEpisodeFileDeleteForUpgrade ? (
<Label kind={kinds.SUCCESS}>
{translate('OnEpisodeFileDeleteForUpgrade')}
</Label>
) : null}
{supportsOnManualInteractionRequired && onManualInteractionRequired ? (
<Label kind={kinds.SUCCESS}>
{translate('OnManualInteractionRequired')}
</Label>
) : null}
{!onGrab &&
!onDownload &&
!onRename &&
!onImportComplete &&
!onHealthIssue &&
!onHealthRestored &&
!onApplicationUpdate &&
!onSeriesAdd &&
!onSeriesDelete &&
!onEpisodeFileDelete &&
!onManualInteractionRequired ? (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
) : null}
<TagList tags={tags} tagList={tagList} />
<EditNotificationModal
id={id}
isOpen={isEditNotificationModalOpen}
onModalClose={handleEditNotificationModalClose}
onDeleteNotificationPress={handleDeleteNotificationPress}
/>
<ConfirmModal
isOpen={isDeleteNotificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeleteNotification}
onCancel={handleDeleteNotificationModalClose}
/>
</Card>
);
}
export default Notification;

View file

@ -1,19 +1,24 @@
import PropTypes from 'prop-types';
import React from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import Notification from 'typings/Notification';
import { PendingSection } from 'typings/pending';
import translate from 'Utilities/String/translate';
import styles from './NotificationEventItems.css';
function NotificationEventItems(props) {
const {
item,
onInputChange
} = props;
interface NotificationEventItemsProps {
item: PendingSection<Notification>;
onInputChange: (change: CheckInputChanged) => void;
}
function NotificationEventItems({
item,
onInputChange,
}: NotificationEventItemsProps) {
const {
onGrab,
onDownload,
@ -41,7 +46,7 @@ function NotificationEventItems(props) {
supportsOnManualInteractionRequired,
supportsOnHealthIssue,
supportsOnHealthRestored,
includeHealthWarnings
includeHealthWarnings,
} = item;
return (
@ -75,19 +80,18 @@ function NotificationEventItems(props) {
/>
</div>
{
onDownload.value &&
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText={translate('OnFileUpgrade')}
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
onChange={onInputChange}
/>
</div>
}
{onDownload.value && (
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText={translate('OnFileUpgrade')}
isDisabled={!supportsOnUpgrade.value}
{...onUpgrade}
onChange={onInputChange}
/>
</div>
)}
<div>
<FormInputGroup
@ -144,19 +148,18 @@ function NotificationEventItems(props) {
/>
</div>
{
onEpisodeFileDelete.value &&
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText={translate('OnEpisodeFileDeleteForUpgrade')}
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
{...onEpisodeFileDeleteForUpgrade}
onChange={onInputChange}
/>
</div>
}
{onEpisodeFileDelete.value && (
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText={translate('OnEpisodeFileDeleteForUpgrade')}
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
{...onEpisodeFileDeleteForUpgrade}
onChange={onInputChange}
/>
</div>
)}
<div>
<FormInputGroup
@ -180,19 +183,18 @@ function NotificationEventItems(props) {
/>
</div>
{
(onHealthIssue.value || onHealthRestored.value) &&
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="includeHealthWarnings"
helpText={translate('IncludeHealthWarnings')}
isDisabled={!supportsOnHealthIssue.value}
{...includeHealthWarnings}
onChange={onInputChange}
/>
</div>
}
{(onHealthIssue.value || onHealthRestored.value) && (
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="includeHealthWarnings"
helpText={translate('IncludeHealthWarnings')}
isDisabled={!supportsOnHealthIssue.value}
{...includeHealthWarnings}
onChange={onInputChange}
/>
</div>
)}
<div>
<FormInputGroup
@ -221,9 +223,4 @@ function NotificationEventItems(props) {
);
}
NotificationEventItems.propTypes = {
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default NotificationEventItems;

View file

@ -1,118 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddNotificationModal from './AddNotificationModal';
import EditNotificationModalConnector from './EditNotificationModalConnector';
import Notification from './Notification';
import styles from './Notifications.css';
class Notifications extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddNotificationModalOpen: false,
isEditNotificationModalOpen: false
};
}
//
// Listeners
onAddNotificationPress = () => {
this.setState({ isAddNotificationModalOpen: true });
};
onAddNotificationModalClose = ({ notificationSelected = false } = {}) => {
this.setState({
isAddNotificationModalOpen: false,
isEditNotificationModalOpen: notificationSelected
});
};
onEditNotificationModalClose = () => {
this.setState({ isEditNotificationModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
onConfirmDeleteNotification,
...otherProps
} = this.props;
const {
isAddNotificationModalOpen,
isEditNotificationModalOpen
} = this.state;
return (
<FieldSet legend={translate('Connections')}>
<PageSectionContent
errorMessage={translate('NotificationsLoadError')}
{...otherProps}
>
<div className={styles.notifications}>
{
items.map((item) => {
return (
<Notification
key={item.id}
{...item}
tagList={tagList}
onConfirmDeleteNotification={onConfirmDeleteNotification}
/>
);
})
}
<Card
className={styles.addNotification}
onPress={this.onAddNotificationPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddNotificationModal
isOpen={isAddNotificationModalOpen}
onModalClose={this.onAddNotificationModalClose}
/>
<EditNotificationModalConnector
isOpen={isEditNotificationModalOpen}
onModalClose={this.onEditNotificationModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
Notifications.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired
};
export default Notifications;

View file

@ -0,0 +1,94 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { NotificationAppState } 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 { fetchNotifications } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import NotificationModel from 'typings/Notification';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AddNotificationModal from './AddNotificationModal';
import EditNotificationModal from './EditNotificationModal';
import Notification from './Notification';
import styles from './Notifications.css';
function Notifications() {
const dispatch = useDispatch();
const { error, isFetching, isPopulated, items } = useSelector(
createSortedSectionSelector<NotificationModel, NotificationAppState>(
'settings.notifications',
sortByProp('name')
)
);
const [isAddNotificationModalOpen, setIsAddNotificationModalOpen] =
useState(false);
const [isEditNotificationModalOpen, setIsEditNotificationModalOpen] =
useState(false);
const handleAddNotificationPress = useCallback(() => {
setIsAddNotificationModalOpen(true);
}, []);
const handleNotificationSelect = useCallback(() => {
setIsAddNotificationModalOpen(false);
setIsEditNotificationModalOpen(true);
}, []);
const handleAddNotificationModalClose = useCallback(() => {
setIsAddNotificationModalOpen(false);
}, []);
const handleEditNotificationModalClose = useCallback(() => {
setIsEditNotificationModalOpen(false);
}, []);
useEffect(() => {
dispatch(fetchNotifications());
}, [dispatch]);
return (
<FieldSet legend={translate('Connections')}>
<PageSectionContent
errorMessage={translate('NotificationsLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
>
<div className={styles.notifications}>
{items.map((item) => (
<Notification key={item.id} {...item} />
))}
<Card
className={styles.addNotification}
onPress={handleAddNotificationPress}
>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<AddNotificationModal
isOpen={isAddNotificationModalOpen}
onNotificationSelect={handleNotificationSelect}
onModalClose={handleAddNotificationModalClose}
/>
<EditNotificationModal
isOpen={isEditNotificationModalOpen}
onModalClose={handleEditNotificationModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
export default Notifications;

View file

@ -1,63 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteNotification, fetchNotifications } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Notifications from './Notifications';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.notifications', sortByProp('name')),
createTagsSelector(),
(notifications, tagList) => {
return {
...notifications,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchNotifications,
deleteNotification
};
class NotificationsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchNotifications();
}
//
// Listeners
onConfirmDeleteNotification = (id) => {
this.props.deleteNotification({ id });
};
//
// Render
render() {
return (
<Notifications
{...this.props}
onConfirmDeleteNotification={this.onConfirmDeleteNotification}
/>
);
}
}
NotificationsConnector.propTypes = {
fetchNotifications: PropTypes.func.isRequired,
deleteNotification: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(NotificationsConnector);

View file

@ -2,6 +2,33 @@ import Provider from './Provider';
interface Notification extends Provider {
enable: boolean;
onGrab: boolean;
onDownload: boolean;
onUpgrade: boolean;
onImportComplete: boolean;
onRename: boolean;
onSeriesAdd: boolean;
onSeriesDelete: boolean;
onEpisodeFileDelete: boolean;
onEpisodeFileDeleteForUpgrade: boolean;
onHealthIssue: boolean;
includeHealthWarnings: boolean;
onHealthRestored: boolean;
onApplicationUpdate: boolean;
onManualInteractionRequired: boolean;
supportsOnGrab: boolean;
supportsOnDownload: boolean;
supportsOnUpgrade: boolean;
supportsOnImportComplete: boolean;
supportsOnRename: boolean;
supportsOnSeriesAdd: boolean;
supportsOnSeriesDelete: boolean;
supportsOnEpisodeFileDelete: boolean;
supportsOnEpisodeFileDeleteForUpgrade: boolean;
supportsOnHealthIssue: boolean;
supportsOnHealthRestored: boolean;
supportsOnApplicationUpdate: boolean;
supportsOnManualInteractionRequired: boolean;
tags: number[];
}