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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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/24] 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); From 09a5d27aef018019f20b5af80ce53ef9dcc42f8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 09:52:28 +0000 Subject: [PATCH 15/24] Simplify ListStore to use GetAllMessageTemplates("") with explicit store filter per reviewer feedback Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/c43f06ca-89ba-4452-860e-665f931c14eb Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@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 30396d8f4..f3417260e 100644 --- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs +++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs @@ -64,7 +64,7 @@ public async Task ListGlobal(DataSourceRequest command) [HttpPost] public async Task ListStore(DataSourceRequest command) { - var allTemplates = await messageTemplateService.GetAllMessageTemplates(CurrentStoreId); + var allTemplates = await messageTemplateService.GetAllMessageTemplates(""); var storeTemplates = allTemplates .Where(t => t.LimitedToStores && t.Stores.Contains(CurrentStoreId)) .ToList(); From 71c0a6c79d3fa2777567a60482cebf52258da40b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 10:14:49 +0000 Subject: [PATCH 16/24] Improve GetAllMessageTemplates: remove cache, add keywords search and pagination params Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/46bb0805-5e61-4e90-a30a-e981522752d0 Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Messages/IMessageTemplateService.cs | 10 ++++-- .../Services/MessageTemplateService.cs | 32 +++++++++---------- .../Controllers/MessageTemplateController.cs | 12 +++---- .../Controllers/MessageTemplateController.cs | 16 +++++----- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs index 491e4fb24..1f9e2f050 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Messages; +using Grand.Domain; +using Grand.Domain.Messages; namespace Grand.Business.Core.Interfaces.Messages; @@ -44,8 +45,11 @@ public interface IMessageTemplateService /// Gets all message templates /// /// Store identifier; pass "" to load all records - /// Message template list - Task> GetAllMessageTemplates(string storeId); + /// Keywords to filter by name; pass "" to skip + /// Page index (0-based) + /// Page size + /// Paged list of message templates + Task> GetAllMessageTemplates(string storeId, string keywords = "", int pageIndex = 0, int pageSize = int.MaxValue); /// /// Create a copy of message template with all depended data diff --git a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs index a9e64c48b..cea084061 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs @@ -1,6 +1,7 @@ using Grand.Business.Core.Interfaces.Common.Security; 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; @@ -136,28 +137,27 @@ public virtual async Task GetMessageTemplateByName(string messa /// Gets all message templates /// /// Store identifier; pass "" to load all records - /// Message template list - public virtual async Task> GetAllMessageTemplates(string storeId) + /// Keywords to filter by name; pass "" to skip + /// Page index (0-based) + /// Page size + /// Paged list of message templates + public virtual async Task> GetAllMessageTemplates(string storeId, string keywords = "", int pageIndex = 0, int pageSize = int.MaxValue) { - var key = string.Format(CacheKey.MESSAGETEMPLATES_ALL_KEY, storeId); - return await _cacheBase.GetAsync(key, async () => - { - var query = from p in _messageTemplateRepository.Table - select p; - - query = query.OrderBy(t => t.Name); - - //Store acl - if (string.IsNullOrEmpty(storeId) || _accessControlConfig.IgnoreStoreLimitations) - return await Task.FromResult(query.ToList()); + var query = from p in _messageTemplateRepository.Table + select p; + //Store acl + if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) query = from p in query where !p.LimitedToStores || p.Stores.Contains(storeId) select p; - query = query.OrderBy(t => t.Name); - return await Task.FromResult(query.ToList()); - }); + if (!string.IsNullOrEmpty(keywords)) + query = query.Where(t => t.Name.Contains(keywords)); + + query = query.OrderBy(t => t.Name); + + return await PagedList.Create(query, pageIndex, pageSize); } /// diff --git a/src/Web/Grand.Web.Admin/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Admin/Controllers/MessageTemplateController.cs index b5d5de26d..cb1331e9f 100644 --- a/src/Web/Grand.Web.Admin/Controllers/MessageTemplateController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/MessageTemplateController.cs @@ -77,12 +77,12 @@ public async Task List() [HttpPost] public async Task List(DataSourceRequest command, MessageTemplateListModel model) { - var messageTemplates = await _messageTemplateService.GetAllMessageTemplates(model.SearchStoreId); + var messageTemplates = await _messageTemplateService.GetAllMessageTemplates( + model.SearchStoreId, + keywords: model.Name, + pageIndex: command.Page - 1, + pageSize: command.PageSize); - if (!string.IsNullOrEmpty(model.Name)) - messageTemplates = messageTemplates.Where - (x => x.Name.ToLowerInvariant().Contains(model.Name.ToLowerInvariant()) || - x.Subject.ToLowerInvariant().Contains(model.Name.ToLowerInvariant())).ToList(); var items = new List(); foreach (var x in messageTemplates) { @@ -103,7 +103,7 @@ public async Task List(DataSourceRequest command, MessageTemplate var gridModel = new DataSourceResult { Data = items, - Total = messageTemplates.Count + Total = messageTemplates.TotalCount }; return Json(gridModel); diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs index f3417260e..5596ad81d 100644 --- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs +++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs @@ -107,13 +107,12 @@ public async Task Create(MessageTemplateModel model, bool continu if (ModelState.IsValid) { // 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)); + // Pass keywords to pre-filter by name at the DB level, then verify exact match and store ownership. + var existingStoreTemplate = (await messageTemplateService.GetAllMessageTemplates("", keywords: model.Name)) + .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")); @@ -276,7 +275,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 existing = await messageTemplateService.GetMessageTemplateByName(messageTemplate.Name, CurrentStoreId); + var existing = (await messageTemplateService.GetAllMessageTemplates("", keywords: messageTemplate.Name)) + .FirstOrDefault(t => t.Name == messageTemplate.Name && t.LimitedToStores && t.Stores.Contains(CurrentStoreId)); if (existing != null) { Error(translationService.GetResource("Admin.Content.MessageTemplates.Fields.Name.AlreadyExists")); From 4cb89b45cbf4f931f91eeaea5fc2253ecb0d89f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 10:16:39 +0000 Subject: [PATCH 17/24] Fix case-insensitive keywords search in GetAllMessageTemplates Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/46bb0805-5e61-4e90-a30a-e981522752d0 Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Grand.Business.Messages/Services/MessageTemplateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs index cea084061..0174b680d 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs @@ -153,7 +153,7 @@ public virtual async Task> GetAllMessageTemplates(st select p; if (!string.IsNullOrEmpty(keywords)) - query = query.Where(t => t.Name.Contains(keywords)); + query = query.Where(t => t.Name.ToLower().Contains(keywords.ToLower())); query = query.OrderBy(t => t.Name); From 60a63734e5b83a8890bc49343e0f1cdce6f20422 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 10:20:15 +0000 Subject: [PATCH 18/24] Extend GetAllMessageTemplates keyword search to match Name or Subject Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/d786fe71-dba9-4d6c-b95b-55f993f1417b Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Interfaces/Messages/IMessageTemplateService.cs | 2 +- .../Services/MessageTemplateService.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs index 1f9e2f050..c107e3104 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Messages/IMessageTemplateService.cs @@ -45,7 +45,7 @@ public interface IMessageTemplateService /// Gets all message templates /// /// Store identifier; pass "" to load all records - /// Keywords to filter by name; pass "" to skip + /// Keywords to filter by name or subject; pass "" to skip /// Page index (0-based) /// Page size /// Paged list of message templates diff --git a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs index 0174b680d..7162ce084 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs @@ -137,7 +137,7 @@ public virtual async Task GetMessageTemplateByName(string messa /// Gets all message templates /// /// Store identifier; pass "" to load all records - /// Keywords to filter by name; pass "" to skip + /// Keywords to filter by name or subject; pass "" to skip /// Page index (0-based) /// Page size /// Paged list of message templates @@ -153,7 +153,11 @@ public virtual async Task> GetAllMessageTemplates(st select p; if (!string.IsNullOrEmpty(keywords)) - query = query.Where(t => t.Name.ToLower().Contains(keywords.ToLower())); + { + var lowerKeywords = keywords.ToLower(); + query = query.Where(t => t.Name.ToLower().Contains(lowerKeywords) || + (t.Subject != null && t.Subject.ToLower().Contains(lowerKeywords))); + } query = query.OrderBy(t => t.Name); From cc37fc2730dd77603209704f650ca6665e7a3eef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 10:35:34 +0000 Subject: [PATCH 19/24] Fix admin MessageTemplate List grid pagination controls Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/4223e887-7440-45d3-92de-e2f09f5498b3 Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Areas/Admin/Views/MessageTemplate/List.cshtml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml index 199f985f8..f143e7fe2 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml @@ -1,4 +1,5 @@ @model MessageTemplateListModel +@inject AdminAreaSettings adminAreaSettings @{ //page title ViewBag.Title = Loc["Admin.Content.MessageTemplates"]; @@ -81,9 +82,7 @@ }, pageable: { refresh: true, - numeric: false, - previousNext: false, - info: false + pageSizes: [@(adminAreaSettings.GridPageSizes)] }, editable: { confirmation: false, From d9f4c6df04f2b30d1434d7297e11bf8739df5485 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 10:40:11 +0000 Subject: [PATCH 20/24] Add edit link to global list Name column; show global templates read-only in Edit view Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/66e028d6-da6a-4ea7-bbb0-59ae0d4a4cde Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Store/Views/MessageTemplate/Edit.cshtml | 28 +++++++++++-------- .../Store/Views/MessageTemplate/List.cshtml | 3 +- .../Controllers/MessageTemplateController.cs | 7 +++-- 3 files changed, 24 insertions(+), 14 deletions(-) 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 index 234ab9549..c012f8d4b 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml @@ -18,16 +18,19 @@
- - - - - @Loc["Admin.Common.Delete"] - + @if (!(bool)(ViewBag.IsReadOnly ?? false)) + { + + + + + @Loc["Admin.Common.Delete"] + + }
@@ -38,4 +41,7 @@ - +@if (!(bool)(ViewBag.IsReadOnly ?? false)) +{ + +} 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 7aedc8cc8..87ecae4b2 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 @@ -119,7 +119,8 @@ columns: [{ field: "Name", title: "@Loc["Admin.Content.MessageTemplates.Fields.Name"]", - width: 300 + width: 300, + template: '#=kendo.htmlEncode(Name)#' }, { field: "Subject", title: "@Loc["Admin.Content.MessageTemplates.Fields.Subject"]", diff --git a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs index 5596ad81d..9d80fa9c8 100644 --- a/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs +++ b/src/Web/Grand.Web.Store/Controllers/MessageTemplateController.cs @@ -161,10 +161,13 @@ public async Task Edit(string id) if (messageTemplate == null) return RedirectToAction("List"); - // Only allow editing store-specific templates belonging to this store - if (!messageTemplate.LimitedToStores || !messageTemplate.Stores.Contains(CurrentStoreId)) + // Block access to templates that belong to a different store + if (messageTemplate.LimitedToStores && !messageTemplate.Stores.Contains(CurrentStoreId)) return RedirectToAction("List"); + // Global templates (LimitedToStores=false) are shown read-only + ViewBag.IsReadOnly = !messageTemplate.LimitedToStores; + var model = messageTemplate.ToModel(); model.SendImmediately = !model.DelayBeforeSend.HasValue; model.HasAttachedDownload = !string.IsNullOrEmpty(model.AttachedDownloadId); From 58447a3a5ff0a9b3b80872a5d7d0c253beabd058 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 17 May 2026 10:40:52 +0000 Subject: [PATCH 21/24] Extract isReadOnly variable in Edit.cshtml to avoid duplication Agent-Logs-Url: https://github.com/grandnode/grandnode2/sessions/66e028d6-da6a-4ea7-bbb0-59ae0d4a4cde Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Areas/Store/Views/MessageTemplate/Edit.cshtml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 index c012f8d4b..e47a3d174 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/MessageTemplate/Edit.cshtml @@ -2,6 +2,7 @@ @{ ViewBag.Title = Loc["Admin.Content.MessageTemplates.EditMessageTemplateDetails"]; Layout = Constants.LayoutStore; + var isReadOnly = (bool)(ViewBag.IsReadOnly ?? false); }
@@ -18,7 +19,7 @@
- @if (!(bool)(ViewBag.IsReadOnly ?? false)) + @if (!isReadOnly) {
-@if (!(bool)(ViewBag.IsReadOnly ?? false)) +@if (!isReadOnly) { } From e8a74edfc147b0e388a28f1ff4cb87dacd664349 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Sun, 17 May 2026 13:35:27 +0200 Subject: [PATCH 22/24] Refactor string comparison in MessageTemplateService --- .../Grand.Business.Messages/Services/MessageTemplateService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs index 7162ce084..acabdda17 100644 --- a/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs +++ b/src/Business/Grand.Business.Messages/Services/MessageTemplateService.cs @@ -156,7 +156,7 @@ public virtual async Task> GetAllMessageTemplates(st { var lowerKeywords = keywords.ToLower(); query = query.Where(t => t.Name.ToLower().Contains(lowerKeywords) || - (t.Subject != null && t.Subject.ToLower().Contains(lowerKeywords))); + (t.Subject != null && t.Subject.Contains(lowerKeywords, StringComparison.CurrentCultureIgnoreCase))); } query = query.OrderBy(t => t.Name); From ea374dde681c61a56a884ceadb1461c332369dbd Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Sun, 17 May 2026 13:36:05 +0200 Subject: [PATCH 23/24] Add pageSize to Kendo UI grid configuration --- .../Areas/Admin/Views/MessageTemplate/List.cshtml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml index f143e7fe2..916688db4 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/MessageTemplate/List.cshtml @@ -76,6 +76,7 @@ // Cancel the changes this.cancelChanges(); }, + pageSize: @(adminAreaSettings.DefaultGridPageSize), serverPaging: true, serverFiltering: true, serverSorting: true From 18ef6ea66c2369e5b86586aaf132be5832e1f299 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Sun, 17 May 2026 13:36:21 +0200 Subject: [PATCH 24/24] Update package versions and project references --- Directory.Packages.props | 68 +++++++++---------- .../Authentication.Facebook.csproj | 2 +- .../Authentication.Google.csproj | 2 +- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 510f84898..1e7e528b7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,69 +3,69 @@ true - + - - + + - + - - - + + + - - + + - - - - + + + + - + - - - + + + - + - + - - + + - + - - + + - - + + - - - - - - + + + + + + @@ -75,7 +75,7 @@ - - + + \ No newline at end of file diff --git a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj index 81006cd9d..d3de7b90d 100644 --- a/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj +++ b/src/Plugins/Authentication.Facebook/Authentication.Facebook.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/Plugins/Authentication.Google/Authentication.Google.csproj b/src/Plugins/Authentication.Google/Authentication.Google.csproj index a76e7720c..1db5fedda 100644 --- a/src/Plugins/Authentication.Google/Authentication.Google.csproj +++ b/src/Plugins/Authentication.Google/Authentication.Google.csproj @@ -20,7 +20,7 @@ - +