diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs index 9a7865748bf..bf6c6c697ef 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeDriver.cs @@ -1,17 +1,17 @@ -using OrchardCore.AdminMenu.Services; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Navigation; +using OrchardCore.Security.Permissions; namespace OrchardCore.AdminMenu.AdminNodes; public sealed class LinkAdminNodeDriver : DisplayDriver { - private readonly IAdminMenuPermissionService _adminMenuPermissionService; + private readonly IPermissionService _permissionService; - public LinkAdminNodeDriver(IAdminMenuPermissionService adminMenuPermissionService) + public LinkAdminNodeDriver(IPermissionService permissionService) { - _adminMenuPermissionService = adminMenuPermissionService; + _permissionService = permissionService; } public override Task DisplayAsync(LinkAdminNode treeNode, BuildDisplayContext context) @@ -31,22 +31,23 @@ public override IDisplayResult Edit(LinkAdminNode treeNode, BuildEditorContext c model.IconClass = treeNode.IconClass; model.Target = treeNode.Target; - var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); - - var selectedPermissions = permissions.Where(p => treeNode.PermissionNames.Contains(p.Name)); + var selectedPermissions = await _permissionService.FindByNamesAsync(treeNode.PermissionNames); model.SelectedItems = selectedPermissions .Select(p => new PermissionViewModel { Name = p.Name, DisplayText = p.Description - }).ToList(); + }).ToArray(); + + var permissions = await _permissionService.GetPermissionsAsync(); + model.AllItems = permissions .Select(p => new PermissionViewModel { Name = p.Name, DisplayText = p.Description - }).ToList(); + }).ToArray(); }).Location("Content"); } @@ -69,10 +70,8 @@ await context.Updater.TryUpdateModelAsync(model, Prefix, ? [] : model.SelectedPermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries); - var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); - treeNode.PermissionNames = permissions - .Where(p => selectedPermissions.Contains(p.Name)) - .Select(p => p.Name).ToArray(); + var permissions = await _permissionService.FindByNamesAsync(selectedPermissions); + treeNode.PermissionNames = permissions.Select(p => p.Name).ToArray(); return Edit(treeNode, context); } diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs index d698d399906..28f495d89a6 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/LinkAdminNodeNavigationBuilder.cs @@ -4,22 +4,22 @@ using OrchardCore.Admin; using OrchardCore.AdminMenu.Services; using OrchardCore.Navigation; +using OrchardCore.Security.Permissions; namespace OrchardCore.AdminMenu.AdminNodes; public class LinkAdminNodeNavigationBuilder : IAdminNodeNavigationBuilder { private readonly ILogger _logger; - private readonly IAdminMenuPermissionService _adminMenuPermissionService; + private readonly IPermissionService _permissionService; private readonly AdminOptions _adminOptions; - public LinkAdminNodeNavigationBuilder( - IAdminMenuPermissionService adminMenuPermissionService, + IPermissionService permissionService, IOptions adminOptions, ILogger logger) { - _adminMenuPermissionService = adminMenuPermissionService; + _permissionService = permissionService; _adminOptions = adminOptions.Value; _logger = logger; } @@ -61,11 +61,8 @@ public Task BuildNavigationAsync(MenuItem menuItem, NavigationBuilder builder, I if (node.PermissionNames.Length > 0) { - var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); - // Find the actual permissions and apply them to the menu. - var selectedPermissions = permissions.Where(p => node.PermissionNames.Contains(p.Name)); - itemBuilder.Permissions(selectedPermissions); + itemBuilder.Permissions(await _permissionService.FindByNamesAsync(node.PermissionNames)); } // Add adminNode's IconClass property values to menuItem.Classes. diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeDriver.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeDriver.cs index 1d627b89147..d6f9fa45b82 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeDriver.cs @@ -1,17 +1,17 @@ -using OrchardCore.AdminMenu.Services; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Navigation; +using OrchardCore.Security.Permissions; namespace OrchardCore.AdminMenu.AdminNodes; public sealed class PlaceholderAdminNodeDriver : DisplayDriver { - private readonly IAdminMenuPermissionService _adminMenuPermissionService; + private readonly IPermissionService _permissionService; - public PlaceholderAdminNodeDriver(IAdminMenuPermissionService adminMenuPermissionService) + public PlaceholderAdminNodeDriver(IPermissionService permissionService) { - _adminMenuPermissionService = adminMenuPermissionService; + _permissionService = permissionService; } public override Task DisplayAsync(PlaceholderAdminNode treeNode, BuildDisplayContext context) @@ -29,22 +29,23 @@ public override IDisplayResult Edit(PlaceholderAdminNode treeNode, BuildEditorCo model.LinkText = treeNode.LinkText; model.IconClass = treeNode.IconClass; - var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); - - var selectedPermissions = permissions.Where(p => treeNode.PermissionNames.Contains(p.Name)); + var selectedPermissions = await _permissionService.FindByNamesAsync(treeNode.PermissionNames); model.SelectedItems = selectedPermissions .Select(p => new PermissionViewModel { Name = p.Name, DisplayText = p.Description - }).ToList(); + }).ToArray(); + + var permissions = await _permissionService.GetPermissionsAsync(); + model.AllItems = permissions .Select(p => new PermissionViewModel { Name = p.Name, DisplayText = p.Description - }).ToList(); + }).ToArray(); }).Location("Content"); } @@ -59,11 +60,13 @@ await context.Updater.TryUpdateModelAsync(model, Prefix, treeNode.LinkText = model.LinkText; treeNode.IconClass = model.IconClass; - var selectedPermissions = (model.SelectedPermissionNames == null ? Array.Empty() : model.SelectedPermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries)); - var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); - treeNode.PermissionNames = permissions - .Where(p => selectedPermissions.Contains(p.Name)) - .Select(p => p.Name).ToArray(); + var selectedPermissions = + model.SelectedPermissionNames == null + ? [] + : model.SelectedPermissionNames.Split(',', StringSplitOptions.RemoveEmptyEntries); + + var permissions = await _permissionService.FindByNamesAsync(selectedPermissions); + treeNode.PermissionNames = permissions.Select(p => p.Name).ToArray(); return Edit(treeNode, context); } diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeNavigationBuilder.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeNavigationBuilder.cs index 2847581c7a0..23a4aef90e5 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeNavigationBuilder.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/AdminNodes/PlaceholderAdminNodeNavigationBuilder.cs @@ -2,17 +2,20 @@ using Microsoft.Extensions.Logging; using OrchardCore.AdminMenu.Services; using OrchardCore.Navigation; +using OrchardCore.Security.Permissions; namespace OrchardCore.AdminMenu.AdminNodes; public class PlaceholderAdminNodeNavigationBuilder : IAdminNodeNavigationBuilder { private readonly ILogger _logger; - private readonly IAdminMenuPermissionService _adminMenuPermissionService; + private readonly IPermissionService _permissionService; - public PlaceholderAdminNodeNavigationBuilder(IAdminMenuPermissionService adminMenuPermissionService, ILogger logger) + public PlaceholderAdminNodeNavigationBuilder( + IPermissionService permissionService, + ILogger logger) { - _adminMenuPermissionService = adminMenuPermissionService; + _permissionService = permissionService; _logger = logger; } @@ -34,10 +37,8 @@ public Task BuildNavigationAsync(MenuItem menuItem, NavigationBuilder builder, I if (node.PermissionNames.Length > 0) { - var permissions = await _adminMenuPermissionService.GetPermissionsAsync(); // Find the actual permissions and apply them to the menu. - var selectedPermissions = permissions.Where(p => node.PermissionNames.Contains(p.Name)); - itemBuilder.Permissions(selectedPermissions); + itemBuilder.Permissions(await _permissionService.FindByNamesAsync(node.PermissionNames)); } // Add adminNode's IconClass property values to menuItem.Classes. diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Services/AdminMenuPermissionService.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Services/AdminMenuPermissionService.cs index 21de676215b..db9ca789eaa 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Services/AdminMenuPermissionService.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Services/AdminMenuPermissionService.cs @@ -2,35 +2,17 @@ namespace OrchardCore.AdminMenu.Services; +[Obsolete("This service is obsolete and will be removed in version 4. Instead, please use IPermissionService")] public sealed class AdminMenuPermissionService : IAdminMenuPermissionService { - private readonly IEnumerable _permissionProviders; + private readonly IPermissionService _permissionService; - // Cached per request. - private List _permissions; - - public AdminMenuPermissionService(IEnumerable permissionProviders) + public AdminMenuPermissionService(IPermissionService permissionService) { - _permissionProviders = permissionProviders; + _permissionService = permissionService; } public async Task> GetPermissionsAsync() - { - if (_permissions != null) - { - return _permissions; - } - - _permissions = []; - - foreach (var permissionProvider in _permissionProviders) - { - var permissions = await permissionProvider.GetPermissionsAsync(); - - _permissions.AddRange(permissions); - } - - return _permissions; - } + => await _permissionService.GetPermissionsAsync(); } diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Startup.cs b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Startup.cs index 74e1b7fa0e9..af4fdf45d9b 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Startup.cs @@ -17,7 +17,10 @@ public override void ConfigureServices(IServiceCollection services) { services.AddPermissionProvider(); services.AddNavigationProvider(); + +#pragma warning disable CS0618 // Type or member is obsolete services.AddScoped(); +#pragma warning restore CS0618 // Type or member is obsolete services.AddScoped(); services.AddScoped(); diff --git a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml index dd5c12bac83..ab4e8b9d256 100644 --- a/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.AdminMenu/Views/Items/LinkAdminNode.Fields.TreeEdit.cshtml @@ -113,19 +113,19 @@
- + @@ -137,7 +137,11 @@
- + + + + + + +
+ + + +
+ +
+
    +
  • +
    {{ item.displayText }}
    + +
    + +
    +
  • +
+
+ +
+
+ + + + + + @T["The permissions required to display this menu item (optional)."] +
+
+
+
+ + diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/wwwroot/Scripts/menu-permission-picker.js b/src/OrchardCore.Modules/OrchardCore.Menu/wwwroot/Scripts/menu-permission-picker.js new file mode 100644 index 00000000000..6eaa7db0266 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Menu/wwwroot/Scripts/menu-permission-picker.js @@ -0,0 +1,54 @@ +/* +** NOTE: This file is generated by Gulp and should not be edited directly! +** Any changes made directly to this file will be overwritten next time its asset group is processed by Gulp. +*/ + +function initMenuPermissionsPicker(element) { + // only run script if element exists + if (element) { + var elementId = element.id; + var selectedItems = JSON.parse(element.dataset.selectedItems || "[]"); + var allItems = JSON.parse(element.dataset.allItems || "[]"); + var vueMultiselect = Vue.component('vue-multiselect', window.VueMultiselect["default"]); + var vm = new Vue({ + el: '#' + elementId, + components: { + 'vue-multiselect': vueMultiselect + }, + data: { + value: null, + arrayOfItems: selectedItems, + options: allItems + }, + computed: { + selectedNames: function selectedNames() { + return this.arrayOfItems.map(function (x) { + return x.name; + }).join(','); + } + }, + methods: { + onSelect: function onSelect(selectedOption, name) { + var self = this; + for (i = 0; i < self.arrayOfItems.length; i++) { + if (self.arrayOfItems[i].name === selectedOption.name) { + return; + } + } + self.arrayOfItems.push(selectedOption); + }, + remove: function remove(item) { + this.arrayOfItems.splice(this.arrayOfItems.indexOf(item), 1); + } + } + }); + + /*Hook for other scripts that might want to have access to the view model*/ + var event = new CustomEvent("menu-permission-picker-created", { + detail: { + vm: vm + } + }); + document.querySelector("body").dispatchEvent(event); + } +} \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/wwwroot/Scripts/menu-permission-picker.min.js b/src/OrchardCore.Modules/OrchardCore.Menu/wwwroot/Scripts/menu-permission-picker.min.js new file mode 100644 index 00000000000..091fcc49fba --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Menu/wwwroot/Scripts/menu-permission-picker.min.js @@ -0,0 +1 @@ +function initMenuPermissionsPicker(e){if(e){var t=e.id,n=JSON.parse(e.dataset.selectedItems||"[]"),a=JSON.parse(e.dataset.allItems||"[]"),s=Vue.component("vue-multiselect",window.VueMultiselect.default),r=new Vue({el:"#"+t,components:{"vue-multiselect":s},data:{value:null,arrayOfItems:n,options:a},computed:{selectedNames:function(){return this.arrayOfItems.map((function(e){return e.name})).join(",")}},methods:{onSelect:function(e,t){var n=this;for(i=0;i {{ Site.SiteName }} - {% shape "menu", alias: "alias:main-menu", cache_id: "main-menu", cache_tag: "alias:main-menu" %} + {% shape "menu", alias: "alias:main-menu", cache_id: "main-menu", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu", cache_context: "user.roles" %} {% render_section "Header", required: false %} diff --git a/src/OrchardCore.Themes/TheBlogTheme/Views/Layout.liquid b/src/OrchardCore.Themes/TheBlogTheme/Views/Layout.liquid index eef06ae255c..74e53d52854 100644 --- a/src/OrchardCore.Themes/TheBlogTheme/Views/Layout.liquid +++ b/src/OrchardCore.Themes/TheBlogTheme/Views/Layout.liquid @@ -29,7 +29,7 @@ {{ "Toggle navigation" | t }} {{ "Menu" | t }} - {% shape "menu", alias: "alias:main-menu", cache_id: "main-menu", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu" %} + {% shape "menu", alias: "alias:main-menu", cache_id: "main-menu", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu", cache_context: "user.roles" %} {% render_section "Header", required: false %} diff --git a/src/OrchardCore.Themes/TheTheme/Views/Layout.cshtml b/src/OrchardCore.Themes/TheTheme/Views/Layout.cshtml index 6c1dc1a97d3..52b028b25d9 100644 --- a/src/OrchardCore.Themes/TheTheme/Views/Layout.cshtml +++ b/src/OrchardCore.Themes/TheTheme/Views/Layout.cshtml @@ -50,7 +50,7 @@ diff --git a/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-ContentMenuItem.cshtml b/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-ContentMenuItem.cshtml index 5e8ff4310d5..0ff0d1636ad 100644 --- a/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-ContentMenuItem.cshtml +++ b/src/OrchardCore.Themes/TheTheme/Views/MenuItemLink-ContentMenuItem.cshtml @@ -1,13 +1,27 @@ +@using Microsoft.AspNetCore.Authorization +@using OrchardCore.Contents + +@inject IContentManager ContentManager +@inject IAuthorizationService AuthorizationService @inject IOptions AutorouteOptions + @{ ContentItem contentItem = Model.ContentItem; + var menuItemPart = contentItem.As(); + + string contentItemId = menuItemPart.ContentItem.Content.ContentMenuItemPart.SelectedContentItem.ContentItemIds[0]; + var routeValues = new RouteValueDictionary(AutorouteOptions.Value.GlobalRouteValues); + routeValues[AutorouteOptions.Value.ContentItemIdKey] = menuItemPart.ContentItem.Content.ContentMenuItemPart.SelectedContentItem.ContentItemIds[0]; + var linkUrl = Url.RouteUrl(routeValues); TagBuilder tag = Tag(Model, "a"); + tag.Attributes["href"] = linkUrl; + tag.InnerHtml.Append(contentItem.DisplayText); if (Model.Level == 0 && Model.HasItems) diff --git a/src/OrchardCore/OrchardCore.AdminMenu.Abstractions/Services/IAdminMenuPermissionService.cs b/src/OrchardCore/OrchardCore.AdminMenu.Abstractions/Services/IAdminMenuPermissionService.cs index e6c0d017eba..d03dedf9149 100644 --- a/src/OrchardCore/OrchardCore.AdminMenu.Abstractions/Services/IAdminMenuPermissionService.cs +++ b/src/OrchardCore/OrchardCore.AdminMenu.Abstractions/Services/IAdminMenuPermissionService.cs @@ -2,6 +2,7 @@ namespace OrchardCore.AdminMenu.Services; +[Obsolete("This service is obsolete and will be removed in version 4. Instead, please use IPermissionService")] public interface IAdminMenuPermissionService { Task> GetPermissionsAsync(); diff --git a/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/DefaultPermissionService.cs b/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/DefaultPermissionService.cs new file mode 100644 index 00000000000..d3e350d774a --- /dev/null +++ b/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/DefaultPermissionService.cs @@ -0,0 +1,56 @@ +namespace OrchardCore.Security.Permissions; + +public sealed class DefaultPermissionService : IPermissionService +{ + private readonly IEnumerable _permissionProviders; + + // Cached per request. + private Dictionary _permissions; + + public DefaultPermissionService(IEnumerable permissionProviders) + { + _permissionProviders = permissionProviders; + } + + public async ValueTask FindByNameAsync(string name) + { + ArgumentException.ThrowIfNullOrEmpty(name); + + if (_permissions is null) + { + await LoadPermissionsAsync(); + } + + if (_permissions.TryGetValue(name, out var permission)) + { + return permission; + } + + return null; + } + + public async ValueTask> GetPermissionsAsync() + { + if (_permissions == null) + { + await LoadPermissionsAsync(); + } + + return _permissions.Values; + } + + private async Task LoadPermissionsAsync() + { + _permissions = []; + + foreach (var permissionProvider in _permissionProviders) + { + var permissions = await permissionProvider.GetPermissionsAsync(); + + foreach (var permission in permissions) + { + _permissions[permission.Name] = permission; + } + } + } +} diff --git a/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/IPermissionService.cs b/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/IPermissionService.cs new file mode 100644 index 00000000000..49f0485c770 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/IPermissionService.cs @@ -0,0 +1,8 @@ +namespace OrchardCore.Security.Permissions; + +public interface IPermissionService +{ + ValueTask FindByNameAsync(string name); + + ValueTask> GetPermissionsAsync(); +} diff --git a/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/PermissionProviderExtensions.cs b/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/PermissionProviderExtensions.cs new file mode 100644 index 00000000000..f98a6386239 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Permissions/PermissionProviderExtensions.cs @@ -0,0 +1,23 @@ +namespace OrchardCore.Security.Permissions; + +public static class PermissionProviderExtensions +{ + public static async ValueTask> FindByNamesAsync(this IPermissionService permissionService, IEnumerable names) + { + ArgumentNullException.ThrowIfNull(names); + + var selectedPermissions = new List(); + + foreach (var name in names) + { + var permission = await permissionService.FindByNameAsync(name); + + if (permission != null) + { + selectedPermissions.Add(permission); + } + } + + return selectedPermissions; + } +} diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Security/OrchardCoreBuilderExtensions.cs b/src/OrchardCore/OrchardCore.Infrastructure/Security/OrchardCoreBuilderExtensions.cs index 1a4320ceaec..1b43d450587 100644 --- a/src/OrchardCore/OrchardCore.Infrastructure/Security/OrchardCoreBuilderExtensions.cs +++ b/src/OrchardCore/OrchardCore.Infrastructure/Security/OrchardCoreBuilderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization; using OrchardCore.Security; using OrchardCore.Security.AuthorizationHandlers; +using OrchardCore.Security.Permissions; namespace Microsoft.Extensions.DependencyInjection; @@ -25,6 +26,7 @@ public static OrchardCoreBuilder AddSecurity(this OrchardCoreBuilder builder) }); services.AddScoped(); + services.AddScoped(); services.AddScoped(); }); diff --git a/src/docs/releases/3.0.0.md b/src/docs/releases/3.0.0.md index a52e416103e..ccbac4db9d4 100644 --- a/src/docs/releases/3.0.0.md +++ b/src/docs/releases/3.0.0.md @@ -157,12 +157,63 @@ The `Roles` recipe now includes the ability to define specific permission behavi For more info about the new `PermissionBehavior`, check out the [documentation](../reference/modules/Roles/README.md). -### ReCaptcha +### ReCaptcha Module #### New ReCaptcha Shape A new `ReCaptcha` shape has been introduced, enabling you to render the ReCaptcha challenge using a customizable shape. For more details, please refer to the [documentation](../reference/modules/ReCaptcha/README.md). +### Menu Module + +#### Permission-Based Menu Visibility + +The Menu module enables you to build frontend menus for your users through a user-friendly interface. We've enhanced this feature by allowing you to require one or more permissions before a menu item becomes visible to the user. + +If you're using a custom `MenuItem` in your project and want to incorporate this functionality, you can achieve it by attaching the `MenuItemPermissionPart` to your custom `MenuItem` content type. + +When caching your menu, it's crucial to include the `cache-context` to ensure the menu is properly cached and invalidated based on the user's roles. This ensures the menu is displayed correctly for each logged-in user, based on their specific roles. + +For example, here's how you can add the menu with `cache-context` using Razor: + +```razor + +``` + +Notice the `cache-context="user.roles"` attribute. + +Alternatively, here's how you can implement the same functionality using Liquid: + +```liquid +{% shape "menu", alias: "alias:main-menu", cache_id: "main-menu", cache_fixed_duration: "00:05:00", cache_tag: "alias:main-menu", cache_context: "user.roles" %} +``` + +Again, notice the inclusion of `cache_context: "user.roles"`. + +By default, permissions are enabled for new tenants. However, if you'd like to add permissions to an existing tenant, you can use the "Add Permissions to Menus" recipe either through the UI or by executing the recipe programmatically as shown below: + +```json +{ + "steps": [ + { + "name": "recipes", + "Values": [ + { + "executionid": "MenuAddPermissions", + "name": "MenuAddPermissions" + } + ] + } + ] +} +``` + +!!! note + Be sure to update all instances where you create a menu shape by adding the cache-context attribute. This ensures the menu is properly cached and tailored based on the user's roles. + +#### Permission-Based Content Menu Visibility + +A new option, **Check content permissions**, has been added to the **Content Menu Item**. This feature allows you to control the visibility of a menu item based on the user's permissions. When this option is enabled, the system ensures that the current user has the `View Content` permission for the selected item before displaying it. + ## Miscellaneous ### Sealing Types