From a465d8f9f615b2750e8a3b84bc289af5514d215f Mon Sep 17 00:00:00 2001 From: support Date: Sun, 25 Oct 2026 12:19:46 +0100 Subject: [PATCH 1/3] Render Nordic product-page view components by name #922 dropped the theme's reference to Grand.Web and its `@addTagHelper *, Grand.Web`, so `` tags for components that live in Grand.Web stopped compiling into tag helpers. Razor emitted them as unknown HTML elements: the productdetails_* widget zones and the also-purchased/related/similar/recommended sections never ran, and the empty elements took a full row inside .nordic-pd__main, pushing the product overview below the gallery. Component.InvokeAsync resolves the component at runtime, as the rest of the theme already does, so the theme keeps depending only on Grand.Web.Models. --- .../Product/ProductLayout.Grouped.cshtml | 4 ++-- .../Product/ProductLayout.Simple.cshtml | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml index 1038aa3f8..3cb4045fc 100644 --- a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml +++ b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml @@ -59,10 +59,10 @@
- + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_before_pictures", additionalData = Model.Id }) - + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_after_pictures", additionalData = Model.Id })
@if (!string.IsNullOrEmpty(Model.Flag)) { 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 ef3f28956..ddbfe8a76 100644 --- a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml +++ b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml @@ -52,18 +52,18 @@ } - +@await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_after_breadcrumb", additionalData = Model.Id })
- + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_top", additionalData = Model.Id })
- + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_before_pictures", additionalData = Model.Id }) - + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_after_pictures", additionalData = Model.Id }) @*Nordic: the info column in editorial order - brand, title, rating and codes, price, delivery notes, short description, choices, buy row, then a perks list*@
@@ -92,7 +92,7 @@
- + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_overview_top", additionalData = Model.Id })
@if (Model.TierPrices.Any()) @@ -192,7 +192,7 @@
} - + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_overview_bottom", additionalData = Model.Id })
@@ -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) { @@ -251,5 +251,5 @@ { @await Component.InvokeAsync("ProductEmailAFriend", new { productId = Model.Id }) } - + @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_bottom", additionalData = Model.Id })
\ No newline at end of file From 267d2aacc6c85834edd4bd5f48cedef82e386da6 Mon Sep 17 00:00:00 2001 From: support Date: Sun, 25 Oct 2026 13:05:35 +0100 Subject: [PATCH 2/3] Move WidgetViewComponent to Grand.Web.Common so works in themes #922 removed `@addTagHelper *, Grand.Web` from plugin and theme _ViewImports and made a plugin reference to Grand.Web a build error. View-component tag helpers are generated per registered assembly, so in any theme or plugin view compiled to a literal unknown element: its widget zones rendered nothing, with no build error. Every plugin and theme registers Grand.Web.Common, which is also an application part of the host, so the component works there for all of them. WidgetModel keeps its Grand.Web.Models.Cms namespace, which storefront and theme _ViewImports import, and the cache key moves to CacheKey. Nordic's widget zones go back to ; the product-list components still live in Grand.Web and stay invoked by name. --- .ai/skills/theme-creation.md | 3 ++- .../Caching/Constants/CommonCacheKey.cs | 14 ++++++++++++++ .../Nordic/Product/ProductLayout.Grouped.cshtml | 4 ++-- .../Nordic/Product/ProductLayout.Simple.cshtml | 14 +++++++------- .../Components/WidgetViewComponentTests.cs | 4 ++-- .../Components/WidgetViewComponent.cs} | 7 +++---- .../Models}/Cms/WidgetModel.cs | 3 ++- src/Web/Grand.Web/Events/Cache/CacheKeyConst.cs | 10 ---------- 8 files changed, 32 insertions(+), 27 deletions(-) rename src/Tests/{Grand.Web.Tests => Grand.Web.Common.Tests}/Components/WidgetViewComponentTests.cs (97%) rename src/Web/{Grand.Web/Components/Widget.cs => Grand.Web.Common/Components/WidgetViewComponent.cs} (90%) rename src/Web/{Grand.Web.Models => Grand.Web.Common/Models}/Cms/WidgetModel.cs (73%) diff --git a/.ai/skills/theme-creation.md b/.ai/skills/theme-creation.md index 64b662ce9..c7cda45d1 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 7b6a30d16..e83df6ba9 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.Grouped.cshtml b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml index 3cb4045fc..1038aa3f8 100644 --- a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml +++ b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Grouped.cshtml @@ -59,10 +59,10 @@
- @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_before_pictures", additionalData = Model.Id }) + - @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_after_pictures", additionalData = Model.Id }) +
@if (!string.IsNullOrEmpty(Model.Flag)) { 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 ddbfe8a76..720dba939 100644 --- a/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml +++ b/src/Plugins/Theme.Nordic/Views/Nordic/Product/ProductLayout.Simple.cshtml @@ -52,18 +52,18 @@ } -@await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_after_breadcrumb", additionalData = Model.Id }) +
- @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_top", additionalData = Model.Id }) +
- @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_before_pictures", additionalData = Model.Id }) + - @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_after_pictures", additionalData = Model.Id }) + @*Nordic: the info column in editorial order - brand, title, rating and codes, price, delivery notes, short description, choices, buy row, then a perks list*@
@@ -92,7 +92,7 @@
- @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_overview_top", additionalData = Model.Id }) +
@if (Model.TierPrices.Any()) @@ -192,7 +192,7 @@
} - @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_overview_bottom", additionalData = Model.Id }) +
@@ -251,5 +251,5 @@ { @await Component.InvokeAsync("ProductEmailAFriend", new { productId = Model.Id }) } - @await Component.InvokeAsync("Widget", new { widgetZone = "productdetails_bottom", additionalData = Model.Id }) +
\ No newline at end of file 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 d2ba6c715..2794a0b3d 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 8cd3d5841..91f9a3a45 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 0dcccd91c..318b4debb 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 7752ef63a..b72d36efb 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 /// From 952fe4bcab53de080bce5a2a7dc8b68343e1c555 Mon Sep 17 00:00:00 2001 From: support Date: Sun, 25 Oct 2026 13:45:16 +0100 Subject: [PATCH 3/3] Use store-widget for the Store panel's widget zones Eight Store panel views, copied from the Admin and Vendor panels, kept and . Grand.Web.Store registers only its own assembly and Grand.Web.Common with @addTagHelper, so those tags compiled to literal unknown elements and the zones rendered no widgets. StoreWidget takes the same widgetZone and additionalData. ViewComponentTagTests checks every in the repository against the assemblies its view registers, since Razor reports no error for a tag it cannot resolve. --- .../Views/ViewComponentTagTests.cs | 116 ++++++++++++++++++ .../Areas/Store/Views/Category/List.cshtml | 2 +- .../Partials/CreateOrUpdate.TabInfo.cshtml | 4 +- .../Store/Views/Home/AccessDenied.cshtml | 4 +- .../Areas/Store/Views/Product/List.cshtml | 2 +- .../AttributeCombinationPopup.cshtml | 2 +- .../Partials/Sales.TabLoyaltyPoints.cshtml | 4 +- .../Partials/Sales.TabShoppingCart.cshtml | 4 +- .../Views/Shared/Partials/UserFields.cshtml | 4 +- 9 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 src/Tests/Grand.Modules.Tests/Views/ViewComponentTagTests.cs diff --git a/src/Tests/Grand.Modules.Tests/Views/ViewComponentTagTests.cs b/src/Tests/Grand.Modules.Tests/Views/ViewComponentTagTests.cs new file mode 100644 index 000000000..61dd46d56 --- /dev/null +++ b/src/Tests/Grand.Modules.Tests/Views/ViewComponentTagTests.cs @@ -0,0 +1,116 @@ +using System.Text.RegularExpressions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Modules.Tests.Views; + +/// +/// Razor turns <vc:...> into a tag helper only for the view components of an assembly +/// the view registers with @addTagHelper. Any other <vc:...> compiles without a +/// warning and reaches the browser as an empty unknown element, so the component never runs. +/// +[TestClass] +public class ViewComponentTagTests +{ + private static readonly string Src = Path.Combine(Installer.RepositoryPaths.Root, "src"); + private static readonly HashSet SkippedFolders = ["bin", "obj", "node_modules", "wwwroot"]; + + [TestMethod] + public void EveryVcTagResolvesToAComponentItsViewRegisters() + { + var components = ComponentAssemblies(); + var problems = new List(); + var checkedTags = 0; + + foreach (var view in SourceFiles(Src, "*.cshtml")) + { + var tags = Regex.Matches(File.ReadAllText(view), " m.Groups[1].Value).Distinct().ToList(); + if (tags.Count == 0) continue; + + var project = ProjectOf(view); + var registered = RegisteredAssemblies(view, project.Dir); + foreach (var tag in tags) + { + checkedTags++; + if (!components.TryGetValue(tag, out var assemblies)) + problems.Add($"{Path.GetRelativePath(Src, view)}: matches no view component"); + else if (!assemblies.Overlaps(registered)) + problems.Add($"{Path.GetRelativePath(Src, view)}: lives in {string.Join(", ", assemblies)}, " + + $"which {project.Name} does not register with @addTagHelper"); + } + } + + Assert.IsTrue(checkedTags > 0, "No tag found, so the scan itself is broken"); + Assert.AreEqual(0, problems.Count, Environment.NewLine + string.Join(Environment.NewLine, problems)); + } + + [TestMethod] + public void ComponentScan_FindsWidgetComponentsOfEveryPanel() + { + var components = ComponentAssemblies(); + + CollectionAssert.AreEquivalent(new[] { "Grand.Web.Common" }, components["widget"].ToList()); + CollectionAssert.AreEquivalent(new[] { "Grand.Web.Admin" }, components["admin-widget"].ToList()); + CollectionAssert.AreEquivalent(new[] { "Grand.Web.Store" }, components["store-widget"].ToList()); + CollectionAssert.AreEquivalent(new[] { "Grand.Web.Vendor" }, components["vendor-widget"].ToList()); + CollectionAssert.AreEquivalent(new[] { "Grand.Web" }, components["related-products"].ToList()); + } + + //tag name -> assemblies that declare a view component with that name + private static Dictionary> ComponentAssemblies() + { + var declaration = new Regex( + @"(?:\[ViewComponent\(Name\s*=\s*""(?[^""]+)""\)\]\s*)?public\s+(?:sealed\s+)?class\s+(?\w+)(?:\s*:\s*(?\w+))?"); + var components = new Dictionary>(); + foreach (var file in SourceFiles(Src, "*.cs").Where(f => !f.Contains($"{Path.DirectorySeparatorChar}Tests{Path.DirectorySeparatorChar}"))) + foreach (Match m in declaration.Matches(File.ReadAllText(file))) + { + var className = m.Groups["class"].Value; + var isComponent = m.Groups["name"].Success || className.EndsWith("ViewComponent") || + m.Groups["base"].Value.EndsWith("ViewComponent"); + if (!isComponent) continue; + + var name = m.Groups["name"].Success ? m.Groups["name"].Value : className[..^"ViewComponent".Length]; + var tag = Regex.Replace(name, "(?<=[a-z0-9])(?=[A-Z])", "-").ToLowerInvariant(); + if (!components.TryGetValue(tag, out var assemblies)) + components[tag] = assemblies = []; + assemblies.Add(ProjectOf(file).Name); + } + + return components; + } + + private static HashSet RegisteredAssemblies(string view, string projectDir) + { + var assemblies = new HashSet(); + for (var dir = Path.GetDirectoryName(view); dir != null && dir.Length >= projectDir.Length; dir = Path.GetDirectoryName(dir)) + { + var imports = Path.Combine(dir, "_ViewImports.cshtml"); + if (!File.Exists(imports)) continue; + foreach (Match m in Regex.Matches(File.ReadAllText(imports), @"@addTagHelper\s+\*\s*,\s*([\w.]+)")) + assemblies.Add(m.Groups[1].Value); + } + + return assemblies; + } + + private static (string Dir, string Name) ProjectOf(string file) + { + for (var dir = Path.GetDirectoryName(file); dir != null; dir = Path.GetDirectoryName(dir)) + { + var project = Directory.GetFiles(dir, "*.csproj").FirstOrDefault(); + if (project != null) return (dir, Path.GetFileNameWithoutExtension(project)); + } + + throw new InvalidOperationException($"{file} belongs to no project"); + } + + private static IEnumerable SourceFiles(string dir, string pattern) + { + foreach (var file in Directory.GetFiles(dir, pattern)) + yield return file; + foreach (var sub in Directory.GetDirectories(dir).Where(d => !SkippedFolders.Contains(Path.GetFileName(d)))) + foreach (var file in SourceFiles(sub, pattern)) + yield return file; + } +} diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Category/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Category/List.cshtml index 7aca2ef9a..94cb93ec9 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Category/List.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Category/List.cshtml @@ -8,7 +8,7 @@ store's own) and no export or import. The ids the script below looks up are unchanged. *@ - + @Loc["Admin.Common.AddNew"] diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Category/Partials/CreateOrUpdate.TabInfo.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Category/Partials/CreateOrUpdate.TabInfo.cshtml index d81e54a70..94097d26f 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Category/Partials/CreateOrUpdate.TabInfo.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Category/Partials/CreateOrUpdate.TabInfo.cshtml @@ -44,7 +44,7 @@ it is shown. The field order is the one this tab always had, except the picture, last because its upload box is the tallest control. The ids the script above shows and hides - pnlSearchBoxOrder, pnlPageSize, pnlPageSizeOptions - sit on the same fields as before. *@ - +
@@ -110,4 +110,4 @@
- + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Home/AccessDenied.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Home/AccessDenied.cshtml index d9c96057f..511e985f3 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Home/AccessDenied.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Home/AccessDenied.cshtml @@ -1,6 +1,6 @@  - + @* the panel's empty state: nothing here the account may see *@
@@ -8,5 +8,5 @@

@Loc["Admin.AccessDenied.Description"]

- +
diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Product/List.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Product/List.cshtml index c0ba12179..1a0008aa5 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Product/List.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Product/List.cshtml @@ -10,7 +10,7 @@ - + @Loc["Admin.Common.AddNew"] diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttributeCombination/AttributeCombinationPopup.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttributeCombination/AttributeCombinationPopup.cshtml index 1b454ab35..b6187c6c0 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttributeCombination/AttributeCombinationPopup.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttributeCombination/AttributeCombinationPopup.cshtml @@ -131,7 +131,7 @@
- + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabLoyaltyPoints.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabLoyaltyPoints.cshtml index a0998ea2d..62bdd9a67 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabLoyaltyPoints.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabLoyaltyPoints.cshtml @@ -1,6 +1,6 @@ @model SalesSettingsModel - + @* the program and what a point is worth, then how points are earned; the rate and the purchase rule read as the sentences they are, with the fields inside them *@ @@ -38,4 +38,4 @@
- + diff --git a/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabShoppingCart.cshtml b/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabShoppingCart.cshtml index 646ad0d3c..e4d8b9d49 100644 --- a/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabShoppingCart.cshtml +++ b/src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabShoppingCart.cshtml @@ -1,6 +1,6 @@ @model SalesSettingsModel - + @* what adding to the cart or wishlist does and how many items they hold, what the cart shows, the wishlist e-mail and the mini cart with what they reveal, then the cart's behaviour *@ @@ -50,7 +50,7 @@
- + - \ No newline at end of file + \ No newline at end of file