diff --git a/zero.Core/Api/MailTemplatesApi.cs b/zero.Core/Api/MailTemplatesApi.cs new file mode 100644 index 00000000..c6dcdc5b --- /dev/null +++ b/zero.Core/Api/MailTemplatesApi.cs @@ -0,0 +1,110 @@ +using FluentValidation; +using Raven.Client.Documents; +using Raven.Client.Documents.Session; +using System.Collections.Generic; +using System.Threading.Tasks; +using zero.Core.Api; +using zero.Core.Entities; +using zero.Core.Extensions; + +namespace zero.Core.Api +{ + public class MailTemplatesApi : BackofficeApi, IMailTemplatesApi + { + protected IValidator Validator { get; private set; } + + public MailTemplatesApi(IBackofficeStore store, IValidator validator) : base(store) + { + Validator = validator; + } + + + + /// + public async Task GetById(string id) + { + return await GetById(id); + } + + + /// + public async Task> GetByIds(params string[] ids) + { + return await GetByIds(ids); + } + + + /// + /// Get all available currencies + /// + public async Task> GetAll() + { + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + return await session.Query().Scope(Scope).ToListAsync(); + } + } + + + /// + public async Task> GetByQuery(ListQuery query) + { + query.SearchSelector = entity => entity.Name; + + using (IAsyncDocumentSession session = Raven.OpenAsyncSession()) + { + return await session.Query() + .Scope(Scope) + .ToQueriedListAsync(query); + } + } + + + /// + public async Task> Save(IMailTemplate model) + { + return await SaveModel(model, Validator); + } + + + /// + public async Task> Delete(string id) + { + return await DeleteById(id); + } + } + + + public interface IMailTemplatesApi + { + /// + /// Get mail template by id + /// + Task GetById(string id); + + /// + /// Get mail templates by ids + /// + Task> GetByIds(params string[] ids); + + /// + /// Get all available mail templates + /// + Task> GetAll(); + + /// + /// Get all available mail templates (with query) + /// + Task> GetByQuery(ListQuery query); + + /// + /// Creates or updates a mail template + /// + Task> Save(IMailTemplate model); + + /// + /// Deletes a mail template by id + /// + Task> Delete(string id); + } +} diff --git a/zero.Core/Constants.cs b/zero.Core/Constants.cs index 7207c4d6..1194a41b 100644 --- a/zero.Core/Constants.cs +++ b/zero.Core/Constants.cs @@ -57,6 +57,7 @@ public const string Languages = "languages"; public const string Translations = "translations"; public const string Countries = "countries"; + public const string Mails = "mailTemplates"; public const string Logging = "logs"; public const string Plugins = "plugins"; public const string CreatePlugin = "createplugin"; diff --git a/zero.Core/Entities/MailTemplate.cs b/zero.Core/Entities/MailTemplate.cs new file mode 100644 index 00000000..81acb632 --- /dev/null +++ b/zero.Core/Entities/MailTemplate.cs @@ -0,0 +1,77 @@ +using zero.Core.Attributes; +using zero.Core.Entities; + +namespace zero.Core.Entities +{ + public class MailTemplate : ZeroEntity, IMailTemplate + { + /// + public string Key { get; set; } + + /// + public string SenderEmail { get; set; } + + /// + public string SenderName { get; set; } + + /// + public string RecipientEmail { get; set; } + + /// + public string Cc { get; set; } + + /// + public string Bcc { get; set; } + + /// + public string Subject { get; set; } + + /// + public string Body { get; set; } + } + + + [Collection("MailTemplates")] + public interface IMailTemplate : IZeroEntity, IAppAwareEntity, IZeroDbConventions + { + /// + /// Alias which is used to get the template in code + /// + string Key { get; set; } + + /// + /// Email address of the sender (overrides email from application) + /// + string SenderEmail { get; set; } + + /// + /// Name of the sender (overrides name from application) + /// + string SenderName { get; set; } + + /// + /// Email address of the recipient. This is only necessary for templates which do not have a dynamic recipient (e.g. reports). + /// + string RecipientEmail { get; set; } + + /// + /// Additional comma-separated emails to send a copy to + /// + string Cc { get; set; } + + /// + /// Additional comma-separated emails to send a hidden copy to + /// + string Bcc { get; set; } + + /// + /// Email subject (can contain placeholders) + /// + string Subject { get; set; } + + /// + /// Email body (can contain placeholders) + /// + string Body { get; set; } + } +} \ No newline at end of file diff --git a/zero.Core/Identity/Permissions/Permissions.cs b/zero.Core/Identity/Permissions/Permissions.cs index 9dc9c61b..02b0e5ff 100644 --- a/zero.Core/Identity/Permissions/Permissions.cs +++ b/zero.Core/Identity/Permissions/Permissions.cs @@ -18,6 +18,7 @@ public const string Languages = PREFIX + Constants.Settings.Languages; public const string Translations = PREFIX + Constants.Settings.Translations; public const string Countries = PREFIX + Constants.Settings.Countries; + public const string Mails = PREFIX + Constants.Settings.Mails; public const string Logging = PREFIX + Constants.Settings.Logging; public const string Plugins = PREFIX + Constants.Settings.Plugins; public const string CreatePlugin = PREFIX + Constants.Settings.CreatePlugin; diff --git a/zero.Core/Validation/MailTemplateValidator.cs b/zero.Core/Validation/MailTemplateValidator.cs new file mode 100644 index 00000000..a481e72e --- /dev/null +++ b/zero.Core/Validation/MailTemplateValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using zero.Core.Entities; + +namespace zero.Core.Validation +{ + public class MailTemplateValidator : ZeroValidator + { + public MailTemplateValidator() + { + + } + } +} diff --git a/zero.Web.UI/App/components/pickers/mailPicker/mailpicker.vue b/zero.Web.UI/App/components/pickers/mailPicker/mailpicker.vue new file mode 100644 index 00000000..33a9f9f6 --- /dev/null +++ b/zero.Web.UI/App/components/pickers/mailPicker/mailpicker.vue @@ -0,0 +1,62 @@ + + + + \ No newline at end of file diff --git a/zero.Web.UI/App/editor/fields/mailtemplatepicker.vue b/zero.Web.UI/App/editor/fields/mailtemplatepicker.vue new file mode 100644 index 00000000..573beb5b --- /dev/null +++ b/zero.Web.UI/App/editor/fields/mailtemplatepicker.vue @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/zero.Web.UI/App/pages/settings/mail.vue b/zero.Web.UI/App/pages/settings/mail.vue new file mode 100644 index 00000000..eae933ba --- /dev/null +++ b/zero.Web.UI/App/pages/settings/mail.vue @@ -0,0 +1,51 @@ + + + + \ No newline at end of file diff --git a/zero.Web.UI/App/pages/settings/mails.vue b/zero.Web.UI/App/pages/settings/mails.vue new file mode 100644 index 00000000..441527a3 --- /dev/null +++ b/zero.Web.UI/App/pages/settings/mails.vue @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/zero.Web.UI/App/pages/settings/routes.js b/zero.Web.UI/App/pages/settings/routes.js index 5425b851..77368007 100644 --- a/zero.Web.UI/App/pages/settings/routes.js +++ b/zero.Web.UI/App/pages/settings/routes.js @@ -9,6 +9,8 @@ import Language from './language.vue'; import Users from './users.vue'; import User from './user.vue'; import UserRole from './role.vue'; +import MailTemplates from './mails.vue'; +import MailTemplate from './mail.vue'; import Translations from './translations.vue'; const alias = __zero.alias.sections.settings; @@ -96,6 +98,8 @@ if (section) addArea(__zero.alias.settings.translations, Translations, Translations, true); + addArea(__zero.alias.settings.mails, MailTemplates, MailTemplate, true); + addArea(__zero.alias.settings.users, Users, User, true, area => { routes.push({ diff --git a/zero.Web.UI/App/renderers/editors/all.js b/zero.Web.UI/App/renderers/editors/all.js index 3e94a0ef..3bc2a568 100644 --- a/zero.Web.UI/App/renderers/editors/all.js +++ b/zero.Web.UI/App/renderers/editors/all.js @@ -6,6 +6,7 @@ import media from './media.js'; //import translation from './translation.js'; import user from './user.js'; import userRole from './userRole.js'; +import mailTemplate from './mailTemplate.js'; export default { application, @@ -14,5 +15,6 @@ export default { media, //translation, user, - userRole + userRole, + mailTemplate }; \ No newline at end of file diff --git a/zero.Web.UI/App/renderers/editors/mailTemplate.js b/zero.Web.UI/App/renderers/editors/mailTemplate.js new file mode 100644 index 00000000..0de05f6a --- /dev/null +++ b/zero.Web.UI/App/renderers/editors/mailTemplate.js @@ -0,0 +1,20 @@ + +import Editor from 'zero/core/editor.ts'; + +const editor = new Editor('mailTemplate', '@mailTemplate.fields.'); + +const general = editor.tab('general', '@ui.tab_general'); +const sender = editor.tab('sender', '@mailTemplate.tabs.sender'); +const recipient = editor.tab('recipient', '@mailTemplate.tabs.recipient'); + +general.field('name', { label: '@ui.name' }).text(60).required(); +general.field('key').text(60).required(); +general.field('subject').text(80).required(); +general.field('body').rte(); +sender.field('senderEmail').text(); +sender.field('senderName').text(); +recipient.field('recipientEmail').text(); +recipient.field('cc').text(); +recipient.field('bcc').text(); + +export default editor; \ No newline at end of file diff --git a/zero.Web.UI/App/renderers/lists/all.js b/zero.Web.UI/App/renderers/lists/all.js index 331bbe5d..c30cdbe8 100644 --- a/zero.Web.UI/App/renderers/lists/all.js +++ b/zero.Web.UI/App/renderers/lists/all.js @@ -3,10 +3,12 @@ import countries from './countries.js'; import languages from './languages.js'; import translations from './translations.js'; import users from './users.js'; +import mails from './mails.js'; export default { countries, languages, translations, - users + users, + mails }; \ No newline at end of file diff --git a/zero.Web.UI/App/renderers/lists/mails.js b/zero.Web.UI/App/renderers/lists/mails.js new file mode 100644 index 00000000..e5ec072a --- /dev/null +++ b/zero.Web.UI/App/renderers/lists/mails.js @@ -0,0 +1,16 @@ + +import List from 'zero/core/list.ts'; +import MailTemplatesApi from 'zero/resources/mailTemplates.js'; + +const list = new List('mailTemplates'); + +list.templateLabel = x => '@mailTemplate.fields.' + x; +list.link = zero.alias.settings.mails + '-edit'; + +list.onFetch(filter => MailTemplatesApi.getAll(filter)); + +list.column('name').name(); +list.column('subject').text(); +list.column('key').text(); + +export default list; \ No newline at end of file diff --git a/zero.Web.UI/App/resources/mailTemplates.js b/zero.Web.UI/App/resources/mailTemplates.js new file mode 100644 index 00000000..646339ae --- /dev/null +++ b/zero.Web.UI/App/resources/mailTemplates.js @@ -0,0 +1,41 @@ +import Axios from 'axios'; + +const base = 'mailTemplates/'; + +export default { + + getById(id) + { + return Axios.get(base + 'getById', { params: { id } }).then(res => Promise.resolve(res.data)); + }, + + getEmpty(id) + { + return Axios.get(base + 'getEmpty').then(res => Promise.resolve(res.data)); + }, + + getAll(query) + { + return Axios.get(base + 'getAll', query ? { params: { query } } : undefined).then(res => Promise.resolve(res.data)); + }, + + getPreviews(ids) + { + return Axios.get(base + 'getPreviews', { params: { ids } }).then(res => Promise.resolve(res.data)); + }, + + getForPicker() + { + return Axios.get(base + 'getForPicker').then(res => Promise.resolve(res.data)); + }, + + save(model) + { + return Axios.post(base + 'save', model).then(res => Promise.resolve(res.data)); + }, + + delete(id) + { + return Axios.delete(base + 'delete', { params: { id } }).then(res => Promise.resolve(res.data)); + } +}; \ No newline at end of file diff --git a/zero.Web.UI/app/components/all.js b/zero.Web.UI/app/components/all.js index be95aa84..1c43e549 100644 --- a/zero.Web.UI/app/components/all.js +++ b/zero.Web.UI/app/components/all.js @@ -36,6 +36,7 @@ import uiIconpicker from './pickers/iconPicker/iconpicker.vue'; import uiMediapicker from './pickers/mediaPicker/mediapicker.vue'; import uiPagepicker from './pickers/pagePicker/pagepicker.vue'; import uiUserpicker from './pickers/userPicker/userpicker.vue'; +import uiMailtemplatepicker from './pickers/mailPicker/mailpicker.vue'; import uiPick from './pickers/pick.vue'; import uiTable from './tables/table.vue'; @@ -96,6 +97,7 @@ export default { uiMediapicker, uiPagepicker, uiUserpicker, + uiMailtemplatepicker, uiPick, uiTable, diff --git a/zero.Web.UI/app/core/editor-field.ts b/zero.Web.UI/app/core/editor-field.ts index c594e7a6..dd3f8c82 100644 --- a/zero.Web.UI/app/core/editor-field.ts +++ b/zero.Web.UI/app/core/editor-field.ts @@ -11,6 +11,7 @@ import Output from '../editor/fields/output.vue'; import Checklist from '../editor/fields/checklist.vue'; import ColorPicker from '../editor/fields/colorpicker.vue'; import CountryPicker from '../editor/fields/countrypicker.vue'; +import MailTemplatePicker from '../editor/fields/mailtemplatepicker.vue'; import CulturePicker from '../editor/fields/culturepicker.vue'; import DatePicker from '../editor/fields/datepicker.vue'; import DateRangePicker from '../editor/fields/daterangepicker.vue'; @@ -99,7 +100,7 @@ class EditorField /** * */ - _setComponent(component, options) + _setComponent(component, options?) { this.#component = component; this.#componentOptions = options || {}; @@ -306,6 +307,17 @@ class EditorField } + /** + * Renders a mail template picker + * @param {number} [limit=1] - Maximum items to be selected + * @returns {EditorField} + */ + mailTemplatePicker(limit) + { + return this._setComponent(MailTemplatePicker, { limit }); + } + + /** * Renders a date picker * @param {object} [options] - Custom options diff --git a/zero.Web/Controllers/MailTemplatesController.cs b/zero.Web/Controllers/MailTemplatesController.cs new file mode 100644 index 00000000..a640636a --- /dev/null +++ b/zero.Web/Controllers/MailTemplatesController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using zero.Core.Api; +using zero.Core; +using zero.Core.Entities; +using zero.Web.Controllers; +using zero.Web.Models; + +namespace zero.Commerce.Backoffice +{ + public class MailTemplatesController : BackofficeController + { + IMailTemplatesApi Api; + + public MailTemplatesController(IMailTemplatesApi api) + { + Api = api; + } + + + public EditModel GetEmpty([FromServices] IMailTemplate blueprint) => Edit(blueprint); + + public async Task> GetById([FromQuery] string id) => Edit(await Api.GetById(id)); + + public async Task> GetAll([FromQuery] ListQuery query) => await Api.GetByQuery(query); + + public async Task> GetForPicker() => (await Api.GetAll()).Select(x => new SelectModel() + { + Id = x.Id, + Name = x.Name, + IsActive = x.IsActive, + IsShared = x.AppId == Constants.Database.SharedAppId + }); + + + public async Task> GetPreviews([FromQuery] List ids) => Previews(await Api.GetByIds(ids.ToArray()), item => new PreviewModel() + { + Id = item.Id, + Icon = "fth-mail", + Name = item.Name + }); + + + public async Task> Save([FromBody] IMailTemplate model) => await Api.Save(model); + + + public async Task> Delete([FromQuery] string id) => await Api.Delete(id); + } +} diff --git a/zero.Web/Defaults/SettingsPermissions.cs b/zero.Web/Defaults/SettingsPermissions.cs index 4fdbfdc0..3ccd4186 100644 --- a/zero.Web/Defaults/SettingsPermissions.cs +++ b/zero.Web/Defaults/SettingsPermissions.cs @@ -17,6 +17,7 @@ namespace zero.Web.Defaults Items.Add(new Permission(Permissions.Settings.Languages, "@settings.system.languages.name", "@settings.system.languages.text", PermissionValueType.CRUD)); Items.Add(new Permission(Permissions.Settings.Countries, "@settings.system.countries.name", "@settings.system.countries.text", PermissionValueType.CRUD)); Items.Add(new Permission(Permissions.Settings.Translations, "@settings.system.translations.name", "@settings.system.translations.text", PermissionValueType.CRUD)); + Items.Add(new Permission(Permissions.Settings.Mails, "@settings.system.mails.name", "@settings.system.mails.text", PermissionValueType.CRUD)); Items.Add(new Permission(Permissions.Settings.Logging, "@settings.system.logs.name", "@settings.system.logs.text", PermissionValueType.CRUD)); Items.Add(new Permission(Permissions.Settings.Plugins, "@settings.plugins.installed.name", "@settings.plugins.installed.text_default", PermissionValueType.CRUD)); Items.Add(new Permission(Permissions.Settings.CreatePlugin, "@settings.plugins.create.name", "@settings.plugins.create.text", PermissionValueType.CRUD)); diff --git a/zero.Web/Defaults/SystemSettings.cs b/zero.Web/Defaults/SystemSettings.cs index 452546b0..2e431a96 100644 --- a/zero.Web/Defaults/SystemSettings.cs +++ b/zero.Web/Defaults/SystemSettings.cs @@ -15,6 +15,7 @@ namespace zero.Web.Defaults AddInternal(Constants.Settings.Languages, "@settings.system.languages.name", "@settings.system.languages.text", "fth-globe"); AddInternal(Constants.Settings.Countries, "@settings.system.countries.name", "@settings.system.countries.text", "fth-map-pin"); AddInternal(Constants.Settings.Translations, "@settings.system.translations.name", "@settings.system.translations.text", "fth-type"); + AddInternal(Constants.Settings.Mails, "@settings.system.mails.name", "@settings.system.mails.text", "fth-mail"); //AddInternal(Constants.Settings.Logging, "@settings.system.logs.name", "@settings.system.logs.text", "fth-file-text"); } } diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs index 1e113dc1..6e54b109 100644 --- a/zero.Web/Defaults/ZeroBackofficePlugin.cs +++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs @@ -60,6 +60,7 @@ namespace zero.Web.Defaults services.AddTransient, ApplicationValidator>(); services.AddTransient, CountryValidator>(); + services.AddTransient, MailTemplateValidator>(); services.AddTransient, LanguageValidator>(); services.AddTransient, TranslationValidator>(); services.AddTransient, PageValidator>(); @@ -76,6 +77,7 @@ namespace zero.Web.Defaults services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/zero.Web/Resources/Localization/zero.en-us.json b/zero.Web/Resources/Localization/zero.en-us.json index 25cf981b..32c9fff0 100644 --- a/zero.Web/Resources/Localization/zero.en-us.json +++ b/zero.Web/Resources/Localization/zero.en-us.json @@ -249,6 +249,10 @@ "name": "Countries", "text": "Manage list of countries" }, + "mails": { + "name": "Mails", + "text": "Edit mail templates" + }, "logs": { "name": "Logging", "text": "Debug und view logs" @@ -543,6 +547,33 @@ "child_count_x": "{count} items" }, + "mailTemplate": { + "name": "Mail template", + "list": "Mail templates", + "emailBox": "Email settings", + "tabs": { + "sender": "Sender", + "recipient": "Recipient" + }, + "fields": { + "key": "Key", + "key_text": "Used to query for the template in code", + "subject": "Subject", + "body": "Body", + "body_text": "Additional body which is rendered into the mail template", + "senderEmail": "Sender email", + "senderEmail_text": "Leave empty to use application email", + "senderName": "Sender name", + "senderName_text": "Leave empty to use application name", + "recipientEmail": "Recipient email", + "recipientEmail_text": "Only used for mails which do not have a dynamic recipient (e.g. reports)", + "cc": "Send copy to (CC)", + "cc_text": "Comma-separated list of e-mails", + "bcc": "Send invisible copy to (BCC)", + "bcc_text": "Comma-separated list of e-mails" + } + }, + "space": { "list": "Spaces", "name": "Space" diff --git a/zero.Web/ZeroVue.cs b/zero.Web/ZeroVue.cs index 2be3247e..acc13e46 100644 --- a/zero.Web/ZeroVue.cs +++ b/zero.Web/ZeroVue.cs @@ -174,6 +174,7 @@ namespace zero.Web settings.Add("logging", Constants.Settings.Logging); settings.Add("languages", Constants.Settings.Languages); settings.Add("translations", Constants.Settings.Translations); + settings.Add("mails", Constants.Settings.Mails); settings.Add("updates", Constants.Settings.Updates); settings.Add("users", Constants.Settings.Users);