Skip to content

Commit 195e0af

Browse files
Use store-widget for the Store panel's widget zones (#994)
1 parent d110aba commit 195e0af

9 files changed

Lines changed: 129 additions & 13 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
using System.Text.RegularExpressions;
2+
using Microsoft.VisualStudio.TestTools.UnitTesting;
3+
4+
namespace Grand.Modules.Tests.Views;
5+
6+
/// <summary>
7+
/// Razor turns <c>&lt;vc:...&gt;</c> into a tag helper only for the view components of an assembly
8+
/// the view registers with <c>@addTagHelper</c>. Any other <c>&lt;vc:...&gt;</c> compiles without a
9+
/// warning and reaches the browser as an empty unknown element, so the component never runs.
10+
/// </summary>
11+
[TestClass]
12+
public class ViewComponentTagTests
13+
{
14+
private static readonly string Src = Path.Combine(Installer.RepositoryPaths.Root, "src");
15+
private static readonly HashSet<string> SkippedFolders = ["bin", "obj", "node_modules", "wwwroot"];
16+
17+
[TestMethod]
18+
public void EveryVcTagResolvesToAComponentItsViewRegisters()
19+
{
20+
var components = ComponentAssemblies();
21+
var problems = new List<string>();
22+
var checkedTags = 0;
23+
24+
foreach (var view in SourceFiles(Src, "*.cshtml"))
25+
{
26+
var tags = Regex.Matches(File.ReadAllText(view), "<vc:([a-z0-9-]+)")
27+
.Select(m => m.Groups[1].Value).Distinct().ToList();
28+
if (tags.Count == 0) continue;
29+
30+
var project = ProjectOf(view);
31+
var registered = RegisteredAssemblies(view, project.Dir);
32+
foreach (var tag in tags)
33+
{
34+
checkedTags++;
35+
if (!components.TryGetValue(tag, out var assemblies))
36+
problems.Add($"{Path.GetRelativePath(Src, view)}: <vc:{tag}> matches no view component");
37+
else if (!assemblies.Overlaps(registered))
38+
problems.Add($"{Path.GetRelativePath(Src, view)}: <vc:{tag}> lives in {string.Join(", ", assemblies)}, " +
39+
$"which {project.Name} does not register with @addTagHelper");
40+
}
41+
}
42+
43+
Assert.IsTrue(checkedTags > 0, "No <vc:...> tag found, so the scan itself is broken");
44+
Assert.AreEqual(0, problems.Count, Environment.NewLine + string.Join(Environment.NewLine, problems));
45+
}
46+
47+
[TestMethod]
48+
public void ComponentScan_FindsWidgetComponentsOfEveryPanel()
49+
{
50+
var components = ComponentAssemblies();
51+
52+
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Common" }, components["widget"].ToList());
53+
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Admin" }, components["admin-widget"].ToList());
54+
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Store" }, components["store-widget"].ToList());
55+
CollectionAssert.AreEquivalent(new[] { "Grand.Web.Vendor" }, components["vendor-widget"].ToList());
56+
CollectionAssert.AreEquivalent(new[] { "Grand.Web" }, components["related-products"].ToList());
57+
}
58+
59+
//tag name -> assemblies that declare a view component with that name
60+
private static Dictionary<string, HashSet<string>> ComponentAssemblies()
61+
{
62+
var declaration = new Regex(
63+
@"(?:\[ViewComponent\(Name\s*=\s*""(?<name>[^""]+)""\)\]\s*)?public\s+(?:sealed\s+)?class\s+(?<class>\w+)(?:\s*:\s*(?<base>\w+))?");
64+
var components = new Dictionary<string, HashSet<string>>();
65+
foreach (var file in SourceFiles(Src, "*.cs").Where(f => !f.Contains($"{Path.DirectorySeparatorChar}Tests{Path.DirectorySeparatorChar}")))
66+
foreach (Match m in declaration.Matches(File.ReadAllText(file)))
67+
{
68+
var className = m.Groups["class"].Value;
69+
var isComponent = m.Groups["name"].Success || className.EndsWith("ViewComponent") ||
70+
m.Groups["base"].Value.EndsWith("ViewComponent");
71+
if (!isComponent) continue;
72+
73+
var name = m.Groups["name"].Success ? m.Groups["name"].Value : className[..^"ViewComponent".Length];
74+
var tag = Regex.Replace(name, "(?<=[a-z0-9])(?=[A-Z])", "-").ToLowerInvariant();
75+
if (!components.TryGetValue(tag, out var assemblies))
76+
components[tag] = assemblies = [];
77+
assemblies.Add(ProjectOf(file).Name);
78+
}
79+
80+
return components;
81+
}
82+
83+
private static HashSet<string> RegisteredAssemblies(string view, string projectDir)
84+
{
85+
var assemblies = new HashSet<string>();
86+
for (var dir = Path.GetDirectoryName(view); dir != null && dir.Length >= projectDir.Length; dir = Path.GetDirectoryName(dir))
87+
{
88+
var imports = Path.Combine(dir, "_ViewImports.cshtml");
89+
if (!File.Exists(imports)) continue;
90+
foreach (Match m in Regex.Matches(File.ReadAllText(imports), @"@addTagHelper\s+\*\s*,\s*([\w.]+)"))
91+
assemblies.Add(m.Groups[1].Value);
92+
}
93+
94+
return assemblies;
95+
}
96+
97+
private static (string Dir, string Name) ProjectOf(string file)
98+
{
99+
for (var dir = Path.GetDirectoryName(file); dir != null; dir = Path.GetDirectoryName(dir))
100+
{
101+
var project = Directory.GetFiles(dir, "*.csproj").FirstOrDefault();
102+
if (project != null) return (dir, Path.GetFileNameWithoutExtension(project));
103+
}
104+
105+
throw new InvalidOperationException($"{file} belongs to no project");
106+
}
107+
108+
private static IEnumerable<string> SourceFiles(string dir, string pattern)
109+
{
110+
foreach (var file in Directory.GetFiles(dir, pattern))
111+
yield return file;
112+
foreach (var sub in Directory.GetDirectories(dir).Where(d => !SkippedFolders.Contains(Path.GetFileName(d))))
113+
foreach (var file in SourceFiles(sub, pattern))
114+
yield return file;
115+
}
116+
}

‎src/Web/Grand.Web.Store/Areas/Store/Views/Category/List.cshtml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
store's own) and no export or import. The ids the script below looks up are unchanged. *@
99
<admin-page title="@Loc["Admin.Catalog.Categories.Manage"]" icon="bi-diagram-3">
1010
<page-actions>
11-
<vc:admin-widget widget-zone="category_list_buttons" additional-data="null"/>
11+
<vc:store-widget widget-zone="category_list_buttons" additional-data="null"/>
1212
<a href="@Url.Action("Create", new { area = Constants.AreaStore })" class="btn btn-primary">
1313
<i class="bi bi-plus-lg"></i> @Loc["Admin.Common.AddNew"]
1414
</a>

‎src/Web/Grand.Web.Store/Areas/Store/Views/Category/Partials/CreateOrUpdate.TabInfo.cshtml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
it is shown. The field order is the one this tab always had, except the picture, last
4545
because its upload box is the tallest control. The ids the script above shows and hides -
4646
pnlSearchBoxOrder, pnlPageSize, pnlPageSizeOptions - sit on the same fields as before. *@
47-
<vc:admin-widget widget-zone="category_details_info_top" additional-data="Model"/>
47+
<vc:store-widget widget-zone="category_details_info_top" additional-data="Model"/>
4848
<admin-card>
4949
<localized-editor localized-template="@template" name="category-info-localized" language-ids="@Model.Locales.Select(c => c.LanguageId).ToList()">
5050
<div class="grand-fields">
@@ -110,4 +110,4 @@
110110
<admin-field asp-for="ExternalId"/>
111111
</div>
112112
</admin-card>
113-
<vc:admin-widget widget-zone="category_details_info_bottom" additional-data="Model"/>
113+
<vc:store-widget widget-zone="category_details_info_bottom" additional-data="Model"/>
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<input id="active-menu-item" type="hidden" value="/Store"/>
22
<admin-page title="@Loc["Admin.AccessDenied.Title"]" icon="bi-shield-lock">
3-
<vc:vendor-widget widget-zone="store_security_access_denied_top" additional-data="null"/>
3+
<vc:store-widget widget-zone="store_security_access_denied_top" additional-data="null"/>
44
@* the panel's empty state: nothing here the account may see *@
55
<admin-card>
66
<div class="grand-empty">
77
<i class="bi bi-shield-lock grand-empty__icon"></i>
88
<p class="mb-0">@Loc["Admin.AccessDenied.Description"]</p>
99
</div>
1010
</admin-card>
11-
<vc:vendor-widget widget-zone="store_security_access_denied_bottom" additional-data="null"/>
11+
<vc:store-widget widget-zone="store_security_access_denied_bottom" additional-data="null"/>
1212
</admin-page>

‎src/Web/Grand.Web.Store/Areas/Store/Views/Product/List.cshtml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<form asp-area="@Constants.AreaStore" asp-controller="Product" asp-action="List" method="post">
1111
<admin-page title="@Loc["Admin.Catalog.Products.Manage"]" icon="bi-boxes">
1212
<page-actions>
13-
<vc:admin-widget widget-zone="product_list_buttons" additional-data="null"/>
13+
<vc:store-widget widget-zone="product_list_buttons" additional-data="null"/>
1414
<a href="@Url.Action("Create", new { area = Constants.AreaStore })" class="btn btn-primary">
1515
<i class="bi bi-plus-lg"></i> @Loc["Admin.Common.AddNew"]
1616
</a>

‎src/Web/Grand.Web.Store/Areas/Store/Views/ProductAttributeCombination/AttributeCombinationPopup.cshtml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
</admin-field>
132132

133133
<div class="grand-popup__actions">
134-
<vc:admin-widget widget-zone="product_details_attribute_combination_buttons" additional-data="Model"/>
134+
<vc:store-widget widget-zone="product_details_attribute_combination_buttons" additional-data="Model"/>
135135
<button class="btn btn-primary" type="submit" name="save">
136136
<i class="bi bi-check-lg"></i> @Loc["Admin.Common.Save"]
137137
</button>

‎src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabLoyaltyPoints.cshtml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@model SalesSettingsModel
22

3-
<vc:admin-widget widget-zone="loyalty_points_settings_top" additional-data="null"/>
3+
<vc:store-widget widget-zone="loyalty_points_settings_top" additional-data="null"/>
44
@* the program and what a point is worth, then how points are earned; the rate and the purchase
55
rule read as the sentences they are, with the fields inside them *@
66
<admin-card>
@@ -38,4 +38,4 @@
3838
<admin-field asp-for="LoyaltyPointsSettings.DisplayHowMuchWillBeEarned"/>
3939
</div>
4040
</admin-card>
41-
<vc:admin-widget widget-zone="loyalty_points_settings_bottom" additional-data="null"/>
41+
<vc:store-widget widget-zone="loyalty_points_settings_bottom" additional-data="null"/>

‎src/Web/Grand.Web.Store/Areas/Store/Views/Setting/Partials/Sales.TabShoppingCart.cshtml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@model SalesSettingsModel
22

3-
<vc:admin-widget widget-zone="shopping_cart_settings_top" additional-data="null"/>
3+
<vc:store-widget widget-zone="shopping_cart_settings_top" additional-data="null"/>
44
@* what adding to the cart or wishlist does and how many items they hold, what the cart shows,
55
the wishlist e-mail and the mini cart with what they reveal, then the cart's behaviour *@
66
<admin-card>
@@ -50,7 +50,7 @@
5050
<admin-field asp-for="ShoppingCartSettings.ReservationDateFormat" size="md"/>
5151
</div>
5252
</admin-card>
53-
<vc:admin-widget widget-zone="shopping_cart_settings_bottom" additional-data="null"/>
53+
<vc:store-widget widget-zone="shopping_cart_settings_bottom" additional-data="null"/>
5454

5555
<script>
5656
$(document).ready(function () {

‎src/Web/Grand.Web.Store/Areas/Store/Views/Shared/Partials/UserFields.cshtml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
Value = contextAccessor.WorkContext.StoreManager.Id
1414
});
1515
}
16-
<vc:admin-widget widget-zone="userfield_details_info_top" additional-data="Model"/>
16+
<vc:store-widget widget-zone="userfield_details_info_top" additional-data="Model"/>
1717
@* one card: the add form on top, the fields under it - addUserFields() appends a new one to
1818
#form-userfield, so it lands right below the form; every row keeps .form-body, which
1919
deleteUserFields() renumbers by, and one text box, one hidden key and one select *@
@@ -133,4 +133,4 @@
133133
});
134134
}
135135
</script>
136-
<vc:admin-widget widget-zone="userfield_details_info_bottom" additional-data="Model"/>
136+
<vc:store-widget widget-zone="userfield_details_info_bottom" additional-data="Model"/>

0 commit comments

Comments
 (0)