Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ public interface IEmailAccountService
/// </summary>
/// <returns>Email accounts list</returns>
Task<IList<EmailAccount>> GetAllEmailAccounts();

/// <summary>
/// Gets email accounts belonging to the specified store.
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <returns>Email accounts for the given store</returns>
Task<IList<EmailAccount>> GetEmailAccountsByStore(string storeId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,18 @@ public virtual async Task<IList<EmailAccount>> GetAllEmailAccounts()
return await Task.FromResult(query.ToList());
});
}

/// <summary>
/// Gets email accounts belonging to the specified store.
/// </summary>
/// <param name="storeId">Store identifier</param>
/// <returns>Email accounts for the given store</returns>
public virtual async Task<IList<EmailAccount>> GetEmailAccountsByStore(string storeId)
{
var allAccounts = await GetAllEmailAccounts();
if (string.IsNullOrEmpty(storeId))
return allAccounts;

return allAccounts.Where(ea => ea.StoreId == storeId).ToList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,14 @@ protected virtual async Task<MessageTemplate> GetMessageTemplate(string messageT
}

protected virtual async Task<EmailAccount> GetEmailAccountOfMessageTemplate(MessageTemplate messageTemplate,
string languageId)
string languageId, string storeId = "")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Does MessageTemplate contain storeId?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MessageTemplate does not have a single StoreId — it uses LimitedToStores (bool) + Stores (list of store IDs), which is the standard GrandNode store-scoping pattern for multi-store visibility.

The storeId in GetEmailAccountOfMessageTemplate comes from the call site (e.g. the order-placed notification passes the order's StoreId). It's already threaded correctly through GetMessageTemplate(messageTemplateName, storeId) for the template lookup, and is now reused for the store-specific email account fallback.

{
var emailAccounId = messageTemplate.GetTranslation(mt => mt.EmailAccountId, languageId);
var emailAccount = (await _emailAccountService.GetEmailAccountById(emailAccounId) ??
await _emailAccountService.GetEmailAccountById(_emailAccountSettings
.DefaultEmailAccountId)) ??
(await _emailAccountService.GetAllEmailAccounts()).FirstOrDefault();
var emailAccount = await _emailAccountService.GetEmailAccountById(emailAccounId);
emailAccount ??= await _emailAccountService.GetEmailAccountById(_emailAccountSettings.DefaultEmailAccountId);
if (emailAccount == null && !string.IsNullOrEmpty(storeId))
emailAccount = (await _emailAccountService.GetEmailAccountsByStore(storeId)).FirstOrDefault();
emailAccount ??= (await _emailAccountService.GetAllEmailAccounts()).FirstOrDefault();
return emailAccount;
}

Expand Down
6 changes: 6 additions & 0 deletions src/Core/Grand.Domain/Messages/EmailAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ public class EmailAccount : BaseEntity
/// Provides a way of specifying the SSL and/or TLS encryption that should be used for a connection
/// </summary>
public int SecureSocketOptionsId { get; set; }

/// <summary>
/// Gets or sets the store identifier this email account belongs to.
/// An empty string means this is a global/shared email account.
/// </summary>
public string StoreId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
<admin-select asp-for="SecureSocketOptionsId" asp-items="EnumTranslationService.ToSelectList((SecureSocketOptions)Model.SecureSocketOptionsId)"/>
</div>
</div>
<div class="form-group">
<admin-label asp-for="StoreId"/>
<div class="col-md-9 col-sm-9">
<select asp-for="StoreId" asp-items="Model.AvailableStores" class="form-control"></select>
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Id))
{
<div class="form-group">
Expand Down
8 changes: 5 additions & 3 deletions src/Web/Grand.Web.Admin/Controllers/EmailAccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ public async Task<IActionResult> MarkAsDefaultEmail(string id)
}

[PermissionAuthorizeAction(PermissionActionName.Create)]
public IActionResult Create()
public async Task<IActionResult> Create()
{
var model = _emailAccountViewModelService.PrepareEmailAccountModel();
var model = await _emailAccountViewModelService.PrepareEmailAccountModel();
return View(model);
}

Expand Down Expand Up @@ -108,7 +108,9 @@ public async Task<IActionResult> Edit(string id)
//No email account found with the specified id
return RedirectToAction("List");

return View(emailAccount.ToModel());
var model = emailAccount.ToModel();
await _emailAccountViewModelService.PrepareAvailableStores(model);
return View(model);
}

[HttpPost]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ namespace Grand.Web.AdminShared.Interfaces;

public interface IEmailAccountViewModelService
{
EmailAccountModel PrepareEmailAccountModel();
Task<EmailAccountModel> PrepareEmailAccountModel();
Task PrepareAvailableStores(EmailAccountModel model);
Task<EmailAccount> InsertEmailAccountModel(EmailAccountModel model);
Task<EmailAccount> UpdateEmailAccountModel(EmailAccount emailAccount, EmailAccountModel model);
Task SendTestEmail(EmailAccount emailAccount, EmailAccountModel model);
Expand Down
3 changes: 2 additions & 1 deletion src/Web/Grand.Web.AdminShared/Mapper/EmailAccountProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public EmailAccountProfile()
CreateMap<EmailAccount, EmailAccountModel>()
.ForMember(dest => dest.Password, mo => mo.Ignore())
.ForMember(dest => dest.IsDefaultEmailAccount, mo => mo.Ignore())
.ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore());
.ForMember(dest => dest.SendTestEmailTo, mo => mo.Ignore())
.ForMember(dest => dest.AvailableStores, mo => mo.Ignore());

CreateMap<EmailAccountModel, EmailAccount>()
.ForMember(dest => dest.Id, mo => mo.Ignore())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Grand.Infrastructure.ModelBinding;
using Grand.Infrastructure.Models;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Grand.Web.AdminShared.Models.Messages;

Expand Down Expand Up @@ -34,4 +35,9 @@ public class EmailAccountModel : BaseEntityModel

[GrandResourceDisplayName("Admin.Configuration.EmailAccounts.Fields.SendTestEmailTo")]
public string SendTestEmailTo { get; set; }

[GrandResourceDisplayName("Admin.Configuration.EmailAccounts.Fields.Store")]
public string StoreId { get; set; }

public IList<SelectListItem> AvailableStores { get; set; } = new List<SelectListItem>();
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
using Grand.Business.Core.Interfaces.Messages;
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Business.Core.Interfaces.Common.Stores;
using Grand.Business.Core.Interfaces.Messages;
using Grand.Domain.Messages;
using Grand.Web.AdminShared.Extensions.Mapping;
using Grand.Web.AdminShared.Interfaces;
using Grand.Web.AdminShared.Models.Messages;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Grand.Web.AdminShared.Services;

public class EmailAccountViewModelService : IEmailAccountViewModelService
{
private readonly IEmailAccountService _emailAccountService;
private readonly IEmailSender _emailSender;
private readonly IStoreService _storeService;
private readonly ITranslationService _translationService;

public EmailAccountViewModelService(IEmailAccountService emailAccountService, IEmailSender emailSender)
public EmailAccountViewModelService(IEmailAccountService emailAccountService, IEmailSender emailSender,
IStoreService storeService, ITranslationService translationService)
{
_emailAccountService = emailAccountService;
_emailSender = emailSender;
_storeService = storeService;
_translationService = translationService;
}

public virtual EmailAccountModel PrepareEmailAccountModel()
public virtual async Task<EmailAccountModel> PrepareEmailAccountModel()
{
var model = new EmailAccountModel {
//default values
Port = 25
};
await PopulateAvailableStores(model);
return model;
}

public virtual async Task PrepareAvailableStores(EmailAccountModel model)
{
await PopulateAvailableStores(model);
}

public virtual async Task<EmailAccount> InsertEmailAccountModel(EmailAccountModel model)
{
var emailAccount = model.ToEntity();
Expand All @@ -52,4 +66,17 @@ public virtual async Task SendTestEmail(EmailAccount emailAccount, EmailAccountM
await _emailSender.SendEmail(emailAccount, subject, body, emailAccount.Email, emailAccount.DisplayName,
model.SendTestEmailTo, null);
}

private async Task PopulateAvailableStores(EmailAccountModel model)
{
model.AvailableStores.Add(new SelectListItem {
Value = "",
Text = _translationService.GetResource("Admin.Settings.StoreScope.AllStores")
});
foreach (var store in await _storeService.GetAllStores())
model.AvailableStores.Add(new SelectListItem {
Value = store.Id,
Text = store.Name
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@model EmailAccountModel
@{
//page title
ViewBag.Title = Loc["Admin.Configuration.EmailAccounts.AddNew"];
}
<form asp-area="@Constants.AreaStore" asp-controller="EmailAccount" asp-action="Create" method="post">

<div class="row">
<div class="col-md-12">
<div class="x_panel light form-fit">
<div class="x_title">
<div class="caption">
<i class="fa fa-envelope"></i>
@Loc["Admin.Configuration.EmailAccounts.AddNew"]
<small>
<i class="fa fa-arrow-circle-left"></i> @Html.ActionLink(Loc["Admin.Configuration.EmailAccounts.BackToList"], "List")
</small>
</div>
<div class="actions">
<button class="btn btn-success" type="submit" name="save">
<i class="fa fa-check"></i> @Loc["Admin.Common.Save"]
</button>
<button class="btn btn-success" type="submit" name="save-continue">
<i class="fa fa-check-circle"></i> @Loc["Admin.Common.SaveContinue"]
</button>
</div>
</div>
<div class="x_content form">
<partial name="Partials/CreateOrUpdate" model="Model"/>
</div>
</div>
</div>
</div>
</form>
40 changes: 40 additions & 0 deletions src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/Edit.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@model EmailAccountModel
@{
//page title
ViewBag.Title = Loc["Admin.Configuration.EmailAccounts.EditEmailAccountDetails"];
}
<form asp-area="@Constants.AreaStore" asp-controller="EmailAccount" asp-action="Edit" method="post">

<div class="row">
<div class="col-md-12">
<div class="x_panel light form-fit">
<div class="x_title">
<div class="caption">
<i class="fa fa-envelope"></i>
@Loc["Admin.Configuration.EmailAccounts.EditEmailAccountDetails"]
<small>
<i class="fa fa-arrow-circle-left"></i> @Html.ActionLink(Loc["Admin.Configuration.EmailAccounts.BackToList"], "List")
</small>
</div>
<div class="actions">
<div class="btn-group btn-group-devided util-btn-margin-bottom-5">
<button class="btn btn-success" type="submit" name="save">
<i class="fa fa-check"></i> @Loc["Admin.Common.Save"]
</button>
<button class="btn btn-success" type="submit" name="save-continue">
<i class="fa fa-check-circle"></i> @Loc["Admin.Common.SaveContinue"]
</button>
<span id="emailAccount-delete" class="btn red">
<i class="fa fa-trash-o"></i><span class="d-none d-sm-inline"> @Loc["Admin.Common.Delete"]</span>
</span>
</div>
</div>
</div>
<div class="x_content form">
<partial name="Partials/CreateOrUpdate" model="Model"/>
</div>
</div>
</div>
</div>
</form>
<admin-delete-confirmation button-id="emailAccount-delete"/>
71 changes: 71 additions & 0 deletions src/Web/Grand.Web.Store/Areas/Store/Views/EmailAccount/List.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
@{
//page title
ViewBag.Title = Loc["Admin.Configuration.EmailAccounts"];
}

<div class="row">
<div class="col-md-12">
<div class="x_panel light form-fit">
<div class="x_title">
<div class="caption">
<i class="fa fa-envelope"></i>
@Loc["Admin.Configuration.EmailAccounts"]
</div>
<div class="actions">
<a href="@Url.Action("Create", new { area = Constants.AreaStore })" class="btn green">
<i class="fa fa-plus"></i><span class="d-none d-sm-inline"> @Loc["Admin.Common.AddNew"] </span>
</a>
</div>
</div>
<div class="x_content form">
<div id="email-accounts-grid"></div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$("#email-accounts-grid").kendoGrid({
dataSource: {
transport: {
read: {
url: "@Html.Raw(Url.Action("List", "EmailAccount", new { area = Constants.AreaStore }))",
type: "POST",
dataType: "json",
data: addAntiForgeryToken
}
},
schema: {
data: "Data",
total: "Total",
errors: "Errors"
},
error: function(e) {
display_kendoui_grid_error(e);
this.cancelChanges();
},
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
pageable: {
refresh: true,
numeric: false,
previousNext: false,
info: false
},
scrollable: false,
columns: [{
field: "Email",
title: "@Loc["Admin.Configuration.EmailAccounts.Fields.Email"]",
template: '<a class="k-link" href="Edit/#=Id#">#=Email#</a>'
}, {
field: "DisplayName",
title: "@Loc["Admin.Configuration.EmailAccounts.Fields.DisplayName"]"
}, {
field: "Host",
title: "@Loc["Admin.Configuration.EmailAccounts.Fields.Host"]"
}]
});
});
</script>
Loading
Loading