From 1f517b57b05626098d1cc7aecd5ec7a827000c36 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 19:02:16 +0000
Subject: [PATCH 01/14] Add MessageTemplate management for Store area
(controller + CRUD views)
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/1b07a443-a39f-4582-9230-d1c8b6154d69
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Store/Views/MessageTemplate/Create.cshtml | 36 +++
.../Store/Views/MessageTemplate/Edit.cshtml | 41 +++
.../Store/Views/MessageTemplate/List.cshtml | 144 ++++++++++
.../Partials/CreateOrUpdate.cshtml | 215 ++++++++++++++
.../Controllers/MessageTemplateController.cs | 263 ++++++++++++++++++
5 files changed, 699 insertions(+)
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml
create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
create mode 100644 src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml
new file mode 100644
index 000000000..566cc6135
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Create.cshtml
@@ -0,0 +1,36 @@
+@model MessageTemplateModel
+@{
+ ViewBag.Title = Loc["Admin.Content.MessageTemplates.AddNew"];
+ Layout = Constants.LayoutStore;
+}
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml
new file mode 100644
index 000000000..234ab9549
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml
@@ -0,0 +1,41 @@
+@model MessageTemplateModel
+@{
+ ViewBag.Title = Loc["Admin.Content.MessageTemplates.EditMessageTemplateDetails"];
+ Layout = Constants.LayoutStore;
+}
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml
new file mode 100644
index 000000000..f1b4a2a08
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml
@@ -0,0 +1,144 @@
+@{
+ ViewBag.Title = Loc["Admin.Content.MessageTemplates"];
+ Layout = Constants.LayoutStore;
+}
+
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
new file mode 100644
index 000000000..ee43d306c
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
@@ -0,0 +1,215 @@
+@using Grand.Domain.Messages
+@using Microsoft.AspNetCore.Mvc.Razor
+@using Constants = Grand.SharedUIResources.Constants
+@model MessageTemplateModel
+
+
+
+
+
+
+
+@{
+ Func
+ template = @;
+}
+
+
+
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
new file mode 100644
index 000000000..6663b9eb6
--- /dev/null
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -0,0 +1,263 @@
+using Grand.Business.Core.Extensions;
+using Grand.Business.Core.Interfaces.Common.Localization;
+using Grand.Business.Core.Interfaces.Messages;
+using Grand.Business.Core.Interfaces.Storage;
+using Grand.Domain.Messages;
+using Grand.Domain.Permissions;
+using Grand.Infrastructure;
+using Grand.Web.AdminShared.Extensions.Mapping;
+using Grand.Web.AdminShared.Models.Messages;
+using Grand.Web.Common.DataSource;
+using Grand.Web.Common.Filters;
+using Grand.Web.Common.Security.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Grand.Web.Store.Controllers;
+
+[PermissionAuthorize(PermissionSystemName.MessageTemplates)]
+public class MessageTemplateController(
+ IMessageTemplateService messageTemplateService,
+ IEmailAccountService emailAccountService,
+ ILanguageService languageService,
+ ITranslationService translationService,
+ IMessageTokenProvider messageTokenProvider,
+ IDownloadService downloadService,
+ IContextAccessor contextAccessor) : BaseStoreController
+{
+ private string CurrentStoreId => contextAccessor.WorkContext.CurrentCustomer.StaffStoreId;
+
+ public IActionResult Index()
+ {
+ return RedirectToAction("List");
+ }
+
+ public IActionResult List()
+ {
+ return View();
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.List)]
+ [HttpPost]
+ public async Task ListGlobal(DataSourceRequest command)
+ {
+ var allTemplates = await messageTemplateService.GetAllMessageTemplates("");
+ var globalTemplates = allTemplates
+ .Where(t => !t.LimitedToStores)
+ .ToList();
+
+ var items = globalTemplates.Select(x => x.ToModel()).ToList();
+
+ var gridModel = new DataSourceResult {
+ Data = items,
+ Total = items.Count
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.List)]
+ [HttpPost]
+ public async Task ListStore(DataSourceRequest command)
+ {
+ var allTemplates = await messageTemplateService.GetAllMessageTemplates(CurrentStoreId);
+ var storeTemplates = allTemplates
+ .Where(t => t.LimitedToStores && t.Stores.Contains(CurrentStoreId))
+ .ToList();
+
+ var items = storeTemplates.Select(x => x.ToModel()).ToList();
+
+ var gridModel = new DataSourceResult {
+ Data = items,
+ Total = items.Count
+ };
+
+ return Json(gridModel);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Create)]
+ public async Task Create()
+ {
+ var model = new MessageTemplateModel {
+ AllowedTokens = messageTokenProvider.GetListOfAllowedTokens()
+ };
+
+ foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId))
+ model.AvailableEmailAccounts.Add(ea.ToModel());
+
+ await AddLocales(languageService, model.Locales);
+
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Create)]
+ [HttpPost]
+ [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")]
+ public async Task Create(MessageTemplateModel model, bool continueEditing)
+ {
+ if (ModelState.IsValid)
+ {
+ var messageTemplate = model.ToEntity();
+ if (!model.HasAttachedDownload)
+ messageTemplate.AttachedDownloadId = "";
+ if (model.SendImmediately)
+ messageTemplate.DelayBeforeSend = null;
+
+ // Assign to the current store
+ messageTemplate.LimitedToStores = true;
+ messageTemplate.Stores = [CurrentStoreId];
+
+ await messageTemplateService.InsertMessageTemplate(messageTemplate);
+
+ Success(translationService.GetResource("Admin.Content.MessageTemplates.AddNew"));
+
+ if (continueEditing)
+ {
+ await SaveSelectedTabIndex();
+ return RedirectToAction("Edit", new { id = messageTemplate.Id });
+ }
+
+ return RedirectToAction("List");
+ }
+
+ model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId);
+ model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens();
+ foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId))
+ model.AvailableEmailAccounts.Add(ea.ToModel());
+
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Preview)]
+ public async Task Edit(string id)
+ {
+ var messageTemplate = await messageTemplateService.GetMessageTemplateById(id);
+ if (messageTemplate == null)
+ return RedirectToAction("List");
+
+ // Only allow editing store-specific templates belonging to this store
+ if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId))
+ return RedirectToAction("List");
+
+ var model = messageTemplate.ToModel();
+ model.SendImmediately = !model.DelayBeforeSend.HasValue;
+ model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId);
+ model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens();
+
+ foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId))
+ model.AvailableEmailAccounts.Add(ea.ToModel());
+
+ await AddLocales(languageService, model.Locales, (locale, languageId) =>
+ {
+ locale.BccEmailAddresses = messageTemplate.GetTranslation(x => x.BccEmailAddresses, languageId, false);
+ locale.Subject = messageTemplate.GetTranslation(x => x.Subject, languageId, false);
+ locale.Body = messageTemplate.GetTranslation(x => x.Body, languageId, false);
+ locale.EmailAccountId = messageTemplate.GetTranslation(x => x.EmailAccountId, languageId, false);
+ });
+
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")]
+ public async Task Edit(MessageTemplateModel model, bool continueEditing)
+ {
+ var messageTemplate = await messageTemplateService.GetMessageTemplateById(model.Id);
+ if (messageTemplate == null)
+ return RedirectToAction("List");
+
+ if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId))
+ return RedirectToAction("List");
+
+ var prevAttachment = messageTemplate.AttachedDownloadId;
+
+ if (ModelState.IsValid)
+ {
+ messageTemplate = model.ToEntity(messageTemplate);
+ if (!model.HasAttachedDownload)
+ messageTemplate.AttachedDownloadId = "";
+ if (model.SendImmediately)
+ messageTemplate.DelayBeforeSend = null;
+
+ // Keep it assigned to the current store
+ messageTemplate.LimitedToStores = true;
+ messageTemplate.Stores = [CurrentStoreId];
+
+ if (!string.IsNullOrEmpty(prevAttachment) && prevAttachment != messageTemplate.AttachedDownloadId)
+ {
+ var attachment = await downloadService.GetDownloadById(prevAttachment);
+ if (attachment != null)
+ await downloadService.DeleteDownload(attachment);
+ }
+
+ await messageTemplateService.UpdateMessageTemplate(messageTemplate);
+
+ Success(translationService.GetResource("Admin.Content.MessageTemplates.Updated"));
+
+ if (continueEditing)
+ {
+ await SaveSelectedTabIndex();
+ return RedirectToAction("Edit", new { id = messageTemplate.Id });
+ }
+
+ return RedirectToAction("List");
+ }
+
+ model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId);
+ model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens();
+ foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId))
+ model.AvailableEmailAccounts.Add(ea.ToModel());
+
+ return View(model);
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Delete)]
+ [HttpPost]
+ public async Task Delete(string id)
+ {
+ var messageTemplate = await messageTemplateService.GetMessageTemplateById(id);
+ if (messageTemplate == null)
+ return RedirectToAction("List");
+
+ if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId))
+ return RedirectToAction("List");
+
+ await messageTemplateService.DeleteMessageTemplate(messageTemplate);
+
+ if (!string.IsNullOrEmpty(messageTemplate.AttachedDownloadId))
+ {
+ var attachment = await downloadService.GetDownloadById(messageTemplate.AttachedDownloadId);
+ if (attachment != null)
+ await downloadService.DeleteDownload(attachment);
+ }
+
+ Success(translationService.GetResource("Admin.Content.MessageTemplates.Deleted"));
+ return RedirectToAction("List");
+ }
+
+ [PermissionAuthorizeAction(PermissionActionName.Edit)]
+ [HttpPost]
+ public async Task CopyTemplate(MessageTemplateModel model)
+ {
+ var messageTemplate = await messageTemplateService.GetMessageTemplateById(model.Id);
+ if (messageTemplate == null)
+ return RedirectToAction("List");
+
+ try
+ {
+ var newMessageTemplate = await messageTemplateService.CopyMessageTemplate(messageTemplate);
+ // Assign copy to the current store
+ newMessageTemplate.LimitedToStores = true;
+ newMessageTemplate.Stores = [CurrentStoreId];
+ await messageTemplateService.UpdateMessageTemplate(newMessageTemplate);
+
+ Success(translationService.GetResource("Admin.Content.MessageTemplates.Copied"));
+ return RedirectToAction("Edit", new { id = newMessageTemplate.Id });
+ }
+ catch (Exception exc)
+ {
+ Error(exc.Message);
+ return RedirectToAction("List");
+ }
+ }
+}
From 100ba4b5028cd72c6f3ad216f7316ca4cb650196 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 12 May 2026 19:03:27 +0000
Subject: [PATCH 02/14] Fix implicit global variable in MessageTemplate partial
view
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/1b07a443-a39f-4582-9230-d1c8b6154d69
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
index ee43d306c..8ef5bcd29 100644
--- a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
@@ -175,7 +175,7 @@
sendImmediately();
$(".SearchTokens").on("input", function (e) { SearchTokens(e); })
$("input").click(function (e) {
- onInputFocus = $(e.target);
+ var onInputFocus = $(e.target);
});
});
From c81b0170c5a016a613f3c8f6ed2066436c0b5d72 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 18:11:52 +0000
Subject: [PATCH 03/14] Prevent duplicate message templates per store in Create
action
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/1c5b0e71-8c09-4c7a-bbfd-8e08e1a565ec
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/MessageTemplateController.cs | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index 6663b9eb6..49c4654e1 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -96,6 +96,18 @@ public async Task Create(MessageTemplateModel model, bool continu
{
if (ModelState.IsValid)
{
+ // Prevent duplicate: check if a template with this name already exists for the current store
+ var existing = await messageTemplateService.GetMessageTemplateByName(model.Name, CurrentStoreId);
+ if (existing != null)
+ {
+ ModelState.AddModelError("Name", translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists"));
+ model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId);
+ model.AllowedTokens = messageTokenProvider.GetListOfAllowedTokens();
+ foreach (var ea in await emailAccountService.GetAllEmailAccounts(CurrentStoreId))
+ model.AvailableEmailAccounts.Add(ea.ToModel());
+ return View(model);
+ }
+
var messageTemplate = model.ToEntity();
if (!model.HasAttachedDownload)
messageTemplate.AttachedDownloadId = "";
From 081fb1898b3309e31bf7f37b8cf12a3b28a35326 Mon Sep 17 00:00:00 2001
From: Krzysztof Pajak
Date: Wed, 13 May 2026 20:28:56 +0200
Subject: [PATCH 04/14] Potential fix for pull request finding 'Generic catch
clause'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
---
.../Grand.Web.Store/Controllers/MessageTemplateController.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index 49c4654e1..e304086e3 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -266,7 +266,7 @@ public async Task CopyTemplate(MessageTemplateModel model)
Success(translationService.GetResource("Admin.Content.MessageTemplates.Copied"));
return RedirectToAction("Edit", new { id = newMessageTemplate.Id });
}
- catch (Exception exc)
+ catch (GrandException exc)
{
Error(exc.Message);
return RedirectToAction("List");
From e720b1afb8cb2f484e2eb53c38cc9be31973fa49 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 18:34:19 +0000
Subject: [PATCH 05/14] Add missing resource string for duplicate template name
validation
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/57979dac-6df7-46fc-90db-fa65d54059b1
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/MessageTemplateController.cs | 1 +
.../App_Data/Resources/DefaultLanguage.xml | Bin 1502728 -> 1503104 bytes
2 files changed, 1 insertion(+)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index e304086e3..6e613bc80 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -5,6 +5,7 @@
using Grand.Domain.Messages;
using Grand.Domain.Permissions;
using Grand.Infrastructure;
+using Grand.SharedKernel;
using Grand.Web.AdminShared.Extensions.Mapping;
using Grand.Web.AdminShared.Models.Messages;
using Grand.Web.Common.DataSource;
diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml
index 4ed1399d65af4dcbfc77a852350c0d60b1b71177..c23d786376f54c640ffcc56fb415c7a43a645d0a 100644
GIT binary patch
delta 133
zcmeCU6Wwq>x}k-!g{g(Pg{6hHg>4IaMAY;LQH)&Eg;ZI5raOdkuuQ+=!LBsDpqpK0
z@}g^M(+_mBcTIPQU^keq(8I~mo)EGrHSaf?rl-P>N06=Cg7ytkO
delta 89
zcmWN=$q9fk5I|8}
Date: Wed, 13 May 2026 18:47:37 +0000
Subject: [PATCH 06/14] Block copy when template with same name already exists
for current store
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/5f08e2f5-9eb0-4ed2-ac62-5e5dc47f2a11
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/MessageTemplateController.cs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index 6e613bc80..b3c86ae9d 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -256,6 +256,14 @@ public async Task CopyTemplate(MessageTemplateModel model)
if (messageTemplate == null)
return RedirectToAction("List");
+ // Prevent duplicate: check if a template with the same name already exists for the current store
+ var existingTemplate = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId);
+ if (existingTemplate != null)
+ {
+ Error(translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists"));
+ return RedirectToAction("List");
+ }
+
try
{
var newMessageTemplate = await messageTemplateService.CopyMessageTemplate(messageTemplate);
From 54f4377feb2e4217c74f2486d65abcef14255431 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 18:57:08 +0000
Subject: [PATCH 07/14] Fix false positive duplicate check when copying global
message templates
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/f3b9557e-207e-4413-9966-997bfeb76665
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Grand.Web.Store/Controllers/MessageTemplateController.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index b3c86ae9d..bd5d867ba 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -256,9 +256,9 @@ public async Task CopyTemplate(MessageTemplateModel model)
if (messageTemplate == null)
return RedirectToAction("List");
- // Prevent duplicate: check if a template with the same name already exists for the current store
+ // Prevent duplicate: check if a store-specific template with the same name already exists for the current store
var existingTemplate = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId);
- if (existingTemplate != null)
+ if (existingTemplate != null && existingTemplate.LimitedToStores && existingTemplate.Stores.Contains(CurrentStoreId))
{
Error(translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists"));
return RedirectToAction("List");
From 74c5fa1fbffcd3a7f28f75a93c9c1c46411597b3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 19:02:04 +0000
Subject: [PATCH 08/14] Add missing resource strings for message template tabs
(store/global)
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/a78f4ba7-57ec-4186-8d1f-befdff60d57c
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../App_Data/Resources/DefaultLanguage.xml | Bin 1503104 -> 1503544 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml
index c23d786376f54c640ffcc56fb415c7a43a645d0a..c772df2d00a47a46a53da1c27723b7b2731b4f15 100644
GIT binary patch
delta 168
zcmZp;AHCyIbVCbc3sVbo3rh=Y3)>d<6LI_{42cX$40;Ud3^@$>3`x@;vNM~ryF)pQ
zJGtVg8$_@hv4WLNKRBOd>GT8-c7y4A`WdaJeGuT7e&8!x*7OD5>`K$~64<%g7sRsz
sF$WNH0x^is4a7V^%nQVPK+F%s0zfPX#6mzU48$TpEV_L`yx4<&0L>jdeE^X4c#F-0MZrpEiM+`p>O(G5e
From fb220b8c58f146d6ed3669ef7db8baf3cced8165 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 13 May 2026 19:17:46 +0000
Subject: [PATCH 09/14] Block copy of store-specific templates
(LimitedToStores=true) in CopyTemplate action
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/d4201352-8e49-4a12-a9d0-e07d480710ea
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Grand.Web.Store/Controllers/MessageTemplateController.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index bd5d867ba..21c82695f 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -256,6 +256,10 @@ public async Task CopyTemplate(MessageTemplateModel model)
if (messageTemplate == null)
return RedirectToAction("List");
+ // Only allow copying global templates (LimitedToStores = false)
+ if (messageTemplate.LimitedToStores)
+ return RedirectToAction("List");
+
// Prevent duplicate: check if a store-specific template with the same name already exists for the current store
var existingTemplate = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId);
if (existingTemplate != null && existingTemplate.LimitedToStores && existingTemplate.Stores.Contains(CurrentStoreId))
From 5195dd85d061968cfc38233c6ac3fbb9e94d574c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 16:21:07 +0000
Subject: [PATCH 10/14] Fix duplicate copy check and add missing
admin.content.messagetemplates.copied resource string
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/2421abf4-924b-47d0-b2c5-d0d54936ed31
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/MessageTemplateController.cs | 8 ++++++--
.../App_Data/Resources/DefaultLanguage.xml | Bin 1503544 -> 1503820 bytes
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index 21c82695f..e1b2271b8 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -261,8 +261,12 @@ public async Task CopyTemplate(MessageTemplateModel model)
return RedirectToAction("List");
// Prevent duplicate: check if a store-specific template with the same name already exists for the current store
- var existingTemplate = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId);
- if (existingTemplate != null && existingTemplate.LimitedToStores && existingTemplate.Stores.Contains(CurrentStoreId))
+ var allTemplates = await messageTemplateService.GetAllMessageTemplates("");
+ var existingStoreTemplate = allTemplates.FirstOrDefault(t =>
+ t.Name == messageTemplate.Name &&
+ t.LimitedToStores &&
+ t.Stores.Contains(CurrentStoreId));
+ if (existingStoreTemplate != null)
{
Error(translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists"));
return RedirectToAction("List");
diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml
index c772df2d00a47a46a53da1c27723b7b2731b4f15..527aba66d2ce1d80baee88cc46c0c772de80b011 100644
GIT binary patch
delta 123
zcmdn-DEiEk=!O=?7N!>F7M2#)7Pc+yi+tFV8S)tl7&509s`5Ecf9Jz)zzPrFA(zqF+UIs0I?tt3jwh(5Q_k@==KI5u^meQ
DU(_n%
delta 114
zcmX@}BzniA=!O=?7N!>F7M2#)7Pc+yi+rZfD&X>%Zt#+eWqMu;yTSAeF>GDa6T;Ya
y+F$sw12G2>a{@6J5OV`D4-oSLF&_}~1F--Q3j(na5DNpb2oQ^If8i&VvKRoG%P?vH
From d90c92a27a72afe10258bdede66f447505c59d64 Mon Sep 17 00:00:00 2001
From: KrzysztofPajak
Date: Thu, 14 May 2026 18:28:44 +0200
Subject: [PATCH 11/14] Optimize duplicate check in message template copy
---
.../Controllers/MessageTemplateController.cs | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index e1b2271b8..b4452b60c 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -2,7 +2,6 @@
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Business.Core.Interfaces.Messages;
using Grand.Business.Core.Interfaces.Storage;
-using Grand.Domain.Messages;
using Grand.Domain.Permissions;
using Grand.Infrastructure;
using Grand.SharedKernel;
@@ -261,12 +260,8 @@ public async Task CopyTemplate(MessageTemplateModel model)
return RedirectToAction("List");
// Prevent duplicate: check if a store-specific template with the same name already exists for the current store
- var allTemplates = await messageTemplateService.GetAllMessageTemplates("");
- var existingStoreTemplate = allTemplates.FirstOrDefault(t =>
- t.Name == messageTemplate.Name &&
- t.LimitedToStores &&
- t.Stores.Contains(CurrentStoreId));
- if (existingStoreTemplate != null)
+ var existing = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId);
+ if (existing != null)
{
Error(translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists"));
return RedirectToAction("List");
From d3f4a12e3738cb7a37416306840a607d8b2d0033 Mon Sep 17 00:00:00 2001
From: Krzysztof Pajak
Date: Thu, 14 May 2026 21:42:28 +0200
Subject: [PATCH 12/14] Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
.../Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
index 8ef5bcd29..8e3520ab3 100644
--- a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Partials/CreateOrUpdate.cshtml
@@ -174,9 +174,6 @@
toggleHasAttachedDownload();
sendImmediately();
$(".SearchTokens").on("input", function (e) { SearchTokens(e); })
- $("input").click(function (e) {
- var onInputFocus = $(e.target);
- });
});
function toggleHasAttachedDownload() {
From 3cd28d80b58b1efd248b2efb3ec474a5f1576e16 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 19:50:06 +0000
Subject: [PATCH 13/14] Add server-side pagination to ListGlobal and ListStore
grid actions
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/40ee3d5e-fc73-4a47-a5ca-f3d900d30869
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Store/Views/MessageTemplate/List.cshtml | 11 +++++------
.../Controllers/MessageTemplateController.cs | 18 ++++++++++++++----
2 files changed, 19 insertions(+), 10 deletions(-)
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml
index f1b4a2a08..7aedc8cc8 100644
--- a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml
+++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/List.cshtml
@@ -1,3 +1,4 @@
+@inject AdminAreaSettings adminAreaSettings
@{
ViewBag.Title = Loc["Admin.Content.MessageTemplates"];
Layout = Constants.LayoutStore;
@@ -57,15 +58,14 @@
display_kendoui_grid_error(e);
this.cancelChanges();
},
+ pageSize: @(adminAreaSettings.DefaultGridPageSize),
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
pageable: {
refresh: true,
- numeric: false,
- previousNext: false,
- info: false
+ pageSizes: [@(adminAreaSettings.GridPageSizes)]
},
scrollable: false,
columns: [{
@@ -106,15 +106,14 @@
display_kendoui_grid_error(e);
this.cancelChanges();
},
+ pageSize: @(adminAreaSettings.DefaultGridPageSize),
serverPaging: true,
serverFiltering: true,
serverSorting: true
},
pageable: {
refresh: true,
- numeric: false,
- previousNext: false,
- info: false
+ pageSizes: [@(adminAreaSettings.GridPageSizes)]
},
scrollable: false,
columns: [{
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index b4452b60c..e2b9558ca 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -45,11 +45,16 @@ public async Task ListGlobal(DataSourceRequest command)
.Where(t => !t.LimitedToStores)
.ToList();
- var items = globalTemplates.Select(x => x.ToModel()).ToList();
+ var total = globalTemplates.Count;
+ var items = globalTemplates
+ .Skip((command.Page - 1) * command.PageSize)
+ .Take(command.PageSize)
+ .Select(x => x.ToModel())
+ .ToList();
var gridModel = new DataSourceResult {
Data = items,
- Total = items.Count
+ Total = total
};
return Json(gridModel);
@@ -64,11 +69,16 @@ public async Task ListStore(DataSourceRequest command)
.Where(t => t.LimitedToStores && t.Stores.Contains(CurrentStoreId))
.ToList();
- var items = storeTemplates.Select(x => x.ToModel()).ToList();
+ var total = storeTemplates.Count;
+ var items = storeTemplates
+ .Skip((command.Page - 1) * command.PageSize)
+ .Take(command.PageSize)
+ .Select(x => x.ToModel())
+ .ToList();
var gridModel = new DataSourceResult {
Data = items,
- Total = items.Count
+ Total = total
};
return Json(gridModel);
From 2668755e3b9d780d5268090e62ae62aa605e56b1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 19:54:09 +0000
Subject: [PATCH 14/14] Fix false-positive duplicate check in Create action to
allow store-specific templates with same name as global templates
Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/e7f3eee3-ad46-44a1-834c-44b0a06d6341
Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com>
---
.../Controllers/MessageTemplateController.cs | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
index e2b9558ca..30396d8f4 100644
--- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
+++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs
@@ -106,9 +106,15 @@ public async Task Create(MessageTemplateModel model, bool continu
{
if (ModelState.IsValid)
{
- // Prevent duplicate: check if a template with this name already exists for the current store
- var existing = await messageTemplateService.GetMessageTemplateByName(model.Name, CurrentStoreId);
- if (existing != null)
+ // Prevent duplicate: check only for store-specific templates with this name for the current store.
+ // GetMessageTemplateByName uses ACL and returns global templates too, so we use GetAllMessageTemplates("")
+ // and filter explicitly to avoid false positives on global templates sharing the same name.
+ var allTemplates = await messageTemplateService.GetAllMessageTemplates("");
+ var existingStoreTemplate = allTemplates.FirstOrDefault(t =>
+ t.Name == model.Name &&
+ t.LimitedToStores &&
+ t.Stores.Contains(CurrentStoreId));
+ if (existingStoreTemplate != null)
{
ModelState.AddModelError("Name", translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists"));
model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId);