From 39550d71a8568eccf00f18dc31270f7b05d01622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:58:09 +0000 Subject: [PATCH 01/12] Initial plan From 1b7b78658fdaa45439eb959603ac2d8af82d3289 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:01:59 +0000 Subject: [PATCH 02/12] Initial analysis complete - planning Blog management for Store area Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 41c9ad2ed..e9f309cde 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100", + "version": "8.0.119", "rollForward": "latestFeature", "allowPrerelease": false } From 406169c89b31baf489f1290df4f95a0a9e9ad7fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:06:55 +0000 Subject: [PATCH 03/12] Add Blog management functionality to Store area Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- global.json | 2 +- .../Areas/Store/Views/Blog/Create.cshtml | 38 +++ .../Areas/Store/Views/Blog/Edit.cshtml | 44 ++++ .../Areas/Store/Views/Blog/List.cshtml | 109 ++++++++ .../Partials/CreateOrUpdate.TabInfo.cshtml | 146 +++++++++++ .../Partials/CreateOrUpdate.TabSeo.cshtml | 75 ++++++ .../Views/Blog/Partials/CreateOrUpdate.cshtml | 29 +++ .../Areas/Store/Views/Blog/Preview.cshtml | 45 ++++ .../Controllers/BlogController.cs | 237 ++++++++++++++++++ 9 files changed, 724 insertions(+), 1 deletion(-) create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Preview.cshtml create mode 100644 src/Web/Grand.Web.Store/Controllers/BlogController.cs diff --git a/global.json b/global.json index e9f309cde..41c9ad2ed 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.119", + "version": "9.0.100", "rollForward": "latestFeature", "allowPrerelease": false } diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml new file mode 100644 index 000000000..be1476ee8 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml @@ -0,0 +1,38 @@ +@model BlogPostModel +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.AddNew"]; + Layout = Constants.LayoutStore; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogPosts.AddNew"] + + @Html.ActionLink(Loc["Admin.Content.Blog.BlogPosts.BackToList"], "List") + +
+
+
+ + + +
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml new file mode 100644 index 000000000..a3c4caa38 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml @@ -0,0 +1,44 @@ +@model BlogPostModel +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.EditDetails"]; + Layout = Constants.LayoutStore; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogPosts.EditDetails"] - @Model.Title + + @Html.ActionLink(Loc["Admin.Content.Blog.BlogPosts.BackToList"], "List") + +
+
+
+ + + + @Loc["Admin.Common.Preview"] + + + +
+
+
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml new file mode 100644 index 000000000..27a532ba9 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml @@ -0,0 +1,109 @@ +@inject AdminAreaSettings adminAreaSettings +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts"]; + Layout = Constants.LayoutStore; +} + +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogPosts"] +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml new file mode 100644 index 000000000..e4634bf65 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml @@ -0,0 +1,146 @@ +@using System.Text.Encodings.Web +@using Microsoft.AspNetCore.Mvc.Razor +@model BlogPostModel +@inject IBlogService blogService + + + +@{ + Func + template = @
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
; +} + +
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+ @{ + ViewData["Reference"] = "Blog"; + ViewData["ObjectId"] = Model.Id; + } + +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml new file mode 100644 index 000000000..7245e6193 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml @@ -0,0 +1,75 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@model BlogPostModel + + +@{ + Func + template = @
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
; +} + +
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml new file mode 100644 index 000000000..3c35bcf9f --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml @@ -0,0 +1,29 @@ +@model BlogPostModel + +
+ + + + + + + +
+ +
+
+
+ + +
+ +
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Preview.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Preview.cshtml new file mode 100644 index 000000000..7181f83b4 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Preview.cshtml @@ -0,0 +1,45 @@ +@model BlogPostModel +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.Preview"]; + Layout = Constants.LayoutStore; +} + +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogPosts.Preview"] - @Model.Title + + @Html.ActionLink(Loc["Admin.Content.Blog.BlogPosts.BackToList"], "List") + +
+
+
+
+

@Model.Title

+ @if (!string.IsNullOrEmpty(Model.BodyOverview)) + { +
+ @Html.Raw(Model.BodyOverview) +
+ } + @if (!string.IsNullOrEmpty(Model.Body)) + { +
+ @Html.Raw(Model.Body) +
+ } + @if (!string.IsNullOrEmpty(Model.Tags)) + { +
+ @Loc["Admin.Content.Blog.BlogPosts.Fields.Tags"]: @Model.Tags +
+ } +
+
+
+
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Controllers/BlogController.cs b/src/Web/Grand.Web.Store/Controllers/BlogController.cs new file mode 100644 index 000000000..b2bc690f4 --- /dev/null +++ b/src/Web/Grand.Web.Store/Controllers/BlogController.cs @@ -0,0 +1,237 @@ +using Grand.Business.Core.Extensions; +using Grand.Business.Core.Interfaces.Cms; +using Grand.Business.Core.Interfaces.Common.Directory; +using Grand.Business.Core.Interfaces.Common.Localization; +using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Permissions; +using Grand.Infrastructure; +using Grand.Web.AdminShared.Extensions; +using Grand.Web.AdminShared.Extensions.Mapping; +using Grand.Web.AdminShared.Interfaces; +using Grand.Web.AdminShared.Models.Blogs; +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.Blog)] +public class BlogController : BaseStoreController +{ + #region Constructors + + public BlogController( + IBlogService blogService, + IBlogViewModelService blogViewModelService, + ILanguageService languageService, + ITranslationService translationService, + IStoreService storeService, + IContextAccessor contextAccessor, + IGroupService groupService, + IDateTimeService dateTimeService) + { + _blogService = blogService; + _blogViewModelService = blogViewModelService; + _languageService = languageService; + _translationService = translationService; + _storeService = storeService; + _contextAccessor = contextAccessor; + _groupService = groupService; + _dateTimeService = dateTimeService; + } + + #endregion + + #region Fields + + private readonly IBlogService _blogService; + private readonly IBlogViewModelService _blogViewModelService; + private readonly ILanguageService _languageService; + private readonly ITranslationService _translationService; + private readonly IStoreService _storeService; + private readonly IContextAccessor _contextAccessor; + private readonly IGroupService _groupService; + private readonly IDateTimeService _dateTimeService; + + #endregion + + #region Blog posts + + public IActionResult Index() + { + return RedirectToAction("List"); + } + + public IActionResult List() + { + return View(); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task List(DataSourceRequest command) + { + var storeId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + var blogPosts = await _blogService.GetAllBlogPosts(storeId, "", command.Page - 1, command.PageSize); + + var gridModel = new DataSourceResult + { + Data = blogPosts.Select(x => new + { + x.Id, + x.Title, + x.BodyOverview, + CreatedOn = _dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), + x.Comments, + Published = x.StartDate == null || x.StartDate <= DateTime.UtcNow + }).ToList(), + Total = blogPosts.TotalCount + }; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task Create() + { + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + var model = new BlogPostModel + { + //default values + AllowComments = true, + CreateDate = DateTime.UtcNow + }; + + //locales + await AddLocales(_languageService, model.Locales); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Create(BlogPostModel model, bool continueEditing) + { + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + var blogPost = await _blogViewModelService.InsertBlogPostModel(model); + Success(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Added")); + return continueEditing ? RedirectToAction("Edit", new { id = blogPost.Id }) : RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Edit(string id) + { + var blogPost = await _blogService.GetBlogPostById(id); + if (blogPost == null) + //No blog post found with the specified id + return RedirectToAction("List"); + + if (!blogPost.LimitedToStores || (blogPost.LimitedToStores && + blogPost.Stores.Contains(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) && + blogPost.Stores.Count > 1)) + { + Warning(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Permissions")); + } + else + { + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("List"); + } + + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + var model = blogPost.ToModel(_dateTimeService); + + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Title = blogPost.GetTranslation(x => x.Title, languageId, false); + locale.Body = blogPost.GetTranslation(x => x.Body, languageId, false); + locale.BodyOverview = blogPost.GetTranslation(x => x.BodyOverview, languageId, false); + locale.MetaKeywords = blogPost.GetTranslation(x => x.MetaKeywords, languageId, false); + locale.MetaDescription = blogPost.GetTranslation(x => x.MetaDescription, languageId, false); + locale.MetaTitle = blogPost.GetTranslation(x => x.MetaTitle, languageId, false); + locale.SeName = blogPost.GetSeName(languageId, false); + }); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task Edit(BlogPostModel model, bool continueEditing) + { + var blogPost = await _blogService.GetBlogPostById(model.Id); + if (blogPost == null) + //No blog post found with the specified id + return RedirectToAction("List"); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("Edit", new { id = blogPost.Id }); + + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + blogPost = await _blogViewModelService.UpdateBlogPostModel(model, blogPost); + Success(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Updated")); + + if (continueEditing) + { + //selected tab + await SaveSelectedTabIndex(); + return RedirectToAction("Edit", new { id = blogPost.Id }); + } + return RedirectToAction("List"); + } + + //If we got this far, something failed, redisplay form + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task Delete(string id) + { + var blogPost = await _blogService.GetBlogPostById(id); + if (blogPost == null) + //No blog post found with the specified id + return RedirectToAction("List"); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("Edit", new { id = blogPost.Id }); + + if (ModelState.IsValid) + { + await _blogService.DeleteBlogPost(blogPost); + Success(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Deleted")); + return RedirectToAction("List"); + } + + Error(ModelState); + return RedirectToAction("Edit", new { id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task Preview(string id) + { + var blogPost = await _blogService.GetBlogPostById(id); + if (blogPost == null) + return RedirectToAction("List"); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("List"); + + var model = blogPost.ToModel(_dateTimeService); + return View(model); + } + + #endregion +} \ No newline at end of file From 9664d859023503ba42c2e91f1e71e1163952e1f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Sep 2025 18:11:38 +0000 Subject: [PATCH 04/12] Remove store restrictions from Admin BlogController as requested Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Controllers/BlogController.cs | 75 +------------------ 1 file changed, 2 insertions(+), 73 deletions(-) diff --git a/src/Web/Grand.Web.Admin/Controllers/BlogController.cs b/src/Web/Grand.Web.Admin/Controllers/BlogController.cs index 5dd83c6bf..53e46f1e2 100644 --- a/src/Web/Grand.Web.Admin/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/BlogController.cs @@ -111,8 +111,6 @@ public async Task Create(BlogPostModel model, bool continueEditin { if (ModelState.IsValid) { - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; var blogPost = await _blogViewModelService.InsertBlogPostModel(model); Success(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Added")); return continueEditing ? RedirectToAction("Edit", new { id = blogPost.Id }) : RedirectToAction("List"); @@ -132,21 +130,6 @@ public async Task Edit(string id) //No blog post found with the specified id return RedirectToAction("List"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - { - if (!blogPost.LimitedToStores || (blogPost.LimitedToStores && - blogPost.Stores.Contains(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) && - blogPost.Stores.Count > 1)) - { - Warning(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Permissions")); - } - else - { - if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("List"); - } - } - ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); var model = blogPost.ToModel(_dateTimeService); //locales @@ -173,15 +156,8 @@ public async Task Edit(BlogPostModel model, bool continueEditing) //No blog post found with the specified id return RedirectToAction("List"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("Edit", new { id = blogPost.Id }); - if (ModelState.IsValid) { - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; - blogPost = await _blogViewModelService.UpdateBlogPostModel(model, blogPost); Success(_translationService.GetResource("Admin.Content.Blog.BlogPosts.Updated")); @@ -222,10 +198,6 @@ public async Task Delete(string id) //No blog post found with the specified id return RedirectToAction("List"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("Edit", new { id = blogPost.Id }); - if (ModelState.IsValid) { await _blogService.DeleteBlogPost(blogPost); @@ -295,7 +267,7 @@ public IActionResult CategoryList() [HttpPost] public async Task CategoryList(DataSourceRequest command) { - var categories = await _blogService.GetAllBlogCategories(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId); + var categories = await _blogService.GetAllBlogCategories(""); var gridModel = new DataSourceResult { Data = categories, Total = categories.Count @@ -321,9 +293,6 @@ public async Task CategoryCreate(BlogCategoryModel model, bool co { if (ModelState.IsValid) { - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; - var blogCategory = model.ToEntity(); blogCategory.SeName = SeoExtensions.GetSeName( string.IsNullOrEmpty(blogCategory.SeName) ? blogCategory.Name : blogCategory.SeName, @@ -352,22 +321,6 @@ public async Task CategoryEdit(string id) //No blog post found with the specified id return RedirectToAction("CategoryList"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - { - if (!blogCategory.LimitedToStores || (blogCategory.LimitedToStores && - blogCategory.Stores.Contains( - _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId) && - blogCategory.Stores.Count > 1)) - { - Warning(_translationService.GetResource("Admin.Content.Blog.BlogCategory.Permissions")); - } - else - { - if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("List"); - } - } - ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); var model = blogCategory.ToModel(); //locales @@ -388,15 +341,8 @@ public async Task CategoryEdit(BlogCategoryModel model, bool cont //No blog post found with the specified id return RedirectToAction("CategoryList"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("CategoryEdit", new { id = blogCategory.Id }); - if (ModelState.IsValid) { - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; - blogCategory = model.ToEntity(blogCategory); blogCategory.SeName = SeoExtensions.GetSeName( string.IsNullOrEmpty(blogCategory.SeName) ? blogCategory.Name : blogCategory.SeName, @@ -436,10 +382,6 @@ public async Task CategoryDelete(string id) //No blog post found with the specified id return RedirectToAction("CategoryList"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!blogcategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("CategoryEdit", new { id = blogcategory.Id }); - if (ModelState.IsValid) { await _blogService.DeleteBlogCategory(blogcategory); @@ -488,10 +430,6 @@ public async Task CategoryPostDelete(string categoryId, string id if (blogCategory == null) return ErrorForKendoGridJson("blogCategory no exists"); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return ErrorForKendoGridJson("blogCategory no permission"); - if (ModelState.IsValid) { var post = blogCategory.BlogPosts.FirstOrDefault(x => x.Id == id); @@ -512,12 +450,9 @@ public async Task BlogPostAddPopup(string categoryId) { var model = new AddBlogPostCategoryModel(); //stores - var storeId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = " " }); - foreach (var s in (await _storeService.GetAllStores()).Where(x => - x.Id == storeId || string.IsNullOrWhiteSpace(storeId))) + foreach (var s in await _storeService.GetAllStores()) model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); model.CategoryId = categoryId; return View(model); @@ -529,9 +464,6 @@ public async Task BlogPostAddPopupList(DataSourceRequest command, { var gridModel = new DataSourceResult(); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - model.SearchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - var posts = await _blogService.GetAllBlogPosts(model.SearchStoreId, blogPostName: model.SearchBlogTitle, pageIndex: command.Page - 1, pageSize: command.PageSize); gridModel.Data = posts.Select(x => new { x.Id, Name = x.Title }); @@ -594,9 +526,6 @@ public async Task CommentDelete(string id) throw new ArgumentException("No comment found with the specified id"); var blogPost = await _blogService.GetBlogPostById(comment.BlogPostId); - if (await _groupService.IsStoreManager(_contextAccessor.WorkContext.CurrentCustomer)) - if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return ErrorForKendoGridJson("blogPost no permission"); if (ModelState.IsValid) { From fede02cf897bedf3e520276456576e16df19c84c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:00:11 +0000 Subject: [PATCH 05/12] Add missing BlogPostModel using directive to Store _ViewImports.cshtml Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml index d9c332025..4d17948f3 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/_ViewImports.cshtml @@ -30,6 +30,7 @@ @using Grand.Web.AdminShared.Models.Customers @using Grand.Web.AdminShared.Models.Cms @using Grand.Web.AdminShared.Models.News +@using Grand.Web.AdminShared.Models.Blogs @inject LocService Loc @inject IEnumTranslationService EnumTranslationService \ No newline at end of file From c7480d10b826c07f3e582901ef302b51ee2a11dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:18:24 +0000 Subject: [PATCH 06/12] Fix GetAllBlogPosts method call parameter mismatch in Store BlogController Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- src/Web/Grand.Web.Store/Controllers/BlogController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Web/Grand.Web.Store/Controllers/BlogController.cs b/src/Web/Grand.Web.Store/Controllers/BlogController.cs index b2bc690f4..71ba71785 100644 --- a/src/Web/Grand.Web.Store/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Store/Controllers/BlogController.cs @@ -73,7 +73,8 @@ public IActionResult List() public async Task List(DataSourceRequest command) { var storeId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - var blogPosts = await _blogService.GetAllBlogPosts(storeId, "", command.Page - 1, command.PageSize); + var blogPosts = await _blogService.GetAllBlogPosts(storeId, + pageIndex: command.Page - 1, pageSize: command.PageSize); var gridModel = new DataSourceResult { From c6075f2c63148883f123981f36881baf77e53e25 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Sun, 28 Sep 2025 18:29:33 +0200 Subject: [PATCH 07/12] Refactor BlogController to reduce dependencies --- .../Controllers/BlogController.cs | 12 +---------- .../Controllers/BlogController.cs | 21 ++++--------------- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/src/Web/Grand.Web.Admin/Controllers/BlogController.cs b/src/Web/Grand.Web.Admin/Controllers/BlogController.cs index 53e46f1e2..ff3d1aa9d 100644 --- a/src/Web/Grand.Web.Admin/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/BlogController.cs @@ -5,9 +5,6 @@ using Grand.Business.Core.Interfaces.Common.Stores; using Grand.Domain.Permissions; using Grand.Domain.Seo; -using Grand.Infrastructure; -using Grand.Web.Admin.Extensions; -using Grand.Web.AdminShared.Extensions; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; using Grand.Web.AdminShared.Models.Blogs; @@ -31,8 +28,6 @@ public BlogController( ILanguageService languageService, ITranslationService translationService, IStoreService storeService, - IContextAccessor contextAccessor, - IGroupService groupService, IDateTimeService dateTimeService, IPictureViewModelService pictureViewModelService, SeoSettings seoSettings) @@ -42,8 +37,6 @@ public BlogController( _languageService = languageService; _translationService = translationService; _storeService = storeService; - _contextAccessor = contextAccessor; - _groupService = groupService; _dateTimeService = dateTimeService; _pictureViewModelService = pictureViewModelService; _seoSettings = seoSettings; @@ -58,8 +51,6 @@ public BlogController( private readonly ILanguageService _languageService; private readonly ITranslationService _translationService; private readonly IStoreService _storeService; - private readonly IContextAccessor _contextAccessor; - private readonly IGroupService _groupService; private readonly IDateTimeService _dateTimeService; private readonly IPictureViewModelService _pictureViewModelService; private readonly SeoSettings _seoSettings; @@ -450,8 +441,7 @@ public async Task BlogPostAddPopup(string categoryId) { var model = new AddBlogPostCategoryModel(); //stores - model.AvailableStores.Add(new SelectListItem - { Text = _translationService.GetResource("Admin.Common.All"), Value = " " }); + model.AvailableStores.Add(new SelectListItem { Text = _translationService.GetResource("Admin.Common.All"), Value = " " }); foreach (var s in await _storeService.GetAllStores()) model.AvailableStores.Add(new SelectListItem { Text = s.Shortcut, Value = s.Id }); model.CategoryId = categoryId; diff --git a/src/Web/Grand.Web.Store/Controllers/BlogController.cs b/src/Web/Grand.Web.Store/Controllers/BlogController.cs index 71ba71785..80a3bf676 100644 --- a/src/Web/Grand.Web.Store/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Store/Controllers/BlogController.cs @@ -72,24 +72,11 @@ public IActionResult List() [HttpPost] public async Task List(DataSourceRequest command) { - var storeId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - var blogPosts = await _blogService.GetAllBlogPosts(storeId, - pageIndex: command.Page - 1, pageSize: command.PageSize); - - var gridModel = new DataSourceResult - { - Data = blogPosts.Select(x => new - { - x.Id, - x.Title, - x.BodyOverview, - CreatedOn = _dateTimeService.ConvertToUserTime(x.CreatedOnUtc, DateTimeKind.Utc), - x.Comments, - Published = x.StartDate == null || x.StartDate <= DateTime.UtcNow - }).ToList(), - Total = blogPosts.TotalCount + var blogPosts = await _blogViewModelService.PrepareBlogPostsModel(command.Page, command.PageSize); + var gridModel = new DataSourceResult { + Data = blogPosts.blogPosts, + Total = blogPosts.totalCount }; - return Json(gridModel); } From ad5a3c53071c27805079f7374af7970bc40f2a9a Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Mon, 29 Sep 2025 19:01:29 +0200 Subject: [PATCH 08/12] Refactor store filtering in BlogService --- src/Business/Grand.Business.Cms/Services/BlogService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Business/Grand.Business.Cms/Services/BlogService.cs b/src/Business/Grand.Business.Cms/Services/BlogService.cs index cbf06419b..83d1764a7 100644 --- a/src/Business/Grand.Business.Cms/Services/BlogService.cs +++ b/src/Business/Grand.Business.Cms/Services/BlogService.cs @@ -102,7 +102,10 @@ public virtual async Task> GetAllBlogPosts(string storeId = } if (!string.IsNullOrEmpty(storeId) && !_accessControlConfig.IgnoreStoreLimitations) - query = query.Where(b => b.Stores.Contains(storeId) || !b.LimitedToStores); + query = from p in query + where !p.LimitedToStores || p.Stores.Contains(storeId) + select p; + if (!string.IsNullOrEmpty(tag)) query = query.Where(x => x.Tags.Contains(tag)); query = query.OrderByDescending(b => b.CreatedOnUtc); From 7138874a3ee06facff0af9e9377fe56b4a39e7e2 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Mon, 29 Sep 2025 19:02:39 +0200 Subject: [PATCH 09/12] Update permissions for StoreManager customer group --- .../Grand.Module.Installer/Extensions/PermissionExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs b/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs index 0638e318d..4665c1839 100644 --- a/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs +++ b/src/Modules/Grand.Module.Installer/Extensions/PermissionExtensions.cs @@ -213,7 +213,8 @@ public static IEnumerable DefaultPermissions() StandardPermission.ManageMerchandiseReturns, StandardPermission.ManageCheckoutAttribute, StandardPermission.ManageReports, - StandardPermission.ManageNews + StandardPermission.ManageNews, + StandardPermission.ManageBlog ] }, From 9845f44d2f2b3d4d095f723a6bd9aca29b81ca57 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Mon, 29 Sep 2025 19:02:56 +0200 Subject: [PATCH 10/12] Fix async retrieval of blog posts in BlogController Updated the `Create` method to await the asynchronous call to `_blogService.GetBlogPostById(id)`. Removed redundant initialization of the `AllowComments` property in `BlogPostModel`. --- src/Web/Grand.Web.Admin/Controllers/BlogController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Web/Grand.Web.Admin/Controllers/BlogController.cs b/src/Web/Grand.Web.Admin/Controllers/BlogController.cs index ff3d1aa9d..67792b9bf 100644 --- a/src/Web/Grand.Web.Admin/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/BlogController.cs @@ -87,7 +87,8 @@ public async Task Create() ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); var model = new BlogPostModel { //default values - AllowComments = true + AllowComments = true, + CreateDate = DateTime.UtcNow }; //locales await AddLocales(_languageService, model.Locales); @@ -472,7 +473,7 @@ public async Task BlogPostAddPopup(AddBlogPostCategoryModel model if (blogCategory != null) foreach (var id in model.SelectedBlogPostIds) { - var post = _blogService.GetBlogPostById(id); + var post = await _blogService.GetBlogPostById(id); if (post != null) if (!blogCategory.BlogPosts.Any(x => x.BlogPostId == id)) { From c35385c1db9469cc26e30b262198f11a0990f08b Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Mon, 29 Sep 2025 19:03:34 +0200 Subject: [PATCH 11/12] Enhance blog management features and UI improvements --- .../Store/Views/Blog/BlogPostAddPopup.cshtml | 169 ++++++ .../Store/Views/Blog/CategoryCreate.cshtml | 37 ++ .../Store/Views/Blog/CategoryEdit.cshtml | 42 ++ .../Store/Views/Blog/CategoryList.cshtml | 85 +++ .../Areas/Store/Views/Blog/Comments.cshtml | 112 ++++ .../Areas/Store/Views/Blog/Create.cshtml | 7 +- .../Areas/Store/Views/Blog/Edit.cshtml | 28 +- .../Areas/Store/Views/Blog/List.cshtml | 48 +- .../CreateOrUpdate.TabComments.cshtml | 93 ++++ .../Partials/CreateOrUpdate.TabInfo.cshtml | 26 +- .../CreateOrUpdate.TabProducts.cshtml | 128 +++++ .../Partials/CreateOrUpdate.TabSeo.cshtml | 2 +- .../Views/Blog/Partials/CreateOrUpdate.cshtml | 23 +- .../CreateOrUpdateCategory.Posts.cshtml | 112 ++++ .../CreateOrUpdateCategory.TabInfo.cshtml | 47 ++ .../Partials/CreateOrUpdateCategory.cshtml | 23 + .../Store/Views/Blog/ProductAddPopup.cshtml | 221 ++++++++ .../Areas/Store/Views/Product/BulkEdit.cshtml | 3 +- .../Controllers/BlogController.cs | 509 +++++++++++++++++- 19 files changed, 1654 insertions(+), 61 deletions(-) create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/BlogPostAddPopup.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryCreate.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryEdit.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryList.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Comments.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabComments.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabProducts.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.Posts.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.TabInfo.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.cshtml create mode 100644 src/Web/Grand.Web.Store/Areas/Store/Views/Blog/ProductAddPopup.cshtml diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/BlogPostAddPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/BlogPostAddPopup.cshtml new file mode 100644 index 000000000..048f14a2d --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/BlogPostAddPopup.cshtml @@ -0,0 +1,169 @@ +@model AddBlogPostCategoryModel +@inject AdminAreaSettings adminAreaSettings + +@{ + Layout = ""; + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogCategory.AddNewPost"]; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogCategory.AddNewPost"] +
+
+
+
+
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryCreate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryCreate.cshtml new file mode 100644 index 000000000..ee5ec727a --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryCreate.cshtml @@ -0,0 +1,37 @@ +@model BlogCategoryModel +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogCategory.AddNew"]; +} +
+ +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogCategory.AddNew"] + + @Html.ActionLink(Loc["Admin.Content.Blog.BlogCategory.BackToList"], "List") + +
+
+
+ + +
+
+
+ +
+ +
+
+
+
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryEdit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryEdit.cshtml new file mode 100644 index 000000000..21a638b94 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryEdit.cshtml @@ -0,0 +1,42 @@ +@model BlogCategoryModel +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogCategory.EditBlogCategoryDetails"]; +} + +
+ +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogCategory.EditBlogCategoryDetails"] + + @Html.ActionLink(Loc["Admin.Content.Blog.BlogCategory.BackToList"], "CategoryList") + +
+
+
+ + + + @Loc["Admin.Common.Delete"] + +
+
+
+ +
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryList.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryList.cshtml new file mode 100644 index 000000000..3b76a7afd --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/CategoryList.cshtml @@ -0,0 +1,85 @@ +@inject AdminAreaSettings adminAreaSettings +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogCategories"]; +} + +
+
+
+
+
+ + @Loc["Admin.Content.Blog.BlogCategories"] +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Comments.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Comments.cshtml new file mode 100644 index 000000000..16f1e41a0 --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Comments.cshtml @@ -0,0 +1,112 @@ +@inject AdminAreaSettings adminAreaSettings +@{ + //page title + ViewBag.Title = Loc["Admin.Content.Blog.Comments"]; + string filterByBlogPostId = ViewBag.FilterByBlogPostId; +} + +
+
+
+
+
+ + @Loc["Admin.Content.Blog.Comments"] +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +@{ + var readUrl = !string.IsNullOrEmpty(filterByBlogPostId) ? Url.Action("Comments", "Blog", new { filterByBlogPostId, area = Constants.AreaStore }) : Url.Action("Comments", "Blog", new { area = Constants.AreaStore }); +} + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml index be1476ee8..c37419ab5 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Create.cshtml @@ -1,8 +1,7 @@ -@model BlogPostModel +@model BlogPostModel @{ //page title ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.AddNew"]; - Layout = Constants.LayoutStore; }
@@ -11,7 +10,7 @@
- + @Loc["Admin.Content.Blog.BlogPosts.AddNew"] @Html.ActionLink(Loc["Admin.Content.Blog.BlogPosts.BackToList"], "List") @@ -25,10 +24,10 @@ -
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml index a3c4caa38..cb19d53d5 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Edit.cshtml @@ -1,44 +1,46 @@ -@model BlogPostModel +@model BlogPostModel @{ //page title - ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.EditDetails"]; - Layout = Constants.LayoutStore; + ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.EditBlogPostDetails"]; } - + +
- - @Loc["Admin.Content.Blog.BlogPosts.EditDetails"] - @Model.Title + + @Loc["Admin.Content.Blog.BlogPosts.EditBlogPostDetails"] - @Model.Title @Html.ActionLink(Loc["Admin.Content.Blog.BlogPosts.BackToList"], "List")
+ - - @Loc["Admin.Common.Preview"] - - - +
+
- \ No newline at end of file + + \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml index 27a532ba9..764550bb4 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/List.cshtml @@ -1,8 +1,7 @@ -@inject AdminAreaSettings adminAreaSettings +@inject AdminAreaSettings adminAreaSettings @{ //page title ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts"]; - Layout = Constants.LayoutStore; }
@@ -10,17 +9,16 @@
- + @Loc["Admin.Content.Blog.BlogPosts"]
-
-
+
+
@@ -32,6 +30,7 @@
+ - \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabComments.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabComments.cshtml new file mode 100644 index 000000000..1604773de --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabComments.cshtml @@ -0,0 +1,93 @@ +@model BlogPostModel +@inject AdminAreaSettings adminAreaSettings +@if (!string.IsNullOrEmpty(Model.Id)) +{ +
+ +
+
+
+ +
+ + +} +else +{ +
+ @Loc["Admin.Content.Blog.BlogPosts.SaveBeforeEdit"] +
+} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml index e4634bf65..2a8f9bf11 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabInfo.cshtml @@ -1,4 +1,4 @@ -@using System.Text.Encodings.Web +@using System.Text.Encodings.Web @using Microsoft.AspNetCore.Mvc.Razor @model BlogPostModel @inject IBlogService blogService @@ -102,6 +102,30 @@
+ @if (!string.IsNullOrEmpty(Model.PictureId)) + { + + + }
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabProducts.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabProducts.cshtml new file mode 100644 index 000000000..cf5cb85cb --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabProducts.cshtml @@ -0,0 +1,128 @@ +@model BlogPostModel +@inject AdminAreaSettings adminAreaSettings +@if (!string.IsNullOrEmpty(Model.Id)) +{ + + + +} +else +{ +
+ @Loc["Admin.Content.Blog.BlogPosts.SaveBeforeEdit"] +
+} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml index 7245e6193..1fef5cb5b 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.TabSeo.cshtml @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Mvc.Razor +@using Microsoft.AspNetCore.Mvc.Razor @model BlogPostModel diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml index 3c35bcf9f..734865b7e 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdate.cshtml @@ -1,4 +1,4 @@ -@model BlogPostModel +@model BlogPostModel
@@ -20,6 +20,27 @@
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.Posts.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.Posts.cshtml new file mode 100644 index 000000000..181e70aeb --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.Posts.cshtml @@ -0,0 +1,112 @@ +@model BlogCategoryModel + +@inject AdminAreaSettings adminAreaSettings +@if (!string.IsNullOrEmpty(Model.Id)) +{ + + + +} +else +{ +
+ @Loc["Admin.Content.Blog.BlogCategory.SaveBeforeEdit"] +
+} \ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.TabInfo.cshtml new file mode 100644 index 000000000..31a0a955c --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.TabInfo.cshtml @@ -0,0 +1,47 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@model BlogCategoryModel + + +@{ + Func + template = @
+
+ +
+ + +
+
+ +
; +} + +
+ + +
+
+ +
+ + +
+
+
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+ +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.cshtml new file mode 100644 index 000000000..2271f47aa --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/Partials/CreateOrUpdateCategory.cshtml @@ -0,0 +1,23 @@ +@model BlogCategoryModel + +
+ + + + + +
+ +
+
+
+ + +
+ +
+
+
+ +
+
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/ProductAddPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/ProductAddPopup.cshtml new file mode 100644 index 000000000..d161932ad --- /dev/null +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Blog/ProductAddPopup.cshtml @@ -0,0 +1,221 @@ +@model BlogProductModel.AddProductModel +@inject AdminAreaSettings adminAreaSettings +@{ + Layout = ""; + //page title + ViewBag.Title = Loc["Admin.Content.Blog.BlogPosts.Products.AddNew"]; +} + +
+ +
+
+ +
+
+ + +
\ No newline at end of file diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Product/BulkEdit.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Product/BulkEdit.cshtml index 49e251ba9..38ae21002 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Product/BulkEdit.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Product/BulkEdit.cshtml @@ -289,8 +289,7 @@ SearchCategoryId: $('#SearchCategoryId').val(), SearchBrandId: $('#SearchBrandId').val(), SearchCollectionId: $('#SearchCollectionId').val(), - SearchProductTypeId: $('#SearchProductTypeId').val(), - SearchStoreId: $('#SearchStoreId').val(), + SearchProductTypeId: $('#SearchProductTypeId').val() }; addAntiForgeryToken(data); return data; diff --git a/src/Web/Grand.Web.Store/Controllers/BlogController.cs b/src/Web/Grand.Web.Store/Controllers/BlogController.cs index 80a3bf676..58a66227b 100644 --- a/src/Web/Grand.Web.Store/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Store/Controllers/BlogController.cs @@ -2,17 +2,20 @@ using Grand.Business.Core.Interfaces.Cms; using Grand.Business.Core.Interfaces.Common.Directory; using Grand.Business.Core.Interfaces.Common.Localization; -using Grand.Business.Core.Interfaces.Common.Stores; +using Grand.Domain.Blogs; using Grand.Domain.Permissions; +using Grand.Domain.Seo; using Grand.Infrastructure; using Grand.Web.AdminShared.Extensions; using Grand.Web.AdminShared.Extensions.Mapping; using Grand.Web.AdminShared.Interfaces; using Grand.Web.AdminShared.Models.Blogs; +using Grand.Web.AdminShared.Models.Common; using Grand.Web.Common.DataSource; using Grand.Web.Common.Filters; using Grand.Web.Common.Security.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; namespace Grand.Web.Store.Controllers; @@ -26,19 +29,19 @@ public BlogController( IBlogViewModelService blogViewModelService, ILanguageService languageService, ITranslationService translationService, - IStoreService storeService, IContextAccessor contextAccessor, - IGroupService groupService, - IDateTimeService dateTimeService) + IDateTimeService dateTimeService, + IPictureViewModelService pictureViewModelService, + SeoSettings seoSettings) { _blogService = blogService; _blogViewModelService = blogViewModelService; _languageService = languageService; _translationService = translationService; - _storeService = storeService; _contextAccessor = contextAccessor; - _groupService = groupService; _dateTimeService = dateTimeService; + _pictureViewModelService = pictureViewModelService; + _seoSettings = seoSettings; } #endregion @@ -49,11 +52,10 @@ public BlogController( private readonly IBlogViewModelService _blogViewModelService; private readonly ILanguageService _languageService; private readonly ITranslationService _translationService; - private readonly IStoreService _storeService; private readonly IContextAccessor _contextAccessor; - private readonly IGroupService _groupService; private readonly IDateTimeService _dateTimeService; - + private readonly IPictureViewModelService _pictureViewModelService; + private readonly SeoSettings _seoSettings; #endregion #region Blog posts @@ -84,8 +86,7 @@ public async Task List(DataSourceRequest command) public async Task Create() { ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); - var model = new BlogPostModel - { + var model = new BlogPostModel { //default values AllowComments = true, CreateDate = DateTime.UtcNow @@ -222,4 +223,490 @@ public async Task Preview(string id) } #endregion + + #region Picture + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task PicturePopup(string blogpostId) + { + var blogpost = await _blogService.GetBlogPostById(blogpostId); + if (blogpost == null) + return Content("Blog post not exist"); + + if (!blogpost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return Content("You don't have access to this blog post"); + + if (string.IsNullOrEmpty(blogpost.PictureId)) + return Content("Picture not exist"); + + return View("Partials/PicturePopup", + await _pictureViewModelService.PreparePictureModel(blogpost.PictureId, blogpost.Id)); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task PicturePopup(PictureModel model) + { + if (ModelState.IsValid) + { + var blogpost = await _blogService.GetBlogPostById(model.ObjectId); + if (blogpost == null) + throw new ArgumentException("No blog post found with the specified id"); + + if (!blogpost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return Content("You don't have access to this blog post"); + + if (string.IsNullOrEmpty(blogpost.PictureId)) + throw new ArgumentException("No picture found with the specified id"); + + if (blogpost.PictureId != model.Id) + throw new ArgumentException("Picture ident doesn't fit with blog post"); + + await _pictureViewModelService.UpdatePicture(model); + + return Content(""); + } + + Error(ModelState); + + return View("Partials/PicturePopup", model); + } + + #endregion + + #region Comments + + public IActionResult Comments(string filterByBlogPostId) + { + ViewBag.FilterByBlogPostId = filterByBlogPostId; + return View(); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task Comments(string filterByBlogPostId, DataSourceRequest command) + { + var model = await _blogViewModelService.PrepareBlogPostCommentsModel(filterByBlogPostId, command.Page, + command.PageSize); + var gridModel = new DataSourceResult { + Data = model.blogComments, + Total = model.totalCount + }; + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task CommentDelete(string id) + { + var comment = await _blogService.GetBlogCommentById(id); + if (comment == null) + throw new ArgumentException("No comment found with the specified id"); + + var blogPost = await _blogService.GetBlogPostById(comment.BlogPostId); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return ErrorForKendoGridJson("You don't have access to this blog post"); + + if (ModelState.IsValid) + { + await _blogService.DeleteBlogComment(comment); + //update totals + var comments = await _blogService.GetBlogCommentsByBlogPostId(blogPost.Id); + blogPost.CommentCount = comments.Count; + await _blogService.UpdateBlogPost(blogPost); + return new JsonResult(""); + } + + return ErrorForKendoGridJson(ModelState); + } + + #endregion + + #region Products + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task Products(string blogPostId, DataSourceRequest command) + { + var blogPost = await _blogService.GetBlogPostById(blogPostId); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return ErrorForKendoGridJson("You don't have access to this blog post"); + + var model = await _blogViewModelService.PrepareBlogProductsModel(blogPostId, command.Page, command.PageSize); + var gridModel = new DataSourceResult { + Data = model.blogProducts, + Total = model.totalCount + }; + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task ProductAddPopup(string blogPostId) + { + var blogPost = await _blogService.GetBlogPostById(blogPostId); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("You don't have access to this blog post"); + + var model = await _blogViewModelService.PrepareBlogModelAddProductModel(blogPostId); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task ProductAddPopupList(DataSourceRequest command, + BlogProductModel.AddProductModel model) + { + model.SearchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + var products = await _blogViewModelService.PrepareProductModel(model, command.Page, command.PageSize); + + var gridModel = new DataSourceResult { + Data = products.products.ToList(), + Total = products.totalCount + }; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task ProductAddPopup(string blogPostId, BlogProductModel.AddProductModel model) + { + var blogPost = await _blogService.GetBlogPostById(blogPostId); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return View("You don't have access to this blog post"); + + if (model.SelectedProductIds != null) await _blogViewModelService.InsertProductModel(blogPostId, model); + return Content(""); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public async Task UpdateProduct(string blogPostId, BlogProductModel model) + { + var blogPost = await _blogService.GetBlogPostById(blogPostId); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + ModelState.AddModelError("Blog", "You don't have access to this blog post"); + + if (ModelState.IsValid) + { + await _blogViewModelService.UpdateProductModel(model); + return new JsonResult(""); + } + + return ErrorForKendoGridJson(ModelState); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task DeleteProduct(string id) + { + var bp = await _blogService.GetBlogProductById(id) ?? throw new ArgumentException("No blog product found with the specified id"); + var blogPost = await _blogService.GetBlogPostById(bp.BlogPostId); + + if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + ModelState.AddModelError("Blog", "You don't have access to this blog post"); + + if (ModelState.IsValid) + { + await _blogViewModelService.DeleteProductModel(id); + return new JsonResult(""); + } + + return ErrorForKendoGridJson(ModelState); + } + + #endregion + + #region Categories + + public IActionResult CategoryList() + { + return View(); + } + + [PermissionAuthorizeAction(PermissionActionName.List)] + [HttpPost] + public async Task CategoryList(DataSourceRequest command) + { + var storeId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + var categories = await _blogService.GetAllBlogCategories(storeId); + var gridModel = new DataSourceResult { + Data = categories, + Total = categories.Count + }; + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Create)] + public async Task CategoryCreate() + { + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + var model = new BlogCategoryModel(); + //locales + await AddLocales(_languageService, model.Locales); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task CategoryCreate(BlogCategoryModel model, bool continueEditing) + { + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + var blogCategory = model.ToEntity(); + blogCategory.SeName = SeoExtensions.GetSeName( + string.IsNullOrEmpty(blogCategory.SeName) ? blogCategory.Name : blogCategory.SeName, + _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + _seoSettings.SeoCharConversion); + + await _blogService.InsertBlogCategory(blogCategory); + Success(_translationService.GetResource("Admin.Content.Blog.BlogCategory.Added")); + return continueEditing + ? RedirectToAction("CategoryEdit", new { id = blogCategory.Id }) + : RedirectToAction("CategoryList"); + } + + //If we got this far, something failed, redisplay form + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + //locales + await AddLocales(_languageService, model.Locales); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + public async Task CategoryEdit(string id) + { + var blogCategory = await _blogService.GetBlogCategoryById(id); + if (blogCategory == null) + //No blog post found with the specified id + return RedirectToAction("CategoryList"); + + if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("CategoryList"); + + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + var model = blogCategory.ToModel(); + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = blogCategory.GetTranslation(x => x.Name, languageId, false); + }); + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] + public async Task CategoryEdit(BlogCategoryModel model, bool continueEditing) + { + var blogCategory = await _blogService.GetBlogCategoryById(model.Id); + if (blogCategory == null) + //No blog post found with the specified id + return RedirectToAction("CategoryList"); + + if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("CategoryList"); + + if (ModelState.IsValid) + { + model.Stores = [_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId]; + blogCategory = model.ToEntity(blogCategory); + blogCategory.SeName = SeoExtensions.GetSeName( + string.IsNullOrEmpty(blogCategory.SeName) ? blogCategory.Name : blogCategory.SeName, + _seoSettings.ConvertNonWesternChars, _seoSettings.AllowUnicodeCharsInUrls, + _seoSettings.SeoCharConversion); + await _blogService.UpdateBlogCategory(blogCategory); + Success(_translationService.GetResource("Admin.Content.Blog.BlogCategory.Updated")); + if (continueEditing) + { + //selected tab + await SaveSelectedTabIndex(); + + return RedirectToAction("CategoryEdit", new { id = blogCategory.Id }); + } + + return RedirectToAction("CategoryList"); + } + + //If we got this far, something failed, redisplay form + ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); + + //locales + await AddLocales(_languageService, model.Locales, (locale, languageId) => + { + locale.Name = blogCategory.GetTranslation(x => x.Name, languageId, false); + }); + + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + [HttpPost] + public async Task CategoryDelete(string id) + { + var blogCategory = await _blogService.GetBlogCategoryById(id); + if (blogCategory == null) + //No blog post found with the specified id + return RedirectToAction("CategoryList"); + + if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return RedirectToAction("CategoryList"); + + if (ModelState.IsValid) + { + await _blogService.DeleteBlogCategory(blogCategory); + + Success(_translationService.GetResource("Admin.Content.Blog.BlogCategory.Deleted")); + return RedirectToAction("CategoryList"); + } + + Error(ModelState); + return RedirectToAction("CategoryEdit", new { id = blogCategory.Id }); + } + + [PermissionAuthorizeAction(PermissionActionName.Preview)] + [HttpPost] + public async Task CategoryPostList(string categoryId) + { + var blogCategory = await _blogService.GetBlogCategoryById(categoryId); + if (blogCategory == null) + return ErrorForKendoGridJson("blogCategory no exists"); + + if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return ErrorForKendoGridJson("You don't have access to this blog category"); + + var blogposts = new List(); + foreach (var item in blogCategory.BlogPosts) + { + var post = new AdminShared.Models.Blogs.BlogCategoryPost { + Id = item.Id, + BlogPostId = item.BlogPostId + }; + var _post = await _blogService.GetBlogPostById(item.BlogPostId); + if (_post != null) + post.Name = _post.Title; + + blogposts.Add(post); + } + + var gridModel = new DataSourceResult { + Data = blogposts, + Total = blogCategory.BlogPosts.Count + }; + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Delete)] + public async Task CategoryPostDelete(string categoryId, string id) + { + var blogCategory = await _blogService.GetBlogCategoryById(categoryId); + if (blogCategory == null) + return ErrorForKendoGridJson("blogCategory no exists"); + + if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + return ErrorForKendoGridJson("You don't have access to this blog category"); + + if (ModelState.IsValid) + { + var post = blogCategory.BlogPosts.FirstOrDefault(x => x.Id == id); + if (post != null) + { + blogCategory.BlogPosts.Remove(post); + await _blogService.UpdateBlogCategory(blogCategory); + } + + return new JsonResult(""); + } + + return ErrorForKendoGridJson(ModelState); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + public IActionResult BlogPostAddPopup(string categoryId) + { + var model = new AddBlogPostCategoryModel { + CategoryId = categoryId + }; + return View(model); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task BlogPostAddPopupList(DataSourceRequest command, AddBlogPostCategoryModel model) + { + var gridModel = new DataSourceResult(); + model.SearchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + var posts = await _blogService.GetAllBlogPosts(model.SearchStoreId, blogPostName: model.SearchBlogTitle, + pageIndex: command.Page - 1, pageSize: command.PageSize); + gridModel.Data = posts.Select(x => new { x.Id, Name = x.Title }); + gridModel.Total = posts.TotalCount; + + return Json(gridModel); + } + + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task BlogPostAddPopup(AddBlogPostCategoryModel model) + { + if (model.SelectedBlogPostIds == null) + { + return Content(""); + } + + var blogCategory = await _blogService.GetBlogCategoryById(model.CategoryId); + if (blogCategory == null) + { + return Content(""); + } + + if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + { + return Content("You don't have access to this blog category"); + } + + await AddSelectedPostsToBlogCategory(blogCategory, model.SelectedBlogPostIds); + + return Content(""); + } + + private async Task AddSelectedPostsToBlogCategory(BlogCategory blogCategory, string[] postIds) + { + foreach (var id in postIds) + { + await AddPostToBlogCategoryIfValid(blogCategory, id); + } + } + + private async Task AddPostToBlogCategoryIfValid(BlogCategory blogCategory, string postId) + { + var post = await _blogService.GetBlogPostById(postId); + if (post == null) + { + return; + } + + if (!post.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) + { + // Skip posts that the user doesn't have access to + return; + } + + if (blogCategory.BlogPosts.Any(x => x.BlogPostId == postId)) + { + // Skip if the post is already in the category + return; + } + + blogCategory.BlogPosts.Add(new Domain.Blogs.BlogCategoryPost { BlogPostId = postId }); + await _blogService.UpdateBlogCategory(blogCategory); + } + + #endregion } \ No newline at end of file From 84bf58af30140459edd6e2cb0bfd16ac5765d820 Mon Sep 17 00:00:00 2001 From: KrzysztofPajak Date: Mon, 29 Sep 2025 19:19:24 +0200 Subject: [PATCH 12/12] Refactor BlogController for improved error handling --- .../Controllers/BlogController.cs | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/Web/Grand.Web.Store/Controllers/BlogController.cs b/src/Web/Grand.Web.Store/Controllers/BlogController.cs index 58a66227b..7cac921c1 100644 --- a/src/Web/Grand.Web.Store/Controllers/BlogController.cs +++ b/src/Web/Grand.Web.Store/Controllers/BlogController.cs @@ -15,13 +15,20 @@ using Grand.Web.Common.Filters; using Grand.Web.Common.Security.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Rendering; namespace Grand.Web.Store.Controllers; [PermissionAuthorize(PermissionSystemName.Blog)] public class BlogController : BaseStoreController { + #region Constants + + private const string NoAccessToBlogPostMessage = "You don't have access to this blog post"; + private const string NoAccessToBlogCategoryMessage = "You don't have access to this blog category"; + private const string CategoryListAction = "CategoryList"; + + #endregion + #region Constructors public BlogController( @@ -234,7 +241,7 @@ public async Task PicturePopup(string blogpostId) return Content("Blog post not exist"); if (!blogpost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return Content("You don't have access to this blog post"); + return Content(NoAccessToBlogPostMessage); if (string.IsNullOrEmpty(blogpost.PictureId)) return Content("Picture not exist"); @@ -254,7 +261,7 @@ public async Task PicturePopup(PictureModel model) throw new ArgumentException("No blog post found with the specified id"); if (!blogpost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return Content("You don't have access to this blog post"); + return Content(NoAccessToBlogPostMessage); if (string.IsNullOrEmpty(blogpost.PictureId)) throw new ArgumentException("No picture found with the specified id"); @@ -305,7 +312,7 @@ public async Task CommentDelete(string id) var blogPost = await _blogService.GetBlogPostById(comment.BlogPostId); if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return ErrorForKendoGridJson("You don't have access to this blog post"); + return ErrorForKendoGridJson(NoAccessToBlogPostMessage); if (ModelState.IsValid) { @@ -331,7 +338,7 @@ public async Task Products(string blogPostId, DataSourceRequest c var blogPost = await _blogService.GetBlogPostById(blogPostId); if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return ErrorForKendoGridJson("You don't have access to this blog post"); + return ErrorForKendoGridJson(NoAccessToBlogPostMessage); var model = await _blogViewModelService.PrepareBlogProductsModel(blogPostId, command.Page, command.PageSize); var gridModel = new DataSourceResult { @@ -347,7 +354,7 @@ public async Task ProductAddPopup(string blogPostId) var blogPost = await _blogService.GetBlogPostById(blogPostId); if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return View("You don't have access to this blog post"); + return View(NoAccessToBlogPostMessage); var model = await _blogViewModelService.PrepareBlogModelAddProductModel(blogPostId); return View(model); @@ -377,7 +384,7 @@ public async Task ProductAddPopup(string blogPostId, BlogProductM var blogPost = await _blogService.GetBlogPostById(blogPostId); if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return View("You don't have access to this blog post"); + return View(NoAccessToBlogPostMessage); if (model.SelectedProductIds != null) await _blogViewModelService.InsertProductModel(blogPostId, model); return Content(""); @@ -389,7 +396,7 @@ public async Task UpdateProduct(string blogPostId, BlogProductMod var blogPost = await _blogService.GetBlogPostById(blogPostId); if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - ModelState.AddModelError("Blog", "You don't have access to this blog post"); + ModelState.AddModelError("Blog", NoAccessToBlogPostMessage); if (ModelState.IsValid) { @@ -407,7 +414,7 @@ public async Task DeleteProduct(string id) var blogPost = await _blogService.GetBlogPostById(bp.BlogPostId); if (!blogPost.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - ModelState.AddModelError("Blog", "You don't have access to this blog post"); + ModelState.AddModelError("Blog", NoAccessToBlogPostMessage); if (ModelState.IsValid) { @@ -469,7 +476,7 @@ public async Task CategoryCreate(BlogCategoryModel model, bool co Success(_translationService.GetResource("Admin.Content.Blog.BlogCategory.Added")); return continueEditing ? RedirectToAction("CategoryEdit", new { id = blogCategory.Id }) - : RedirectToAction("CategoryList"); + : RedirectToAction(CategoryListAction); } //If we got this far, something failed, redisplay form @@ -485,10 +492,10 @@ public async Task CategoryEdit(string id) var blogCategory = await _blogService.GetBlogCategoryById(id); if (blogCategory == null) //No blog post found with the specified id - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); ViewBag.AllLanguages = await _languageService.GetAllLanguages(true); var model = blogCategory.ToModel(); @@ -508,10 +515,10 @@ public async Task CategoryEdit(BlogCategoryModel model, bool cont var blogCategory = await _blogService.GetBlogCategoryById(model.Id); if (blogCategory == null) //No blog post found with the specified id - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); if (ModelState.IsValid) { @@ -531,7 +538,7 @@ public async Task CategoryEdit(BlogCategoryModel model, bool cont return RedirectToAction("CategoryEdit", new { id = blogCategory.Id }); } - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); } //If we got this far, something failed, redisplay form @@ -553,17 +560,17 @@ public async Task CategoryDelete(string id) var blogCategory = await _blogService.GetBlogCategoryById(id); if (blogCategory == null) //No blog post found with the specified id - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); if (ModelState.IsValid) { await _blogService.DeleteBlogCategory(blogCategory); Success(_translationService.GetResource("Admin.Content.Blog.BlogCategory.Deleted")); - return RedirectToAction("CategoryList"); + return RedirectToAction(CategoryListAction); } Error(ModelState); @@ -579,7 +586,7 @@ public async Task CategoryPostList(string categoryId) return ErrorForKendoGridJson("blogCategory no exists"); if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return ErrorForKendoGridJson("You don't have access to this blog category"); + return ErrorForKendoGridJson(NoAccessToBlogCategoryMessage); var blogposts = new List(); foreach (var item in blogCategory.BlogPosts) @@ -610,7 +617,7 @@ public async Task CategoryPostDelete(string categoryId, string id return ErrorForKendoGridJson("blogCategory no exists"); if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) - return ErrorForKendoGridJson("You don't have access to this blog category"); + return ErrorForKendoGridJson(NoAccessToBlogCategoryMessage); if (ModelState.IsValid) { @@ -636,21 +643,6 @@ public IActionResult BlogPostAddPopup(string categoryId) return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] - [HttpPost] - public async Task BlogPostAddPopupList(DataSourceRequest command, AddBlogPostCategoryModel model) - { - var gridModel = new DataSourceResult(); - model.SearchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; - - var posts = await _blogService.GetAllBlogPosts(model.SearchStoreId, blogPostName: model.SearchBlogTitle, - pageIndex: command.Page - 1, pageSize: command.PageSize); - gridModel.Data = posts.Select(x => new { x.Id, Name = x.Title }); - gridModel.Total = posts.TotalCount; - - return Json(gridModel); - } - [PermissionAuthorizeAction(PermissionActionName.Edit)] [HttpPost] public async Task BlogPostAddPopup(AddBlogPostCategoryModel model) @@ -668,7 +660,7 @@ public async Task BlogPostAddPopup(AddBlogPostCategoryModel model if (!blogCategory.AccessToEntityByStore(_contextAccessor.WorkContext.CurrentCustomer.StaffStoreId)) { - return Content("You don't have access to this blog category"); + return Content(NoAccessToBlogCategoryMessage); } await AddSelectedPostsToBlogCategory(blogCategory, model.SelectedBlogPostIds); @@ -676,6 +668,21 @@ public async Task BlogPostAddPopup(AddBlogPostCategoryModel model return Content(""); } + [PermissionAuthorizeAction(PermissionActionName.Edit)] + [HttpPost] + public async Task BlogPostAddPopupList(DataSourceRequest command, AddBlogPostCategoryModel model) + { + var gridModel = new DataSourceResult(); + model.SearchStoreId = _contextAccessor.WorkContext.CurrentCustomer.StaffStoreId; + + var posts = await _blogService.GetAllBlogPosts(model.SearchStoreId, blogPostName: model.SearchBlogTitle, + pageIndex: command.Page - 1, pageSize: command.PageSize); + gridModel.Data = posts.Select(x => new { x.Id, Name = x.Title }); + gridModel.Total = posts.TotalCount; + + return Json(gridModel); + } + private async Task AddSelectedPostsToBlogCategory(BlogCategory blogCategory, string[] postIds) { foreach (var id in postIds)