mirror of https://github.com/immich-app/immich.git
feat: Notification Email Templates (#13940)
parent
4bf1b84cc2
commit
292182fa7f
Binary file not shown.
|
After Width: | Height: | Size: 195 KiB |
@ -0,0 +1,115 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SystemConfigTemplateEmailsDto {
|
||||
/// Returns a new [SystemConfigTemplateEmailsDto] instance.
|
||||
SystemConfigTemplateEmailsDto({
|
||||
required this.albumInviteTemplate,
|
||||
required this.albumUpdateTemplate,
|
||||
required this.welcomeTemplate,
|
||||
});
|
||||
|
||||
String albumInviteTemplate;
|
||||
|
||||
String albumUpdateTemplate;
|
||||
|
||||
String welcomeTemplate;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigTemplateEmailsDto &&
|
||||
other.albumInviteTemplate == albumInviteTemplate &&
|
||||
other.albumUpdateTemplate == albumUpdateTemplate &&
|
||||
other.welcomeTemplate == welcomeTemplate;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumInviteTemplate.hashCode) +
|
||||
(albumUpdateTemplate.hashCode) +
|
||||
(welcomeTemplate.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigTemplateEmailsDto[albumInviteTemplate=$albumInviteTemplate, albumUpdateTemplate=$albumUpdateTemplate, welcomeTemplate=$welcomeTemplate]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'albumInviteTemplate'] = this.albumInviteTemplate;
|
||||
json[r'albumUpdateTemplate'] = this.albumUpdateTemplate;
|
||||
json[r'welcomeTemplate'] = this.welcomeTemplate;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SystemConfigTemplateEmailsDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SystemConfigTemplateEmailsDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SystemConfigTemplateEmailsDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigTemplateEmailsDto(
|
||||
albumInviteTemplate: mapValueOfType<String>(json, r'albumInviteTemplate')!,
|
||||
albumUpdateTemplate: mapValueOfType<String>(json, r'albumUpdateTemplate')!,
|
||||
welcomeTemplate: mapValueOfType<String>(json, r'welcomeTemplate')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SystemConfigTemplateEmailsDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SystemConfigTemplateEmailsDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SystemConfigTemplateEmailsDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SystemConfigTemplateEmailsDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SystemConfigTemplateEmailsDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SystemConfigTemplateEmailsDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SystemConfigTemplateEmailsDto-objects as value to a dart map
|
||||
static Map<String, List<SystemConfigTemplateEmailsDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SystemConfigTemplateEmailsDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SystemConfigTemplateEmailsDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'albumInviteTemplate',
|
||||
'albumUpdateTemplate',
|
||||
'welcomeTemplate',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SystemConfigTemplatesDto {
|
||||
/// Returns a new [SystemConfigTemplatesDto] instance.
|
||||
SystemConfigTemplatesDto({
|
||||
required this.email,
|
||||
});
|
||||
|
||||
SystemConfigTemplateEmailsDto email;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SystemConfigTemplatesDto &&
|
||||
other.email == email;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(email.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SystemConfigTemplatesDto[email=$email]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'email'] = this.email;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SystemConfigTemplatesDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SystemConfigTemplatesDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SystemConfigTemplatesDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SystemConfigTemplatesDto(
|
||||
email: SystemConfigTemplateEmailsDto.fromJson(json[r'email'])!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SystemConfigTemplatesDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SystemConfigTemplatesDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SystemConfigTemplatesDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SystemConfigTemplatesDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SystemConfigTemplatesDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SystemConfigTemplatesDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SystemConfigTemplatesDto-objects as value to a dart map
|
||||
static Map<String, List<SystemConfigTemplatesDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SystemConfigTemplatesDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SystemConfigTemplatesDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'email',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class TemplateDto {
|
||||
/// Returns a new [TemplateDto] instance.
|
||||
TemplateDto({
|
||||
required this.template,
|
||||
});
|
||||
|
||||
String template;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is TemplateDto &&
|
||||
other.template == template;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(template.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'TemplateDto[template=$template]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'template'] = this.template;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [TemplateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static TemplateDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "TemplateDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return TemplateDto(
|
||||
template: mapValueOfType<String>(json, r'template')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<TemplateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <TemplateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = TemplateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, TemplateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, TemplateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = TemplateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of TemplateDto-objects as value to a dart map
|
||||
static Map<String, List<TemplateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<TemplateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = TemplateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'template',
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class TemplateResponseDto {
|
||||
/// Returns a new [TemplateResponseDto] instance.
|
||||
TemplateResponseDto({
|
||||
required this.html,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
String html;
|
||||
|
||||
String name;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is TemplateResponseDto &&
|
||||
other.html == html &&
|
||||
other.name == name;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(html.hashCode) +
|
||||
(name.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'TemplateResponseDto[html=$html, name=$name]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'html'] = this.html;
|
||||
json[r'name'] = this.name;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [TemplateResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static TemplateResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "TemplateResponseDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return TemplateResponseDto(
|
||||
html: mapValueOfType<String>(json, r'html')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<TemplateResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <TemplateResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = TemplateResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, TemplateResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, TemplateResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = TemplateResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of TemplateResponseDto-objects as value to a dart map
|
||||
static Map<String, List<TemplateResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<TemplateResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = TemplateResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'html',
|
||||
'name',
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,3 +1,13 @@
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class TestEmailResponseDto {
|
||||
messageId!: string;
|
||||
}
|
||||
export class TemplateResponseDto {
|
||||
name!: string;
|
||||
html!: string;
|
||||
}
|
||||
export class TemplateDto {
|
||||
@IsString()
|
||||
template!: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export const replaceTemplateTags = (template: string, variables: Record<string, string | undefined>) => {
|
||||
return template.replaceAll(/{(.*?)}/g, (_, key) => {
|
||||
return variables[key] || `{${key}}`;
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplate } from '@immich/sdk';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import FormatMessage from '$lib/components/i18n/format-message.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiEyeOutline } from '@mdi/js';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
|
||||
|
||||
interface Props {
|
||||
savedConfig: SystemConfigDto;
|
||||
defaultConfig: SystemConfigDto;
|
||||
config: SystemConfigDto;
|
||||
disabled?: boolean;
|
||||
onReset: SettingsResetEvent;
|
||||
onSave: SettingsSaveEvent;
|
||||
}
|
||||
|
||||
let { savedConfig, config = $bindable() }: Props = $props();
|
||||
|
||||
let htmlPreview = $state('');
|
||||
let loadingPreview = $state(false);
|
||||
|
||||
const getTemplate = async (name: string, template: string) => {
|
||||
try {
|
||||
loadingPreview = true;
|
||||
const result = await getNotificationTemplate({ name, templateDto: { template } });
|
||||
htmlPreview = result.html;
|
||||
} catch (error) {
|
||||
handleError(error, 'Could not load template.');
|
||||
} finally {
|
||||
loadingPreview = false;
|
||||
}
|
||||
};
|
||||
|
||||
const closePreviewModal = () => {
|
||||
htmlPreview = '';
|
||||
};
|
||||
|
||||
const templateConfigs = [
|
||||
{
|
||||
label: $t('admin.template_email_welcome'),
|
||||
templateKey: 'welcomeTemplate' as const,
|
||||
descriptionTags: '{username}, {password}, {displayName}, {baseUrl}',
|
||||
templateName: 'welcome',
|
||||
},
|
||||
{
|
||||
label: $t('admin.template_email_invite_album'),
|
||||
templateKey: 'albumInviteTemplate' as const,
|
||||
descriptionTags: '{senderName}, {recipientName}, {albumId}, {albumName}, {baseUrl}',
|
||||
templateName: 'album-invite',
|
||||
},
|
||||
{
|
||||
label: $t('admin.template_email_update_album'),
|
||||
templateKey: 'albumUpdateTemplate' as const,
|
||||
descriptionTags: '{recipientName}, {albumId}, {albumName}, {baseUrl}',
|
||||
templateName: 'album-update',
|
||||
},
|
||||
];
|
||||
|
||||
const isEdited = (templateKey: keyof SystemConfigTemplateEmailsDto) =>
|
||||
config.templates.email[templateKey] !== savedConfig.templates.email[templateKey];
|
||||
|
||||
const onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" {onsubmit} class="mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion
|
||||
key="templates"
|
||||
title={$t('admin.template_email_settings')}
|
||||
subtitle={$t('admin.template_settings_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<FormatMessage key="admin.template_email_if_empty">
|
||||
{$t('admin.template_email_if_empty')}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
<hr />
|
||||
{#if loadingPreview}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
|
||||
{#each templateConfigs as { label, templateKey, descriptionTags, templateName }}
|
||||
<SettingTextarea
|
||||
{label}
|
||||
description={$t('admin.template_email_available_tags', { values: { tags: descriptionTags } })}
|
||||
bind:value={config.templates.email[templateKey]}
|
||||
isEdited={isEdited(templateKey)}
|
||||
disabled={!config.notifications.smtp.enabled}
|
||||
/>
|
||||
<div class="flex justify-between">
|
||||
<Button
|
||||
size="sm"
|
||||
onclick={() => getTemplate(templateName, config.templates.email[templateKey])}
|
||||
title={$t('admin.template_email_preview')}
|
||||
>
|
||||
<Icon path={mdiEyeOutline} class="mr-1" />
|
||||
{$t('admin.template_email_preview')}
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
</div>
|
||||
|
||||
{#if htmlPreview}
|
||||
<FullScreenModal title={$t('admin.template_email_preview')} onClose={closePreviewModal} width="wide">
|
||||
<div style="position:relative; width:100%; height:90vh; overflow: hidden">
|
||||
<iframe
|
||||
title={$t('admin.template_email_preview')}
|
||||
srcdoc={htmlPreview}
|
||||
style="width: 100%; height: 100%; border: none; overflow:hidden; position: absolute; top: 0; left: 0;"
|
||||
></iframe>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue