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; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.MessageTemplates.AddNew"] + + @Html.ActionLink(Loc["Admin.Content.MessageTemplates.BackToList"], "List") + +
+
+
+ + +
+
+
+
+ +
+
+
+
+
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; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.MessageTemplates.EditMessageTemplateDetails"] - @Model.Name + + @Html.ActionLink(Loc["Admin.Content.MessageTemplates.BackToList"], "List") + +
+
+
+ + + + + @Loc["Admin.Common.Delete"] + +
+
+
+
+ +
+
+
+
+
+ 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; +} + +
+
+
+
+
+ + @Loc["Admin.Content.MessageTemplates"] +
+ +
+
+ + + + +
+
+
+ + +
+
+
+
+
+
+
+
+
+ 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 = @
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ @foreach (var token in Model.AllowedTokens) + { + + } +
+ + +
+
+
+ +
+ + +
+
+ +
; +} + +
+
+
+ +
+ @if (!string.IsNullOrEmpty(Model.Id)) + { + + + } + else + { + + } +
+
+
+ +
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ @foreach (var token in Model.AllowedTokens) + { + + } +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+ + +
+
+
+ +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ + +
+
+
+
+
+ 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);