mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-24 14:27:06 -04:00
resolve merge conflict
This commit is contained in:
parent
1c30ecd66d
commit
06baead15f
34 changed files with 1679 additions and 13 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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%;
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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%;
|
||||
}
|
10
frontend/src/Settings/Notifications/NotificationTemplates/NotificationTemplateItem.css.d.ts
vendored
Normal file
10
frontend/src/Settings/Notifications/NotificationTemplates/NotificationTemplateItem.css.d.ts
vendored
Normal 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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
9
frontend/src/Settings/Notifications/NotificationTemplates/NotificationTemplates.css.d.ts
vendored
Normal file
9
frontend/src/Settings/Notifications/NotificationTemplates/NotificationTemplates.css.d.ts
vendored
Normal 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;
|
|
@ -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;
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
71
frontend/src/Store/Actions/Settings/notificationTemplates.js
Normal file
71
frontend/src/Store/Actions/Settings/notificationTemplates.js
Normal 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)
|
||||
}
|
||||
|
||||
};
|
|
@ -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,
|
||||
|
|
22
frontend/src/typings/Settings/NotificationTemplate.ts
Normal file
22
frontend/src/typings/Settings/NotificationTemplate.ts
Normal 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;
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue