diff --git a/.ai/skills/theme-creation.md b/.ai/skills/theme-creation.md index 64b662ce93..c7cda45d16 100644 --- a/.ai/skills/theme-creation.md +++ b/.ai/skills/theme-creation.md @@ -84,7 +84,8 @@ Do not use this skill for changes to `Grand.Web/Views` themselves — that is `. 16. Add `Views/{ThemeName}/_ViewStart.cshtml`. 17. Copy **only** the views the theme actually changes. Everything else resolves through the fallback locations. Each copied view is a file that must be re-reconciled on every upgrade. 18. A copied view keeps the same `@model`, the same route names, and the same storefront data attributes — `data-cart-action`, product IDs, quick-view URLs, wishlist, compare, add-to-cart, image `alt` / `title` / `loading`. -19. Keep widget zones (`@await Component.InvokeAsync("Widget", new { widgetZone = "..." })`) present in copied views. Dropping a zone silently disables every installed widget on that page. +19. Keep widget zones (`` or `@await Component.InvokeAsync("Widget", new { widgetZone = "..." })`) present in copied views. Dropping a zone silently disables every installed widget on that page. + A `` tag only works for a view component in an assembly the theme registers with `@addTagHelper`, which here is `Grand.Web.Common` (``). A theme cannot reference `Grand.Web` (`GRAND001`), so every other `` copied from a `Grand.Web` view, such as ``, renders as an empty unknown element with no build error. Rewrite those as `@await Component.InvokeAsync("RelatedProducts", new { productId = Model.Id })`. 20. A theme must not change view models, controller actions, or route definitions. If a theme needs different data, the change belongs in `Grand.Web`, not the theme. #### Assets diff --git a/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs b/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs index 7b6a30d16c..e83df6ba9e 100644 --- a/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs +++ b/src/Core/Grand.Infrastructure/Caching/Constants/CommonCacheKey.cs @@ -217,4 +217,18 @@ public static partial class CacheKey public static string PAGES_PATTERN_KEY => "Grand.pages."; #endregion + + #region Widgets + + /// + /// Key for widget info + /// + /// + /// {0} : current store ID + /// {1} : customer groups + /// {2} : widget zone + /// + public static string WIDGET_MODEL_KEY => "Grand.pres.widget-{0}-{1}-{2}"; + + #endregion } \ No newline at end of file diff --git a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml index ef3f289560..720dba9391 100644 --- a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml +++ b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml @@ -236,10 +236,10 @@ - - - - + @await Component.InvokeAsync("ProductsAlsoPurchased", new { productId = Model.Id }) + @await Component.InvokeAsync("RelatedProducts", new { productId = Model.Id }) + @await Component.InvokeAsync("SimilarProducts", new { productId = Model.Id }) + @await Component.InvokeAsync("RecommendedProducts", new { productId = Model.Id }) @if (Model.RenderCaptcha) { diff --git a/src/Tests/Grand.Web.Tests/Components/WidgetViewComponentTests.cs b/src/Tests/Grand.Web.Common.Tests/Components/WidgetViewComponentTests.cs similarity index 97% rename from src/Tests/Grand.Web.Tests/Components/WidgetViewComponentTests.cs rename to src/Tests/Grand.Web.Common.Tests/Components/WidgetViewComponentTests.cs index d2ba6c715e..2794a0b3d0 100644 --- a/src/Tests/Grand.Web.Tests/Components/WidgetViewComponentTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/Components/WidgetViewComponentTests.cs @@ -4,14 +4,14 @@ using Grand.Infrastructure.Caching; using Grand.Infrastructure.Configuration; using Grand.Mediator; -using Grand.Web.Components; +using Grand.Web.Common.Components; using Grand.Web.Models.Cms; using Microsoft.AspNetCore.Mvc.ViewComponents; using Microsoft.Extensions.Caching.Memory; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; -namespace Grand.Web.Tests.Components; +namespace Grand.Web.Common.Tests.Components; [TestClass] public class WidgetViewComponentTests diff --git a/src/Web/Grand.Web/Components/Widget.cs b/src/Web/Grand.Web.Common/Components/WidgetViewComponent.cs similarity index 90% rename from src/Web/Grand.Web/Components/Widget.cs rename to src/Web/Grand.Web.Common/Components/WidgetViewComponent.cs index 8cd3d58417..91f9a3a455 100644 --- a/src/Web/Grand.Web/Components/Widget.cs +++ b/src/Web/Grand.Web.Common/Components/WidgetViewComponent.cs @@ -2,12 +2,11 @@ using Grand.Domain.Customers; using Grand.Infrastructure; using Grand.Infrastructure.Caching; -using Grand.Web.Common.Components; -using Grand.Web.Events.Cache; +using Grand.Infrastructure.Caching.Constants; using Grand.Web.Models.Cms; using Microsoft.AspNetCore.Mvc; -namespace Grand.Web.Components; +namespace Grand.Web.Common.Components; public class WidgetViewComponent : BaseViewComponent { @@ -24,7 +23,7 @@ public WidgetViewComponent(IContextAccessor contextAccessor, ICacheBase cacheBas public async Task InvokeAsync(string widgetZone, object additionalData = null) { - var cacheKey = string.Format(CacheKeyConst.WIDGET_MODEL_KEY, _contextAccessor.StoreContext.CurrentStore.Id, + var cacheKey = string.Format(CacheKey.WIDGET_MODEL_KEY, _contextAccessor.StoreContext.CurrentStore.Id, string.Join(",", _contextAccessor.WorkContext.CurrentCustomer.GetCustomerGroupIds()), widgetZone); diff --git a/src/Web/Grand.Web.Models/Cms/WidgetModel.cs b/src/Web/Grand.Web.Common/Models/Cms/WidgetModel.cs similarity index 73% rename from src/Web/Grand.Web.Models/Cms/WidgetModel.cs rename to src/Web/Grand.Web.Common/Models/Cms/WidgetModel.cs index 0dcccd91cf..318b4debba 100644 --- a/src/Web/Grand.Web.Models/Cms/WidgetModel.cs +++ b/src/Web/Grand.Web.Common/Models/Cms/WidgetModel.cs @@ -1,5 +1,6 @@ using Grand.Infrastructure.Models; +//the namespace stays from Grand.Web.Models: storefront and theme _ViewImports import it namespace Grand.Web.Models.Cms; public class WidgetModel : BaseModel @@ -7,4 +8,4 @@ public class WidgetModel : BaseModel public string WidgetZone { get; set; } public string ViewComponentName { get; set; } public object AdditionalData { get; set; } -} \ No newline at end of file +} diff --git a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs index 7752ef63a2..b72d36efb6 100644 --- a/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs +++ b/src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs @@ -288,16 +288,6 @@ public static class CacheKeyConst /// public const string SITEMAP_PAGE_MODEL_KEY = "Grand.pres.sitemap.page-{0}-{1}-{2}"; - /// - /// Key for widget info - /// - /// - /// {0} : current store ID - /// {1} : customer groups - /// {2} : widget zone - /// - public const string WIDGET_MODEL_KEY = "Grand.pres.widget-{0}-{1}-{2}"; - /// /// Key for caching ///