Skip to content
Merged
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
@@ -1,4 +1,5 @@
using Grand.Domain.Messages;
using Grand.Domain;
using Grand.Domain.Messages;

namespace Grand.Business.Core.Interfaces.Messages;

Expand Down Expand Up @@ -30,8 +31,11 @@ public interface IEmailAccountService
Task<EmailAccount> GetEmailAccountById(string emailAccountId);

/// <summary>
/// Gets all email accounts
/// Gets all email accounts, optionally filtered by store, with pagination support.
/// </summary>
/// <returns>Email accounts list</returns>
Task<IList<EmailAccount>> GetAllEmailAccounts();
/// <param name="storeId">Store identifier; pass empty string to return all accounts</param>
/// <param name="pageIndex">Page index (0-based)</param>
/// <param name="pageSize">Page size</param>
/// <returns>Paged list of email accounts</returns>
Task<IPagedList<EmailAccount>> GetAllEmailAccounts(string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Grand.Business.Core.Interfaces.Messages;
using Grand.Data;
using Grand.Domain;
using Grand.Domain.Messages;
using Grand.Infrastructure.Caching;
using Grand.Infrastructure.Caching.Constants;
Expand Down Expand Up @@ -109,8 +110,7 @@ public virtual async Task UpdateEmailAccount(EmailAccount emailAccount)
public virtual async Task DeleteEmailAccount(EmailAccount emailAccount)
{
ArgumentNullException.ThrowIfNull(emailAccount);
var emailAccounts = await GetAllEmailAccounts();
if (emailAccounts.Count == 1)
if (_emailAccountRepository.Table.Take(2).Count() <= 1)
throw new GrandException("You cannot delete this email account. At least one account is required.");

await _emailAccountRepository.DeleteAsync(emailAccount);
Expand All @@ -134,16 +134,20 @@ public virtual async Task<EmailAccount> GetEmailAccountById(string emailAccountI
}

/// <summary>
/// Gets all email accounts
/// Gets all email accounts, optionally filtered by store, with pagination support.
/// </summary>
/// <returns>Email accounts list</returns>
public virtual async Task<IList<EmailAccount>> GetAllEmailAccounts()
/// <param name="storeId">Store identifier; pass empty string to return all accounts</param>
/// <param name="pageIndex">Page index (0-based)</param>
/// <param name="pageSize">Page size</param>
/// <returns>Paged list of email accounts</returns>
public virtual async Task<IPagedList<EmailAccount>> GetAllEmailAccounts(string storeId = "", int pageIndex = 0, int pageSize = int.MaxValue)
{
return await _cacheBase.GetAsync(CacheKey.EMAILACCOUNT_ALL_KEY, async () =>
{
var query = from ea in _emailAccountRepository.Table
select ea;
return await Task.FromResult(query.ToList());
});
var query = from ea in _emailAccountRepository.Table
select ea;

if (!string.IsNullOrEmpty(storeId))
query = query.Where(ea => ea.StoreId == storeId);

return await PagedList<EmailAccount>.Create(query, pageIndex, pageSize);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,9 @@ protected virtual async Task<EmailAccount> GetEmailAccountOfMessageTemplate(Mess
string languageId)
{
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);
Comment thread
KrzysztofPajak marked this conversation as resolved.
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; }
Comment thread
KrzysztofPajak marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,6 @@ public static partial class CacheKey
/// </remarks>
public static string EMAILACCOUNT_BY_ID_KEY => "Grand.emailaccount.id-{0}";

/// <summary>
/// Key for caching
/// </summary>
/// <remarks>
/// </remarks>
public static string EMAILACCOUNT_ALL_KEY => "Grand.emailaccount.all";

/// <summary>
/// Key pattern to clear cache
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,18 @@ public async Task UpdateEmailAccount_InvokeExpectedMethods()
[TestMethod]
public async Task DeleteEmailAccount_InvokeExpectedMethods()
{
_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<List<EmailAccount>>>>()))
.Returns(Task.FromResult(new List<EmailAccount> { new(), new() }));
_repository.Setup(r => r.Table).Returns(new List<EmailAccount> { new(), new() }.AsQueryable());
await _service.DeleteEmailAccount(new EmailAccount());
_repository.Verify(c => c.DeleteAsync(It.IsAny<EmailAccount>()), Times.Once);
_mediatorMock.Verify(c => c.Publish(It.IsAny<EntityDeleted<EmailAccount>>(), default), Times.Once);
_cacheMock.Verify(c => c.RemoveByPrefix(It.IsAny<string>(), It.IsAny<bool>()), Times.Once);
}

[TestMethod]
public void DeleteEmailAccount_ExistOnlyOneAccount_ThrowException()
public async Task DeleteEmailAccount_ExistOnlyOneAccount_ThrowException()
{
//we can't delete account if exist only one
_cacheMock.Setup(c => c.GetAsync(It.IsAny<string>(), It.IsAny<Func<Task<List<EmailAccount>>>>()))
.Returns(Task.FromResult(new List<EmailAccount> { new() }));
Assert.ThrowsExactlyAsync<GrandException>(async () => await _service.DeleteEmailAccount(new EmailAccount()));
_repository.Setup(r => r.Table).Returns(new List<EmailAccount> { new() }.AsQueryable());
await Assert.ThrowsExactlyAsync<GrandException>(async () => await _service.DeleteEmailAccount(new EmailAccount()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Grand.Business.Core.Interfaces.Messages;
using Grand.Business.Core.Queries.Messages;
using Grand.Business.Messages.Services;
using Grand.Domain;
using Grand.Domain.Catalog;
using Grand.Domain.Common;
using Grand.Domain.Customers;
Expand Down Expand Up @@ -51,9 +52,9 @@ public void Init()
new List<Language> { new() { Name = "English" }, new() { Name = "Polish" } } as IList<Language>));

_emailAccountServiceMock = new Mock<IEmailAccountService>();
_emailAccountServiceMock.Setup(x => x.GetAllEmailAccounts())
.Returns(Task.FromResult(
new List<EmailAccount> { new() { Email = "sdfsdf@mail.com" } } as IList<EmailAccount>));
_emailAccountServiceMock.Setup(x => x.GetAllEmailAccounts(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
.Returns(Task.FromResult<IPagedList<EmailAccount>>(
new PagedList<EmailAccount>(new List<EmailAccount> { new() { Email = "sdfsdf@mail.com" } }, 0, int.MaxValue)));

_messageTokenProviderMock = new Mock<IMessageTokenProvider>();

Expand Down
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>
Comment thread
KrzysztofPajak marked this conversation as resolved.
</div>
@if (!string.IsNullOrEmpty(Model.Id))
{
<div class="form-group">
Expand Down
15 changes: 8 additions & 7 deletions src/Web/Grand.Web.Admin/Controllers/EmailAccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,14 @@ public IActionResult List()
[PermissionAuthorizeAction(PermissionActionName.List)]
public async Task<IActionResult> List(DataSourceRequest command)
{
var emailAccountModels = (await _emailAccountService.GetAllEmailAccounts())
.Select(x => x.ToModel())
.ToList();
var emailAccounts = await _emailAccountService.GetAllEmailAccounts(pageIndex: command.Page - 1, pageSize: command.PageSize);
var emailAccountModels = emailAccounts.Select(x => x.ToModel()).ToList();
foreach (var eam in emailAccountModels)
eam.IsDefaultEmailAccount = eam.Id == _emailAccountSettings.DefaultEmailAccountId;

var gridModel = new DataSourceResult {
Data = emailAccountModels,
Total = emailAccountModels.Count
Total = emailAccounts.TotalCount
};

return Json(gridModel);
Expand All @@ -78,9 +77,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 +107,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
});
Comment thread
KrzysztofPajak marked this conversation as resolved.
}
}
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"/>
Loading
Loading