resolve merge conflict

This commit is contained in:
James 2024-12-26 23:44:06 +11:00
parent 1c30ecd66d
commit 06baead15f
34 changed files with 1679 additions and 13 deletions

View file

@ -14,6 +14,7 @@ import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import NotificationTemplate from 'typings/Settings/NotificationTemplate';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import NamingConfig from 'typings/Settings/NamingConfig';
@ -55,6 +56,12 @@ export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export interface NotificationTemplateAppState
extends AppSectionState<NotificationTemplate>,
AppSectionSaveState {
pendingChanges: Partial<NotificationTemplate>;
}
export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionItemSchemaState<QualityProfile> {}
@ -101,6 +108,7 @@ interface SettingsAppState {
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
notifications: NotificationAppState;
notificationTemplates: NotificationTemplateAppState;
qualityProfiles: QualityProfilesAppState;
releaseProfiles: ReleaseProfilesAppState;
ui: UiSettingsAppState;

View file

@ -20,6 +20,7 @@ import EnhancedSelectInput from './Select/EnhancedSelectInput';
import IndexerFlagsSelectInput from './Select/IndexerFlagsSelectInput';
import IndexerSelectInput from './Select/IndexerSelectInput';
import LanguageSelectInput from './Select/LanguageSelectInput';
import NotificationTemplateSelectInput from './Select/NotificationTemplateSelectInput';
import MonitorEpisodesSelectInput from './Select/MonitorEpisodesSelectInput';
import MonitorNewItemsSelectInput from './Select/MonitorNewItemsSelectInput';
import ProviderDataSelectInput from './Select/ProviderOptionSelectInput';
@ -82,6 +83,9 @@ function getComponent(type: InputType) {
case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInput;
case inputTypes.NOTIFICATION_TEMPLATE_SELECT:
return NotificationTemplateSelectInput;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInput;

View file

@ -0,0 +1,81 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { fetchNotificationTemplates } from 'Store/Actions/settingsActions';
import { EnhancedSelectInputChanged } from 'typings/inputs';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
function createNotificationTemplatesSelector(includeAny: boolean) {
return createSelector(
(state: AppState) => state.settings.notificationTemplates,
(notificationTemplates) => {
const { isFetching, isPopulated, error, items } = notificationTemplates;
const values = items.sort(sortByProp('name')).map((notificationTemplate) => {
return {
key: notificationTemplate.id,
value: notificationTemplate.name,
};
});
if (includeAny) {
values.unshift({
key: 0,
value: `(${translate('Fallback')})`,
});
}
return {
isFetching,
isPopulated,
error,
values,
};
}
);
}
interface NotificationTemplateSelectInputConnectorProps {
name: string;
value: number;
includeAny?: boolean;
values: object[];
onChange: (change: EnhancedSelectInputChanged<number>) => void;
}
function NotificationTemplateSelectInput({
name,
value,
includeAny = false,
onChange,
}: NotificationTemplateSelectInputConnectorProps) {
const dispatch = useDispatch();
const { isFetching, isPopulated, values } = useSelector(
createNotificationTemplatesSelector(includeAny)
);
useEffect(() => {
if (!isPopulated) {
dispatch(fetchNotificationTemplates());
}
}, [isPopulated, dispatch]);
return (
<EnhancedSelectInput
name={name}
value={value}
isFetching={isFetching}
values={values}
onChange={onChange}
/>
);
}
NotificationTemplateSelectInput.defaultProps = {
includeAny: false,
};
export default NotificationTemplateSelectInput;

View file

@ -14,6 +14,7 @@ export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect';
export const NOTIFICATION_TEMPLATE_SELECT = 'notificationTemplateSelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
@ -42,6 +43,7 @@ export const all = [
PATH,
QUALITY_PROFILE_SELECT,
INDEXER_SELECT,
NOTIFICATION_TEMPLATE_SELECT,
DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT,
LANGUAGE_SELECT,
@ -75,6 +77,7 @@ export type InputType =
| 'indexerSelect'
| 'indexerFlagsSelect'
| 'languageSelect'
| 'notificationTemplateSelect'
| 'downloadClientSelect'
| 'rootFolderSelect'
| 'select'

View file

@ -4,6 +4,7 @@ import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import NotificationsConnector from './Notifications/NotificationsConnector';
import NotificationTemplates from './NotificationTemplates/NotificationTemplates';
function NotificationSettings() {
return (
@ -14,6 +15,7 @@ function NotificationSettings() {
<PageContentBody>
<NotificationsConnector />
<NotificationTemplates />
</PageContentBody>
</PageContent>
);

View file

@ -0,0 +1,41 @@
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 EditNotificationTemplateModalContent from './EditNotificationTemplateModalContent';
interface EditNotificationTemplateModalProps {
id?: number;
isOpen: boolean;
onModalClose: () => void;
onDeleteNotificationTemplatePress?: () => void;
}
function EditNotificationTemplateModal({
isOpen,
onModalClose,
...otherProps
}: EditNotificationTemplateModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(
clearPendingChanges({
section: 'settings.notificationTemplates',
})
);
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditNotificationTemplateModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditNotificationTemplateModal;

View file

@ -0,0 +1,11 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
.tagInternalInput {
composes: internalInput from '~Components/Form/Tag/TagInput.css';
flex: 0 0 100%;
}

View file

@ -0,0 +1,8 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'tagInternalInput': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -0,0 +1,342 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
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 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 { inputTypes, kinds } from 'Helpers/Props';
import {
saveNotificationTemplate,
setNotificationTemplateValue,
} from 'Store/Actions/Settings/notificationTemplates';
import selectSettings from 'Store/Selectors/selectSettings';
import NotificationTemplate from 'typings/Settings/NotificationTemplate';
import translate from 'Utilities/String/translate';
import styles from './EditNotificationTemplateModalContent.css';
const newNotificationTemplate: NotificationTemplate = {
id: 0,
name: '',
title: '',
body: '',
onGrab: true,
onDownload: true,
onUpgrade: true,
onImportComplete: true,
onRename: false,
onSeriesAdd: true,
onSeriesDelete: false,
onEpisodeFileDelete: false,
onEpisodeFileDeleteForUpgrade: false,
onHealthIssue: false,
onHealthRestored: false,
onApplicationUpdate: false,
onManualInteractionRequired: false
};
function createNotificationTemplateSelector(id?: number) {
return createSelector(
(state: AppState) => state.settings.notificationTemplates,
(notificationTemplates) => {
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
notificationTemplates;
const mapping = id ? items.find((i) => i.id === id)! : newNotificationTemplate;
const settings = selectSettings<NotificationTemplate>(
mapping,
pendingChanges,
saveError
);
return {
id,
isFetching,
error,
isSaving,
saveError,
item: settings.settings,
...settings,
};
}
);
}
interface EditNotificationTemplateModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteNotificationTemplatePress?: () => void;
}
function EditNotificationTemplateModalContent({
id,
onModalClose,
onDeleteNotificationTemplatePress,
}: EditNotificationTemplateModalContentProps) {
const { item, isFetching, isSaving, error, saveError, ...otherProps } =
useSelector(createNotificationTemplateSelector(id));
const {
name,
title,
body,
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
onEpisodeFileDelete,
onEpisodeFileDeleteForUpgrade,
onHealthIssue,
onHealthRestored,
onApplicationUpdate,
onManualInteractionRequired
} = item;
const dispatch = useDispatch();
const previousIsSaving = usePrevious(isSaving);
useEffect(() => {
if (!id) {
Object.entries(newNotificationTemplate).forEach(([name, value]) => {
// @ts-expect-error 'setNotificationTemplateValue' isn't typed yet
dispatch(setNotificationTemplateValue({ name, value }));
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (previousIsSaving && !isSaving && !saveError) {
onModalClose();
}
}, [previousIsSaving, isSaving, saveError, onModalClose]);
const handleSavePress = useCallback(() => {
dispatch(saveNotificationTemplate({ id }));
}, [dispatch, id]);
const onInputChange = useCallback(
(payload: { name: string; value: string | number | boolean }) => {
// @ts-expect-error 'setNotificationTemplateValue' isn't typed yet
dispatch(setNotificationTemplateValue(payload));
},
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditNotificationTemplate') : translate('AddNotificationTemplate')}
</ModalHeader>
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
canEdit={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Title')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_AREA}
name="title"
helpText={translate('NotificationTemplateTitleHelpText')}
{...title}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Body')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_AREA}
name="body"
helpText={translate('NotificationTemplateBodyHelpText')}
{...body}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('NotificationTriggers')}</FormLabel>
<div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onGrab"
helpText={translate('OnGrab')}
{...onGrab}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onDownload"
helpText={translate('OnFileImport')}
{...onDownload}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onUpgrade"
helpText={translate('OnFileUpgrade')}
{...onUpgrade}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onImportComplete"
helpText={translate('OnImportComplete')}
{...onImportComplete}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onRename"
helpText={translate('OnRename')}
{...onRename}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onSeriesAdd"
helpText={translate('OnSeriesAdd')}
{...onSeriesAdd}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onSeriesDelete"
helpText={translate('OnSeriesDelete')}
{...onSeriesDelete}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDelete"
helpText={translate('OnEpisodeFileDelete')}
{...onEpisodeFileDelete}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onEpisodeFileDeleteForUpgrade"
helpText={translate('OnEpisodeFileDeleteForUpgrade')}
{...onEpisodeFileDeleteForUpgrade}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onHealthIssue"
helpText={translate('OnHealthIssue')}
{...onHealthIssue}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onHealthRestored"
helpText={translate('OnHealthRestored')}
{...onHealthRestored}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onApplicationUpdate"
helpText={translate('OnApplicationUpdate')}
{...onApplicationUpdate}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onManualInteractionRequired"
helpText={translate('OnManualInteractionRequired')}
{...onManualInteractionRequired}
onChange={onInputChange}
/>
</div>
</div>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteNotificationTemplatePress}
>
{translate('Delete')}
</Button>
) : null}
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditNotificationTemplateModalContent;

View file

@ -0,0 +1,25 @@
.notificationTemplate {
composes: card from '~Components/Card.css';
width: 290px;
}
.enabled {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.label {
composes: label from '~Components/Label.css';
max-width: 100%;
}

View file

@ -0,0 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'enabled': string;
'label': string;
'name': string;
'notificationTemplate': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -0,0 +1,190 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { kinds } from 'Helpers/Props';
import { deleteNotificationTemplate } from 'Store/Actions/Settings/notificationTemplates';
import NotificationTemplate from 'typings/Settings/NotificationTemplate';
import translate from 'Utilities/String/translate';
import EditNotificationTemplateModal from './EditNotificationTemplateModal';
import styles from './NotificationTemplateItem.css';
interface NotificationTemplateProps extends NotificationTemplate {
title: string;
body: string;
onGrab: boolean;
onDownload: boolean;
onUpgrade: boolean;
onImportComplete: boolean;
onRename: boolean;
onSeriesAdd: boolean;
onSeriesDelete: boolean;
onEpisodeFileDelete: boolean;
onEpisodeFileDeleteForUpgrade: boolean;
onHealthIssue: boolean;
onHealthRestored: boolean;
onApplicationUpdate: boolean;
onManualInteractionRequired: boolean;
}
function NotificationTemplateItem(props: NotificationTemplateProps) {
const {
id,
name,
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
onEpisodeFileDelete,
onEpisodeFileDeleteForUpgrade,
onHealthIssue,
onHealthRestored,
onApplicationUpdate,
onManualInteractionRequired
} = props;
const dispatch = useDispatch();
const [
isEditNotificationTemplateModalOpen,
setEditNotificationTemplateModalOpen,
setEditNotificationTemplateModalClosed,
] = useModalOpenState(false);
const [
isDeleteNotificationTemplateModalOpen,
setDeleteNotificationTemplateModalOpen,
setDeleteNotificationTemplateModalClosed,
] = useModalOpenState(false);
const handleDeletePress = useCallback(() => {
dispatch(deleteNotificationTemplate({ id }));
}, [id, dispatch]);
return (
<Card
className={styles.notificationTemplate}
overlayContent={true}
onPress={setEditNotificationTemplateModalOpen}
>
{name ? <div className={styles.name}>{name}</div> : null}
{
onGrab ?
<Label kind={kinds.SUCCESS}>
{translate('OnGrab')}
</Label> :
null
}
{
onDownload ?
<Label kind={kinds.SUCCESS}>
{translate('OnFileImport')}
</Label> :
null
}
{
onUpgrade ?
<Label kind={kinds.SUCCESS}>
{translate('OnFileUpgrade')}
</Label> :
null
}
{
onImportComplete ?
<Label kind={kinds.SUCCESS}>
{translate('OnImportComplete')}
</Label> :
null
}
{
onRename ?
<Label kind={kinds.SUCCESS}>
{translate('OnRename')}
</Label> :
null
}
{
onSeriesAdd ?
<Label kind={kinds.SUCCESS}>
{translate('OnSeriesAdd')}
</Label> :
null
}
{
onSeriesDelete ?
<Label kind={kinds.SUCCESS}>
{translate('OnSeriesDelete')}
</Label> :
null
}
{
onEpisodeFileDelete ?
<Label kind={kinds.SUCCESS}>
{translate('OnEpisodeFileDelete')}
</Label> :
null
}
{
onEpisodeFileDeleteForUpgrade ?
<Label kind={kinds.SUCCESS}>
{translate('OnEpisodeFileDeleteForUpgrade')}
</Label> :
null
}
{
onHealthIssue ?
<Label kind={kinds.SUCCESS}>
{translate('OnHealthIssue')}
</Label> :
null
}
{
onHealthRestored ?
<Label kind={kinds.SUCCESS}>
{translate('OnHealthRestored')}
</Label> :
null
}
{
onApplicationUpdate ?
<Label kind={kinds.SUCCESS}>
{translate('OnApplicationUpdate')}
</Label> :
null
}
{
onManualInteractionRequired ?
<Label kind={kinds.SUCCESS}>
{translate('OnManualInteractionRequired')}
</Label> :
null
}
<EditNotificationTemplateModal
id={id}
isOpen={isEditNotificationTemplateModalOpen}
onModalClose={setEditNotificationTemplateModalClosed}
onDeleteNotificationTemplatePress={setDeleteNotificationTemplateModalOpen}
/>
<ConfirmModal
isOpen={isDeleteNotificationTemplateModalOpen}
kind={kinds.DANGER}
title={translate('DeleteNotificationTemplate')}
message={translate('DeleteNotificationTemplateMessageText', {
name: name ?? id,
})}
confirmLabel={translate('Delete')}
onConfirm={handleDeletePress}
onCancel={setDeleteNotificationTemplateModalClosed}
/>
</Card>
);
}
export default NotificationTemplateItem;

View file

@ -0,0 +1,19 @@
.notificationTemplates {
display: flex;
flex-wrap: wrap;
}
.addNotificationTemplate {
composes: notificationTemplate from '~./NotificationTemplateItem.css';
background-color: var(--cardAlternateBackgroundColor);
color: var(--gray);
text-align: center;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--cardCenterBackgroundColor);
}

View file

@ -0,0 +1,9 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'addNotificationTemplate': string;
'center': string;
'notificationTemplates': string;
}
export const cssExports: CssExports;
export default cssExports;

View file

@ -0,0 +1,70 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { NotificationTemplateAppState } 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 useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons } from 'Helpers/Props';
import { fetchNotificationTemplates } from 'Store/Actions/Settings/notificationTemplates';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import translate from 'Utilities/String/translate';
import EditNotificationTemplateModal from './EditNotificationTemplateModal';
import NotificationTemplateItem from './NotificationTemplateItem';
import styles from './NotificationTemplates.css';
function NotificationTemplates() {
const { items, isFetching, isPopulated, error }: NotificationTemplateAppState =
useSelector(createClientSideCollectionSelector('settings.notificationTemplates'));
const dispatch = useDispatch();
const [
isAddNotificationTemplateModalOpen,
setAddNotificationTemplateModalOpen,
setAddNotificationTemplateModalClosed,
] = useModalOpenState(false);
useEffect(() => {
dispatch(fetchNotificationTemplates());
}, [dispatch]);
return (
<FieldSet legend={translate('NotificationTemplates')}>
<PageSectionContent
errorMessage={translate('NotificationTemplatesLoadError')}
isFetching={isFetching}
isPopulated={isPopulated}
error={error}
>
<div className={styles.notificationTemplates}>
{items.map((item) => {
return (
<NotificationTemplateItem
key={item.id}
{...item}
/>
);
})}
<Card
className={styles.addNotificationTemplate}
onPress={setAddNotificationTemplateModalOpen}
>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<EditNotificationTemplateModal
isOpen={isAddNotificationTemplateModalOpen}
onModalClose={setAddNotificationTemplateModalClosed}
/>
</PageSectionContent>
</FieldSet>
);
}
export default NotificationTemplates;

View file

@ -44,7 +44,8 @@ function EditNotificationModalContent(props) {
name,
tags,
fields,
message
message,
notificationTemplateId
} = item;
return (
@ -95,6 +96,23 @@ function EditNotificationModalContent(props) {
onInputChange={onInputChange}
/>
{
item.implementationName === 'Email' ?
<FormGroup>
<FormLabel>{translate('NotificationTemplate')}</FormLabel>
<FormInputGroup
type={inputTypes.NOTIFICATION_TEMPLATE_SELECT}
name="notificationTemplateId"
helpText={translate('NotificationNotificationTemplateHelpText')}
{...notificationTemplateId}
includeAny={true}
onChange={onInputChange}
/>
</FormGroup> :
null
}
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>

View file

@ -266,6 +266,7 @@ Notification.propTypes = {
supportsOnHealthRestored: PropTypes.bool.isRequired,
supportsOnApplicationUpdate: PropTypes.bool.isRequired,
supportsOnManualInteractionRequired: PropTypes.bool.isRequired,
notificationTemplateId: PropTypes.number.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteNotification: PropTypes.func.isRequired

View file

@ -0,0 +1,71 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.notificationTemplates';
//
// Actions Types
export const FETCH_NOTIFICATION_TEMPLATES = 'settings/notificationTemplates/fetchNotificationTemplates';
export const SAVE_NOTIFICATION_TEMPLATE = 'settings/notificationTemplates/saveNotificationTemplate';
export const DELETE_NOTIFICATION_TEMPLATE = 'settings/notificationTemplates/deleteNotificationTemplate';
export const SET_NOTIFICATION_TEMPLATE_VALUE = 'settings/notificationTemplates/setNotificationTemplateValue';
//
// Action Creators
export const fetchNotificationTemplates = createThunk(FETCH_NOTIFICATION_TEMPLATES);
export const saveNotificationTemplate = createThunk(SAVE_NOTIFICATION_TEMPLATE);
export const deleteNotificationTemplate = createThunk(DELETE_NOTIFICATION_TEMPLATE);
export const setNotificationTemplateValue = createAction(SET_NOTIFICATION_TEMPLATE_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_NOTIFICATION_TEMPLATES]: createFetchHandler(section, '/notificationtemplate'),
[SAVE_NOTIFICATION_TEMPLATE]: createSaveProviderHandler(section, '/notificationtemplate'),
[DELETE_NOTIFICATION_TEMPLATE]: createRemoveItemHandler(section, '/notificationtemplate')
},
//
// Reducers
reducers: {
[SET_NOTIFICATION_TEMPLATE_VALUE]: createSetSettingValueReducer(section)
}
};

View file

@ -21,6 +21,7 @@ import metadata from './Settings/metadata';
import naming from './Settings/naming';
import namingExamples from './Settings/namingExamples';
import notifications from './Settings/notifications';
import notificationTemplates from './Settings/notificationTemplates';
import qualityDefinitions from './Settings/qualityDefinitions';
import qualityProfiles from './Settings/qualityProfiles';
import releaseProfiles from './Settings/releaseProfiles';
@ -47,6 +48,7 @@ export * from './Settings/metadata';
export * from './Settings/naming';
export * from './Settings/namingExamples';
export * from './Settings/notifications';
export * from './Settings/notificationTemplates';
export * from './Settings/qualityDefinitions';
export * from './Settings/qualityProfiles';
export * from './Settings/releaseProfiles';
@ -83,6 +85,7 @@ export const defaultState = {
naming: naming.defaultState,
namingExamples: namingExamples.defaultState,
notifications: notifications.defaultState,
notificationTemplates: notificationTemplates.defaultState,
qualityDefinitions: qualityDefinitions.defaultState,
qualityProfiles: qualityProfiles.defaultState,
releaseProfiles: releaseProfiles.defaultState,
@ -129,6 +132,7 @@ export const actionHandlers = handleThunks({
...naming.actionHandlers,
...namingExamples.actionHandlers,
...notifications.actionHandlers,
...notificationTemplates.actionHandlers,
...qualityDefinitions.actionHandlers,
...qualityProfiles.actionHandlers,
...releaseProfiles.actionHandlers,
@ -165,6 +169,7 @@ export const reducers = createHandleActions({
...naming.reducers,
...namingExamples.reducers,
...notifications.reducers,
...notificationTemplates.reducers,
...qualityDefinitions.reducers,
...qualityProfiles.reducers,
...releaseProfiles.reducers,

View file

@ -0,0 +1,22 @@
import ModelBase from 'App/ModelBase';
interface NotificationTemplate extends ModelBase {
name: string;
title: string;
body: string;
onGrab: boolean;
onDownload: boolean;
onUpgrade: boolean;
onImportComplete: boolean;
onRename: boolean;
onSeriesAdd: boolean;
onSeriesDelete: boolean;
onEpisodeFileDelete: boolean;
onEpisodeFileDeleteForUpgrade: boolean;
onHealthIssue: boolean;
onHealthRestored: boolean;
onApplicationUpdate: boolean;
onManualInteractionRequired: boolean;
}
export default NotificationTemplate;

View file

@ -0,0 +1,100 @@
using System.Data;
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(216)]
public class add_notification_template : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Create.TableForModel("NotificationTemplates")
.WithColumn("Name").AsString().NotNullable().Unique()
.WithColumn("Title").AsString().NotNullable()
.WithColumn("Body").AsString().NotNullable()
.WithColumn("OnGrab").AsBoolean().WithDefaultValue(true)
.WithColumn("OnDownload").AsBoolean().WithDefaultValue(true)
.WithColumn("OnUpgrade").AsBoolean().WithDefaultValue(true)
.WithColumn("OnImportComplete").AsBoolean().WithDefaultValue(true)
.WithColumn("OnRename").AsBoolean().WithDefaultValue(false)
.WithColumn("OnSeriesAdd").AsBoolean().WithDefaultValue(true)
.WithColumn("OnSeriesDelete").AsBoolean().WithDefaultValue(false)
.WithColumn("OnEpisodeFileDelete").AsBoolean().WithDefaultValue(false)
.WithColumn("OnEpisodeFileDeleteForUpgrade").AsBoolean().WithDefaultValue(false)
.WithColumn("OnHealthIssue").AsBoolean().WithDefaultValue(false)
.WithColumn("OnHealthRestored").AsBoolean().WithDefaultValue(false)
.WithColumn("OnApplicationUpdate").AsBoolean().WithDefaultValue(false)
.WithColumn("OnManualInteractionRequired").AsBoolean().WithDefaultValue(false);
Alter.Table("Notifications").AddColumn("NotificationTemplateId").AsInt32().WithDefaultValue(0);
Execute.WithConnection(CreateDefaultHtmlTemplate);
Execute.WithConnection(UpdateEmailConnections);
}
private void CreateDefaultHtmlTemplate(IDbConnection conn, IDbTransaction tran)
{
var name = "Email template";
var title = "Sonarr - {{ if grab_message }}Episode Grabbed{{ else if series_add_message }}Series Added{{ else }}{{fallback_title}}{{ end }}";
var body = @"<!DOCTYPE html>
<html lang=""en"" xmlns:th=""http://www.thymeleaf.org"">
<head>
<title>Sonarr Notification</title>
</head>
<body>
{{ if grab_message }}
{{ series = grab_message.series }}
<p>{{grab_message.episode.parsed_episode_info.series_title}} - {{grab_message.episode.parsed_episode_info.release_title}} sent to queue.</p>
{{ else if series_add_message }}
{{ series = series_add_message.series }}
{{ else }}
<p>{{fallback_body}}</p>
{{ end }}
{{ if series }}
<h3>{{series.title}}</h3>
<p>{{series.overview}}</p>
{{- for image in series.images }}
{{ if image.cover_type == ""Banner"" }}
<img src=""{{image.remote_url}}"" alt=""Series banner"">
{{ end }}
{{- end }}
{{ end }}
<div id=""footer"">
<p>Metadata is provided by theTVDB</p>
</div>
</body>
</html>";
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "INSERT INTO \"NotificationTemplates\" (\"Name\", \"Title\", \"Body\") VALUES (?, ?, ?)";
updateCmd.AddParameter(name);
updateCmd.AddParameter(title);
updateCmd.AddParameter(body);
updateCmd.ExecuteNonQuery();
}
}
private void UpdateEmailConnections(IDbConnection conn, IDbTransaction tran)
{
using (var selectCmd = conn.CreateCommand())
{
selectCmd.Transaction = tran;
selectCmd.CommandText = "SELECT \"Id\" from \"NotificationTemplates\" DESC LIMIT 1";
var id = selectCmd.ExecuteReader().Read();
using (var updateCmd = conn.CreateCommand())
{
updateCmd.Transaction = tran;
updateCmd.CommandText = "UPDATE \"Notifications\" SET \"NotificationTemplateId\" = ? WHERE \"Implementation\" = 'Email' and \"NotificationTemplateId\" = 0";
updateCmd.AddParameter(id);
updateCmd.ExecuteNonQuery();
}
}
}
}
}

View file

@ -28,6 +28,7 @@ using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Notifications.NotificationTemplates;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
@ -161,6 +162,7 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<DownloadClientStatus>("DownloadClientStatus").RegisterModel();
Mapper.Entity<ImportListStatus>("ImportListStatus").RegisterModel();
Mapper.Entity<NotificationStatus>("NotificationStatus").RegisterModel();
Mapper.Entity<NotificationTemplate>("NotificationTemplates").RegisterModel();
Mapper.Entity<CustomFilter>("CustomFilters").RegisterModel();

View file

@ -44,6 +44,7 @@
"AddNewSeriesSearchForCutoffUnmetEpisodes": "Start search for cutoff unmet episodes",
"AddNewSeriesSearchForMissingEpisodes": "Start search for missing episodes",
"AddNotificationError": "Unable to add a new notification, please try again.",
"AddNotificationTemplate": "Add Notification Template",
"AddQualityProfile": "Add Quality Profile",
"AddQualityProfileError": "Unable to add a new quality profile, please try again.",
"AddReleaseProfile": "Add Release Profile",
@ -166,6 +167,7 @@
"BlocklistRelease": "Blocklist Release",
"BlocklistReleaseHelpText": "Blocks this release from being redownloaded by {appName} via RSS or Automatic Search",
"BlocklistReleases": "Blocklist Releases",
"Body": "Body",
"Branch": "Branch",
"BranchUpdate": "Branch to use to update {appName}",
"BranchUpdateMechanism": "Branch used by external update mechanism",
@ -361,6 +363,8 @@
"DeleteIndexerMessageText": "Are you sure you want to delete the indexer '{name}'?",
"DeleteNotification": "Delete Notification",
"DeleteNotificationMessageText": "Are you sure you want to delete the notification '{name}'?",
"DeleteNotificationTemplate": "Delete Notification Template",
"DeleteNotificationTemplateMessageText": "Are you sure you want to delete the notification template '{name}'?",
"DeleteQualityProfile": "Delete Quality Profile",
"DeleteQualityProfileMessageText": "Are you sure you want to delete the quality profile '{name}'?",
"DeleteReleaseProfile": "Delete Release Profile",
@ -595,6 +599,7 @@
"EditIndexerImplementation": "Edit Indexer - {implementationName}",
"EditListExclusion": "Edit List Exclusion",
"EditMetadata": "Edit {metadataType} Metadata",
"EditNotificationTemplate": "Edit Notification Template",
"EditQualityProfile": "Edit Quality Profile",
"EditReleaseProfile": "Edit Release Profile",
"EditRemotePathMapping": "Edit Remote Path Mapping",
@ -1501,6 +1506,12 @@
"NotificationsValidationUnableToConnectToService": "Unable to connect to {serviceName}",
"NotificationsValidationUnableToSendTestMessage": "Unable to send test message: {exceptionMessage}",
"NotificationsValidationUnableToSendTestMessageApiResponse": "Unable to send test message. Response from API: {error}",
"NotificationTemplateBodyHelpText": "The notification body supports template placeholders",
"NotificationNotificationTemplateHelpText": "Use text from selected template for notification",
"NotificationTemplate": "Notification Template",
"NotificationTemplates": "Notification Templates",
"NotificationTemplatesLoadError": "Unable to load Notification Templates",
"NotificationTemplateTitleHelpText": "The notification title supports template placeholders",
"NzbgetHistoryItemMessage": "PAR Status: {parStatus} - Unpack Status: {unpackStatus} - Move Status: {moveStatus} - Script Status: {scriptStatus} - Delete Status: {deleteStatus} - Mark Status: {markStatus}",
"Ok": "Ok",
"OnApplicationUpdate": "On Application Update",
@ -1965,6 +1976,7 @@
"Status": "Status",
"StopSelecting": "Stop Selecting",
"Style": "Style",
"Subject": "Subject",
"SubtitleLanguages": "Subtitle Languages",
"Sunday": "Sunday",
"SupportedAutoTaggingProperties": "{appName} supports the follow properties for auto tagging rules",

View file

@ -9,6 +9,7 @@ using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Notifications.NotificationTemplates;
namespace NzbDrone.Core.Notifications.Email
{
@ -16,14 +17,16 @@ namespace NzbDrone.Core.Notifications.Email
{
private readonly ICertificateValidationService _certificateValidationService;
private readonly ILocalizationService _localizationService;
private readonly INotificationTemplateService _notificationTemplateService;
private readonly Logger _logger;
public override string Name => _localizationService.GetLocalizedString("NotificationsEmailSettingsName");
public Email(ICertificateValidationService certificateValidationService, ILocalizationService localizationService, Logger logger)
public Email(ICertificateValidationService certificateValidationService, ILocalizationService localizationService, INotificationTemplateService notificationTemplateService, Logger logger)
{
_certificateValidationService = certificateValidationService;
_localizationService = localizationService;
_notificationTemplateService = notificationTemplateService;
_logger = logger;
}
@ -33,66 +36,98 @@ namespace NzbDrone.Core.Notifications.Email
{
var body = $"{grabMessage.Message} sent to queue.";
SendEmail(Settings, EPISODE_GRABBED_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, grabMessage, EPISODE_GRABBED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnDownload(DownloadMessage message)
{
var body = $"{message.Message} Downloaded and sorted.";
SendEmail(Settings, EPISODE_DOWNLOADED_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, message, EPISODE_DOWNLOADED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnImportComplete(ImportCompleteMessage message)
{
var body = $"All expected episode files in {message.Message} downloaded and sorted.";
SendEmail(Settings, IMPORT_COMPLETE_TITLE, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, message, IMPORT_COMPLETE_TITLE, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnEpisodeFileDelete(EpisodeDeleteMessage deleteMessage)
{
var body = $"{deleteMessage.Message} deleted.";
SendEmail(Settings, EPISODE_DELETED_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, deleteMessage, EPISODE_DELETED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnSeriesAdd(SeriesAddMessage message)
{
var body = $"{message.Message}";
SendEmail(Settings, SERIES_ADDED_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, message, SERIES_ADDED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnSeriesDelete(SeriesDeleteMessage deleteMessage)
{
var body = $"{deleteMessage.Message}";
var body = $"{deleteMessage.Message}.";
SendEmail(Settings, SERIES_DELETED_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, deleteMessage, SERIES_DELETED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnHealthIssue(HealthCheck.HealthCheck message)
{
SendEmail(Settings, HEALTH_ISSUE_TITLE_BRANDED, message.Message);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, message, HEALTH_ISSUE_TITLE_BRANDED, message.Message);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnHealthRestored(HealthCheck.HealthCheck previousMessage)
{
SendEmail(Settings, HEALTH_RESTORED_TITLE_BRANDED, $"The following issue is now resolved: {previousMessage.Message}");
var body = $"The following issue is now resolved: {previousMessage.Message}";
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, previousMessage, HEALTH_RESTORED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
{
var body = $"{updateMessage.Message}";
SendEmail(Settings, APPLICATION_UPDATE_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, updateMessage, APPLICATION_UPDATE_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override void OnManualInteractionRequired(ManualInteractionRequiredMessage message)
{
var body = $"{message.Message} requires manual interaction.";
SendEmail(Settings, MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED, body);
var notificationTemplateId = ((NotificationDefinition)this.Definition).NotificationTemplateId;
var processedNotificationTemplate = _notificationTemplateService.processNotificationTemplate(notificationTemplateId, message, MANUAL_INTERACTION_REQUIRED_TITLE_BRANDED, body);
SendEmail(Settings, processedNotificationTemplate.Title, processedNotificationTemplate.Body, true);
}
public override ValidationResult Test()

View file

@ -22,6 +22,7 @@ namespace NzbDrone.Core.Notifications
public bool OnHealthRestored { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool OnManualInteractionRequired { get; set; }
public int NotificationTemplateId { get; set; }
[MemberwiseEqualityIgnore]
public bool SupportsOnGrab { get; set; }

View file

@ -7,6 +7,7 @@ namespace NzbDrone.Core.Notifications
public interface INotificationRepository : IProviderRepository<NotificationDefinition>
{
void UpdateSettings(NotificationDefinition model);
void removeNotificationTemplate(int notificationTemplateId);
}
public class NotificationRepository : ProviderRepository<NotificationDefinition>, INotificationRepository
@ -20,5 +21,19 @@ namespace NzbDrone.Core.Notifications
{
SetFields(model, m => m.Settings);
}
public void removeNotificationTemplate(int notificationTemplateId)
{
var models = All();
foreach (var model in models)
{
if (model.NotificationTemplateId == notificationTemplateId)
{
model.NotificationTemplateId = 0;
Update(model);
}
}
}
}
}

View file

@ -0,0 +1,109 @@
using System;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Notifications.NotificationTemplates
{
public class NotificationTemplate : ModelBase, IEquatable<NotificationTemplate>
{
public NotificationTemplate()
{
}
public NotificationTemplate(
string name,
string title,
string body,
bool onGrab,
bool onDownload,
bool onUpgrade,
bool onImportComplete,
bool onRename,
bool onSeriesAdd,
bool onSeriesDelete,
bool onEpisodeFileDelete,
bool onEpisodeFileDeleteForUpgrade,
bool onHealthIssue,
bool onHealthRestored,
bool onApplicationUpdate,
bool onManualInteractionRequired)
{
Name = name;
Title = title;
Body = body;
OnGrab = onGrab;
OnDownload = onDownload;
OnUpgrade = onUpgrade;
OnImportComplete = onImportComplete;
OnRename = onRename;
OnSeriesAdd = onSeriesAdd;
OnSeriesDelete = onSeriesDelete;
OnEpisodeFileDelete = onEpisodeFileDelete;
OnEpisodeFileDeleteForUpgrade = onEpisodeFileDeleteForUpgrade;
OnHealthIssue = onHealthIssue;
OnHealthRestored = onHealthRestored;
OnApplicationUpdate = onApplicationUpdate;
OnManualInteractionRequired = onManualInteractionRequired;
}
public string Name { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public bool OnGrab { get; set; }
public bool OnDownload { get; set; }
public bool OnUpgrade { get; set; }
public bool OnImportComplete { get; set; }
public bool OnRename { get; set; }
public bool OnSeriesAdd { get; set; }
public bool OnSeriesDelete { get; set; }
public bool OnEpisodeFileDelete { get; set; }
public bool OnEpisodeFileDeleteForUpgrade { get; set; }
public bool OnHealthIssue { get; set; }
public bool OnHealthRestored { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool OnManualInteractionRequired { get; set; }
public bool Equals(NotificationTemplate other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return Equals(Id, other.Id);
}
public override bool Equals(object obj)
{
if (obj is null)
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
if (obj.GetType() != GetType())
{
return false;
}
return Equals((NotificationTemplate)obj);
}
public override string ToString()
{
return Name;
}
public override int GetHashCode()
{
return Id;
}
}
}

View file

@ -0,0 +1,21 @@
namespace NzbDrone.Core.Notifications.NotificationTemplates
{
public class NotificationTemplateParameters
{
public NotificationTemplateParameters()
{
}
public string FallbackTitle { get; set; }
public string FallbackBody { get; set; }
public GrabMessage GrabMessage { get; set; }
public SeriesAddMessage SeriesAddMessage { get; set; }
public EpisodeDeleteMessage EpisodeDeleteMessage { get; set; }
public SeriesDeleteMessage SeriesDeleteMessage { get; set; }
public ImportCompleteMessage ImportCompleteMessage { get; set; }
public DownloadMessage DownloadMessage { get; set; }
public HealthCheck.HealthCheck HealthCheck { get; set; }
public ApplicationUpdateMessage ApplicationUpdateMessage { get; set; }
public ManualInteractionRequiredMessage ManualInteractionRequiredMessage { get; set; }
}
}

View file

@ -0,0 +1,17 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Notifications.NotificationTemplates
{
public interface INotificationTemplateRepository : IBasicRepository<NotificationTemplate>
{
}
public class NotificationTemplateRepository : BasicRepository<NotificationTemplate>, INotificationTemplateRepository
{
public NotificationTemplateRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View file

@ -0,0 +1,236 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Messaging.Events;
using Scriban;
namespace NzbDrone.Core.Notifications.NotificationTemplates
{
public interface INotificationTemplateService
{
void Update(NotificationTemplate notificationTemplate);
NotificationTemplate Insert(NotificationTemplate notificationTemplate);
List<NotificationTemplate> All();
NotificationTemplate GetById(int id);
void Delete(int id);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, GrabMessage grabMessage, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, SeriesAddMessage message, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, EpisodeDeleteMessage deleteMessage, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, SeriesDeleteMessage deleteMessage, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, ImportCompleteMessage message, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, DownloadMessage message, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, HealthCheck.HealthCheck message, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, ApplicationUpdateMessage updateMessage, string fallbackTitle, string fallbackBody);
NotificationTemplate processNotificationTemplate(int notificationTemplateId, ManualInteractionRequiredMessage message, string fallbackTitle, string fallbackBody);
}
public class NotificationTemplateService : INotificationTemplateService
{
private readonly INotificationTemplateRepository _templateRepository;
private readonly IEventAggregator _eventAggregator;
private readonly ICached<Dictionary<int, NotificationTemplate>> _cache;
private readonly INotificationRepository _notificationRepository;
public NotificationTemplateService(INotificationTemplateRepository templateRepository,
ICacheManager cacheManager,
IEventAggregator eventAggregator,
INotificationRepository notificationRepository)
{
_templateRepository = templateRepository;
_eventAggregator = eventAggregator;
_cache = cacheManager.GetCache<Dictionary<int, NotificationTemplate>>(typeof(NotificationTemplate), "templates");
_notificationRepository = notificationRepository;
}
private Dictionary<int, NotificationTemplate> AllDictionary()
{
return _cache.Get("all", () => _templateRepository.All().ToDictionary(m => m.Id));
}
public List<NotificationTemplate> All()
{
return AllDictionary().Values.ToList();
}
public NotificationTemplate GetById(int id)
{
return AllDictionary()[id];
}
public void Update(NotificationTemplate notificationTemplate)
{
_templateRepository.Update(notificationTemplate);
_cache.Clear();
}
public void Update(List<NotificationTemplate> notificationTemplate)
{
_templateRepository.UpdateMany(notificationTemplate);
_cache.Clear();
}
public NotificationTemplate Insert(NotificationTemplate notificationTemplate)
{
var result = _templateRepository.Insert(notificationTemplate);
_cache.Clear();
return result;
}
public void Delete(int id)
{
_notificationRepository.removeNotificationTemplate(id);
_templateRepository.Delete(id);
_cache.Clear();
}
public void Delete(List<int> ids)
{
foreach (var id in ids)
{
_notificationRepository.removeNotificationTemplate(id);
_templateRepository.Delete(id);
}
_cache.Clear();
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, GrabMessage grabMessage, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
GrabMessage = grabMessage
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, SeriesAddMessage message, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
SeriesAddMessage = message
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, EpisodeDeleteMessage deleteMessage, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
EpisodeDeleteMessage = deleteMessage
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, SeriesDeleteMessage deleteMessage, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
SeriesDeleteMessage = deleteMessage
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, ImportCompleteMessage message, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
ImportCompleteMessage = message
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, DownloadMessage message, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
DownloadMessage = message
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, HealthCheck.HealthCheck message, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
HealthCheck = message
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, ApplicationUpdateMessage updateMessage, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
ApplicationUpdateMessage = updateMessage
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
NotificationTemplate INotificationTemplateService.processNotificationTemplate(int notificationTemplateId, ManualInteractionRequiredMessage message, string fallbackTitle, string fallbackBody)
{
var templateParams = new NotificationTemplateParameters
{
FallbackTitle = fallbackTitle,
FallbackBody = fallbackBody,
ManualInteractionRequiredMessage = message
};
return this.ProcessNotificationTemplate(notificationTemplateId, templateParams);
}
private NotificationTemplate ProcessNotificationTemplate(int notificationTemplateId, NotificationTemplateParameters templateParams)
{
var processedNotificationTemplate = new NotificationTemplate();
processedNotificationTemplate.Title = templateParams.FallbackTitle;
processedNotificationTemplate.Body = templateParams.FallbackBody;
if (notificationTemplateId > 0)
{
var notificationTemplate = _templateRepository.Find(notificationTemplateId);
if (notificationTemplate != null && (
(templateParams.GrabMessage != null && notificationTemplate.OnGrab)
|| (templateParams.SeriesAddMessage != null && notificationTemplate.OnSeriesAdd)
|| (templateParams.EpisodeDeleteMessage != null && notificationTemplate.OnEpisodeFileDelete)
|| (templateParams.SeriesDeleteMessage != null && notificationTemplate.OnSeriesDelete)
|| (templateParams.ImportCompleteMessage != null && notificationTemplate.OnImportComplete)
|| (templateParams.DownloadMessage != null && notificationTemplate.OnDownload)
|| (templateParams.HealthCheck != null && (notificationTemplate.OnHealthIssue || notificationTemplate.OnHealthRestored))
|| (templateParams.ApplicationUpdateMessage != null && notificationTemplate.OnApplicationUpdate)
|| (templateParams.ManualInteractionRequiredMessage != null && notificationTemplate.OnManualInteractionRequired)))
{
if (!string.IsNullOrEmpty(notificationTemplate.Title))
{
var tpl = Template.Parse(notificationTemplate.Title);
processedNotificationTemplate.Title = tpl.Render(templateParams);
}
if (!string.IsNullOrEmpty(notificationTemplate.Body))
{
var tpl = Template.Parse(notificationTemplate.Body);
processedNotificationTemplate.Body = tpl.Render(templateParams);
}
return processedNotificationTemplate;
}
}
return processedNotificationTemplate;
}
}
}

View file

@ -27,6 +27,7 @@
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />
<PackageReference Include="Npgsql" Version="7.0.9" />
<PackageReference Include="Scriban" Version="5.12.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Sonarr.Common.csproj" />

View file

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Linq;
using FluentValidation;
using FluentValidation.Results;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Notifications.NotificationTemplates;
using NzbDrone.Core.Validation;
using Sonarr.Http;
using Sonarr.Http.REST;
using Sonarr.Http.REST.Attributes;
namespace Sonarr.Api.V3.NotificationTemplates
{
[V3ApiController]
public class NotificationTemplateController : RestController<NotificationTemplateResource>
{
private readonly INotificationTemplateService _templateService;
public NotificationTemplateController(INotificationTemplateService templateService)
{
_templateService = templateService;
SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Name)
.Must((v, c) => !_templateService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
}
protected override NotificationTemplateResource GetResourceById(int id)
{
return _templateService.GetById(id).ToResource();
}
[HttpGet]
[Produces("application/json")]
public List<NotificationTemplateResource> GetAll()
{
return _templateService.All().ToResource();
}
[RestPostById]
[Consumes("application/json")]
public ActionResult<NotificationTemplateResource> Create([FromBody] NotificationTemplateResource notificationTemplateResource)
{
var model = notificationTemplateResource.ToModel();
Validate(model);
return Created(_templateService.Insert(model).Id);
}
[RestPutById]
[Consumes("application/json")]
public ActionResult<NotificationTemplateResource> Update([FromBody] NotificationTemplateResource resource)
{
var model = resource.ToModel();
Validate(model);
_templateService.Update(model);
return Accepted(model.Id);
}
[RestDeleteById]
public void DeleteFormat(int id)
{
_templateService.Delete(id);
}
private void Validate(NotificationTemplate notificationTemplate)
{
// TODO
}
private void VerifyValidationResult(ValidationResult validationResult)
{
var result = new NzbDroneValidationResult(validationResult.Errors);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
}
}
}

View file

@ -0,0 +1,89 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using NzbDrone.Core.Notifications.NotificationTemplates;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.NotificationTemplates
{
public class NotificationTemplateResource : RestResource
{
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public override int Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public bool OnGrab { get; set; }
public bool OnDownload { get; set; }
public bool OnUpgrade { get; set; }
public bool OnImportComplete { get; set; }
public bool OnRename { get; set; }
public bool OnSeriesAdd { get; set; }
public bool OnSeriesDelete { get; set; }
public bool OnEpisodeFileDelete { get; set; }
public bool OnEpisodeFileDeleteForUpgrade { get; set; }
public bool OnHealthIssue { get; set; }
public bool IncludeHealthWarnings { get; set; }
public bool OnHealthRestored { get; set; }
public bool OnApplicationUpdate { get; set; }
public bool OnManualInteractionRequired { get; set; }
}
public static class NotificationTemplateResourceMapper
{
public static NotificationTemplateResource ToResource(this NotificationTemplate model)
{
var resource = new NotificationTemplateResource
{
Id = model.Id,
Name = model.Name,
Title = model.Title,
Body = model.Body,
OnGrab = model.OnGrab,
OnDownload = model.OnDownload,
OnUpgrade = model.OnUpgrade,
OnImportComplete = model.OnImportComplete,
OnRename = model.OnRename,
OnSeriesAdd = model.OnSeriesAdd,
OnSeriesDelete = model.OnSeriesDelete,
OnEpisodeFileDelete = model.OnEpisodeFileDelete,
OnEpisodeFileDeleteForUpgrade = model.OnEpisodeFileDeleteForUpgrade,
OnHealthIssue = model.OnHealthIssue,
OnHealthRestored = model.OnHealthRestored,
OnApplicationUpdate = model.OnApplicationUpdate,
OnManualInteractionRequired = model.OnManualInteractionRequired
};
return resource;
}
public static List<NotificationTemplateResource> ToResource(this IEnumerable<NotificationTemplate> models)
{
return models.Select(m => m.ToResource()).ToList();
}
public static NotificationTemplate ToModel(this NotificationTemplateResource resource)
{
return new NotificationTemplate
{
Id = resource.Id,
Name = resource.Name,
Title = resource.Title,
Body = resource.Body,
OnGrab = resource.OnGrab,
OnDownload = resource.OnDownload,
OnUpgrade = resource.OnUpgrade,
OnImportComplete = resource.OnImportComplete,
OnRename = resource.OnRename,
OnSeriesAdd = resource.OnSeriesAdd,
OnSeriesDelete = resource.OnSeriesDelete,
OnEpisodeFileDelete = resource.OnEpisodeFileDelete,
OnEpisodeFileDeleteForUpgrade = resource.OnEpisodeFileDeleteForUpgrade,
OnHealthIssue = resource.OnHealthIssue,
OnHealthRestored = resource.OnHealthRestored,
OnApplicationUpdate = resource.OnApplicationUpdate,
OnManualInteractionRequired = resource.OnManualInteractionRequired
};
}
}
}

View file

@ -32,6 +32,7 @@ namespace Sonarr.Api.V3.Notifications
public bool SupportsOnHealthRestored { get; set; }
public bool SupportsOnApplicationUpdate { get; set; }
public bool SupportsOnManualInteractionRequired { get; set; }
public int NotificationTemplateId { get; set; }
public string TestCommand { get; set; }
}
@ -73,6 +74,7 @@ namespace Sonarr.Api.V3.Notifications
resource.SupportsOnHealthRestored = definition.SupportsOnHealthRestored;
resource.SupportsOnApplicationUpdate = definition.SupportsOnApplicationUpdate;
resource.SupportsOnManualInteractionRequired = definition.SupportsOnManualInteractionRequired;
resource.NotificationTemplateId = definition.NotificationTemplateId;
return resource;
}
@ -113,6 +115,7 @@ namespace Sonarr.Api.V3.Notifications
definition.SupportsOnHealthRestored = resource.SupportsOnHealthRestored;
definition.SupportsOnApplicationUpdate = resource.SupportsOnApplicationUpdate;
definition.SupportsOnManualInteractionRequired = resource.SupportsOnManualInteractionRequired;
definition.NotificationTemplateId = resource.NotificationTemplateId;
return definition;
}