diff --git a/common.props b/common.props index b79d4aa..6f03b79 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 0.1.0 + 0.2.0 $(NoWarn);CS1591;CS0436 module true diff --git a/docs/README.md b/docs/README.md index 5f3e6a5..adcfcd1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,2 +1,43 @@ # Abp.DynamicMenu + +[![ABP version](https://img.shields.io/badge/dynamic/xml?style=flat-square&color=yellow&label=abp&query=%2F%2FProject%2FPropertyGroup%2FAbpVersion&url=https%3A%2F%2Fraw.githubusercontent.com%2FEasyAbp%2FAbp.DynamicMenu%2Fmaster%2FDirectory.Build.props)](https://abp.io) +[![NuGet](https://img.shields.io/nuget/v/EasyAbp.Abp.DynamicMenu.Domain.Shared.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.Abp.DynamicMenu.Domain.Shared) +[![NuGet Download](https://img.shields.io/nuget/dt/EasyAbp.Abp.DynamicMenu.Domain.Shared.svg?style=flat-square)](https://www.nuget.org/packages/EasyAbp.Abp.DynamicMenu.Domain.Shared) +[![GitHub stars](https://img.shields.io/github/stars/EasyAbp/Abp.DynamicMenu?style=social)](https://www.github.com/EasyAbp/Abp.DynamicMenu) + An abp module that dynamically creates menu items for ABP UI projects in runtime. + +## Online Demo + +We have launched an online demo for this module: [https://dynamicmenu.samples.easyabp.io](https://dynamicmenu.samples.easyabp.io) + +![demo.gif](/docs/images/demo.gif) + +## Installation + +1. Install the following NuGet packages. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-nuget-packages)) + + * EasyAbp.Abp.DynamicMenu.Application + * EasyAbp.Abp.DynamicMenu.Application.Contracts + * EasyAbp.Abp.DynamicMenu.Domain + * EasyAbp.Abp.DynamicMenu.Domain.Shared + * EasyAbp.Abp.DynamicMenu.EntityFrameworkCore + * EasyAbp.Abp.DynamicMenu.HttpApi + * EasyAbp.Abp.DynamicMenu.HttpApi.Client + * EasyAbp.Abp.DynamicMenu.Web + +1. Add `DependsOn(typeof(AbpDynamicMenuXxxModule))` attribute to configure the module dependencies. ([see how](https://github.com/EasyAbp/EasyAbpGuide/blob/master/docs/How-To.md#add-module-dependencies)) + +1. Add `builder.ConfigureAbpDynamicMenu();` to the `OnModelCreating()` method in **MyProjectMigrationsDbContext.cs**. + +1. Add EF Core migrations and update your database. See: [ABP document](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=MVC&DB=EF#add-database-migration). + +## Usage + +1. Create a dynamic menu item on the management page. + +2. Refresh the page and you can see the menu item you just created. + +## Road map + +- [ ] More customizable options for menu items. diff --git a/docs/images/demo.gif b/docs/images/demo.gif new file mode 100644 index 0000000..10e3215 Binary files /dev/null and b/docs/images/demo.gif differ diff --git a/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/DynamicMenuBlazorHostModule.cs b/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/DynamicMenuBlazorHostModule.cs index c2b5998..dfc84a8 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/DynamicMenuBlazorHostModule.cs +++ b/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/DynamicMenuBlazorHostModule.cs @@ -51,9 +51,8 @@ namespace EasyAbp.Abp.DynamicMenu.Blazor.Server.Host { [DependsOn( - typeof(DynamicMenuEntityFrameworkCoreModule), - typeof(DynamicMenuApplicationModule), typeof(DynamicMenuHttpApiModule), + typeof(DynamicMenuHostSharedModule), typeof(AbpAspNetCoreMvcUiBasicThemeModule), typeof(AbpAutofacModule), typeof(AbpSwashbuckleModule), diff --git a/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host.csproj b/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host.csproj index f9c5f7e..65bcce7 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host.csproj +++ b/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host.csproj @@ -21,7 +21,6 @@ - @@ -46,7 +45,6 @@ - diff --git a/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EntityFrameworkCore/UnifiedDbContext.cs b/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EntityFrameworkCore/UnifiedDbContext.cs index 93e20b5..1b8f1ab 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EntityFrameworkCore/UnifiedDbContext.cs +++ b/host/EasyAbp.Abp.DynamicMenu.Blazor.Server.Host/EntityFrameworkCore/UnifiedDbContext.cs @@ -28,7 +28,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ConfigureIdentity(); modelBuilder.ConfigureFeatureManagement(); modelBuilder.ConfigureTenantManagement(); - modelBuilder.ConfigureDynamicMenu(); + modelBuilder.ConfigureAbpDynamicMenu(); } } } diff --git a/host/EasyAbp.Abp.DynamicMenu.Host.Shared/DemoDataSeedContributor.cs b/host/EasyAbp.Abp.DynamicMenu.Host.Shared/DemoDataSeedContributor.cs new file mode 100644 index 0000000..37f9076 --- /dev/null +++ b/host/EasyAbp.Abp.DynamicMenu.Host.Shared/DemoDataSeedContributor.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using EasyAbp.Abp.DynamicMenu.MenuItems; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace EasyAbp.Abp.DynamicMenu +{ + public class DemoDataSeedContributor : IDataSeedContributor, ITransientDependency + { + private readonly IMenuItemRepository _menuItemRepository; + + public DemoDataSeedContributor(IMenuItemRepository menuItemRepository) + { + _menuItemRepository = menuItemRepository; + } + + [UnitOfWork] + public async Task SeedAsync(DataSeedContext context) + { + if (await _menuItemRepository.FindAsync(x => x.Name == "DemoMenu") != null) + { + return; + } + + var subItems = new List + { + new("DemoMenu", "ChangePassword", "Change password", "~/Account/Manage", null, null, null, null, + DynamicMenuConsts.DefaultLResourceTypeName, DynamicMenuConsts.DefaultLResourceTypeAssemblyName, + null) + }; + + var demoMenu = new MenuItem(null, "DemoMenu", "Demo menu", null, null, null, null, null, + DynamicMenuConsts.DefaultLResourceTypeName, DynamicMenuConsts.DefaultLResourceTypeAssemblyName, subItems); + + await _menuItemRepository.InsertAsync(demoMenu, true); + } + } +} \ No newline at end of file diff --git a/host/EasyAbp.Abp.DynamicMenu.Host.Shared/DynamicMenuHostSharedModule.cs b/host/EasyAbp.Abp.DynamicMenu.Host.Shared/DynamicMenuHostSharedModule.cs new file mode 100644 index 0000000..5a128b2 --- /dev/null +++ b/host/EasyAbp.Abp.DynamicMenu.Host.Shared/DynamicMenuHostSharedModule.cs @@ -0,0 +1,14 @@ +using EasyAbp.Abp.DynamicMenu.EntityFrameworkCore; +using Volo.Abp.Modularity; + +namespace EasyAbp.Abp.DynamicMenu +{ + [DependsOn( + typeof(DynamicMenuEntityFrameworkCoreModule), + typeof(DynamicMenuApplicationModule) + )] + public class DynamicMenuHostSharedModule : AbpModule + { + + } +} diff --git a/host/EasyAbp.Abp.DynamicMenu.Host.Shared/EasyAbp.Abp.DynamicMenu.Host.Shared.csproj b/host/EasyAbp.Abp.DynamicMenu.Host.Shared/EasyAbp.Abp.DynamicMenu.Host.Shared.csproj index 07f240f..4b64cfd 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Host.Shared/EasyAbp.Abp.DynamicMenu.Host.Shared.csproj +++ b/host/EasyAbp.Abp.DynamicMenu.Host.Shared/EasyAbp.Abp.DynamicMenu.Host.Shared.csproj @@ -3,8 +3,13 @@ - netstandard2.0 + netstandard2.1 EasyAbp.Abp.DynamicMenu + + + + + diff --git a/host/EasyAbp.Abp.DynamicMenu.Web.Unified/DynamicMenuWebUnifiedModule.cs b/host/EasyAbp.Abp.DynamicMenu.Web.Unified/DynamicMenuWebUnifiedModule.cs index 1a38f61..56467ce 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Web.Unified/DynamicMenuWebUnifiedModule.cs +++ b/host/EasyAbp.Abp.DynamicMenu.Web.Unified/DynamicMenuWebUnifiedModule.cs @@ -42,8 +42,7 @@ namespace EasyAbp.Abp.DynamicMenu { [DependsOn( typeof(DynamicMenuWebModule), - typeof(DynamicMenuApplicationModule), - typeof(DynamicMenuEntityFrameworkCoreModule), + typeof(DynamicMenuHostSharedModule), typeof(AbpAuditLoggingEntityFrameworkCoreModule), typeof(AbpAutofacModule), typeof(AbpAccountWebModule), @@ -65,7 +64,7 @@ namespace EasyAbp.Abp.DynamicMenu typeof(AbpAspNetCoreMvcUiBasicThemeModule), typeof(AbpAspNetCoreSerilogModule), typeof(AbpSwashbuckleModule) - )] + )] public class DynamicMenuWebUnifiedModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EasyAbp.Abp.DynamicMenu.Web.Unified.csproj b/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EasyAbp.Abp.DynamicMenu.Web.Unified.csproj index dc06d28..a2c37a2 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EasyAbp.Abp.DynamicMenu.Web.Unified.csproj +++ b/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EasyAbp.Abp.DynamicMenu.Web.Unified.csproj @@ -40,8 +40,6 @@ - - diff --git a/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EntityFrameworkCore/UnifiedDbContext.cs b/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EntityFrameworkCore/UnifiedDbContext.cs index 9bdb868..29474f9 100644 --- a/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EntityFrameworkCore/UnifiedDbContext.cs +++ b/host/EasyAbp.Abp.DynamicMenu.Web.Unified/EntityFrameworkCore/UnifiedDbContext.cs @@ -27,7 +27,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ConfigureIdentity(); modelBuilder.ConfigureFeatureManagement(); modelBuilder.ConfigureTenantManagement(); - modelBuilder.ConfigureDynamicMenu(); + modelBuilder.ConfigureAbpDynamicMenu(); } } } diff --git a/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/ExceededMenuLevelLimitException.cs b/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/ExceededMenuLevelLimitException.cs new file mode 100644 index 0000000..9038725 --- /dev/null +++ b/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/ExceededMenuLevelLimitException.cs @@ -0,0 +1,13 @@ +using Volo.Abp; + +namespace EasyAbp.Abp.DynamicMenu.MenuItems +{ + public sealed class ExceededMenuLevelLimitException : BusinessException + { + public ExceededMenuLevelLimitException(int maxLevel) + : base("EasyAbp.Abp.DynamicMenu:ExceededMenuLevelLimit") + { + Data["MaxLevel"] = maxLevel; + } + } +} \ No newline at end of file diff --git a/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemAppService.cs b/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemAppService.cs index 9cf4fc6..b9e203d 100644 --- a/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemAppService.cs +++ b/src/EasyAbp.Abp.DynamicMenu.Application/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemAppService.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EasyAbp.Abp.DynamicMenu.Permissions; @@ -40,7 +42,7 @@ protected override async Task GetEntityByIdAsync(MenuItemKey id) { // TODO: AbpHelper generated return await AsyncExecuter.FirstOrDefaultAsync( - _repository.Where(e => + (await _repository.WithDetailsAsync()).Where(e => e.Name == id.Name ) ); @@ -82,6 +84,30 @@ public override async Task UpdateAsync(MenuItemKey id, UpdateMenuIt return await MapToGetOutputDtoAsync(entity); } + public override async Task DeleteAsync(MenuItemKey id) + { + await CheckDeletePolicyAsync(); + + var menuItem = await GetEntityByIdAsync(id); + + await RecursiveDeleteAsync(menuItem); + + await CurrentUnitOfWork.SaveChangesAsync(); + } + + protected virtual async Task RecursiveDeleteAsync(MenuItem menuItem) + { + if (!menuItem.MenuItems.IsNullOrEmpty()) + { + foreach (var subItem in menuItem.MenuItems) + { + await RecursiveDeleteAsync(subItem); + } + } + + await _repository.DeleteAsync(menuItem); + } + protected async Task CheckParentNameAsync([CanBeNull] string parentName) { if (parentName == null) @@ -89,10 +115,23 @@ protected async Task CheckParentNameAsync([CanBeNull] string parentName) return; } - if (await _repository.FindAsync(x => x.Name == parentName) == null) + var parent = await _repository.FindAsync(x => x.Name == parentName); + + if (parent == null) { throw new NonexistentParentMenuItemException(parentName); } + + // Maximum menu item level: 3 + if (!parent.ParentName.IsNullOrEmpty()) + { + var grandparent = await _repository.GetAsync(x => x.Name == parent.ParentName); + + if (grandparent.ParentName != null) + { + throw new ExceededMenuLevelLimitException(3); + } + } } } } diff --git a/src/EasyAbp.Abp.DynamicMenu.Blazor/Menus/DynamicMenuMenuContributor.cs b/src/EasyAbp.Abp.DynamicMenu.Blazor/Menus/DynamicMenuMenuContributor.cs index 52da4d9..230e889 100644 --- a/src/EasyAbp.Abp.DynamicMenu.Blazor/Menus/DynamicMenuMenuContributor.cs +++ b/src/EasyAbp.Abp.DynamicMenu.Blazor/Menus/DynamicMenuMenuContributor.cs @@ -86,7 +86,7 @@ protected virtual async Task AddDynamicMenuItemsAsync(IHasMenuItems parent, IEnu continue; } - child.Url = menuItem.UrlMvc ?? menuItem.Url; + child.Url = menuItem.UrlBlazor ?? menuItem.Url; } else { diff --git a/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/en.json b/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/en.json index ec683e1..e277674 100644 --- a/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/en.json +++ b/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/en.json @@ -24,6 +24,7 @@ "SubMenuItems": "Sub-items", "MenuItemDeletionConfirmationMessage": "Are you sure to delete the menu item {0}?", "SuccessfullyDeleted": "Successfully deleted", - "EasyAbp.Abp.DynamicMenu:NonexistentParentMenuItem": "Nonexistent parent menu item: {ParentName}" + "EasyAbp.Abp.DynamicMenu:NonexistentParentMenuItem": "Nonexistent parent menu item: {ParentName}", + "EasyAbp.Abp.DynamicMenu:ExceededMenuLevelLimit": "Exceeded the maximum menu item level: {MaxLevel}" } } \ No newline at end of file diff --git a/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hans.json b/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hans.json index 2c4bbf4..26a8396 100644 --- a/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hans.json +++ b/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hans.json @@ -24,6 +24,7 @@ "SubMenuItems": "子菜单项", "MenuItemDeletionConfirmationMessage": "确认删除菜单项 {0}?", "SuccessfullyDeleted": "删除成功", - "EasyAbp.Abp.DynamicMenu:NonexistentParentMenuItem": "不存在的父级菜单项:{ParentName}" + "EasyAbp.Abp.DynamicMenu:NonexistentParentMenuItem": "不存在的父级菜单项:{ParentName}", + "EasyAbp.Abp.DynamicMenu:ExceededMenuLevelLimit": "菜单项最大支持 {MaxLevel} 级" } } \ No newline at end of file diff --git a/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hant.json b/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hant.json index c057bca..c02852c 100644 --- a/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hant.json +++ b/src/EasyAbp.Abp.DynamicMenu.Domain.Shared/EasyAbp/Abp/DynamicMenu/Localization/zh-Hant.json @@ -24,6 +24,7 @@ "SubMenuItems": "子菜單項", "MenuItemDeletionConfirmationMessage": "確認刪除菜單項 {0}?", "SuccessfullyDeleted": "刪除成功", - "EasyAbp.Abp.DynamicMenu:NonexistentParentMenuItem": "不存在的父級菜單項:{ParentName}" + "EasyAbp.Abp.DynamicMenu:NonexistentParentMenuItem": "不存在的父級菜單項:{ParentName}", + "EasyAbp.Abp.DynamicMenu:ExceededMenuLevelLimit": "菜單項最大支持 {MaxLevel} 級" } } \ No newline at end of file diff --git a/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContext.cs b/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContext.cs index 8d9f620..8381fce 100644 --- a/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContext.cs +++ b/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContext.cs @@ -23,7 +23,7 @@ protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); - builder.ConfigureDynamicMenu(); + builder.ConfigureAbpDynamicMenu(); } } } diff --git a/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContextModelCreatingExtensions.cs b/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContextModelCreatingExtensions.cs index 7337b84..10bffae 100644 --- a/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContextModelCreatingExtensions.cs +++ b/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/EntityFrameworkCore/DynamicMenuDbContextModelCreatingExtensions.cs @@ -8,7 +8,7 @@ namespace EasyAbp.Abp.DynamicMenu.EntityFrameworkCore { public static class DynamicMenuDbContextModelCreatingExtensions { - public static void ConfigureDynamicMenu( + public static void ConfigureAbpDynamicMenu( this ModelBuilder builder, Action optionsAction = null) { @@ -45,14 +45,16 @@ public static void ConfigureDynamicMenu( builder.Entity(b => { b.ToTable(options.TablePrefix + "MenuItems", options.Schema); - b.ConfigureByConvention(); + b.ConfigureByConvention(); + + /* Configure more properties here */ b.HasKey(e => new { e.Name, }); - /* Configure more properties here */ + b.HasMany(x => x.MenuItems).WithOne(); }); } } diff --git a/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemEfCoreQuerableExtensions.cs b/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemEfCoreQuerableExtensions.cs index ecd4929..89bc223 100644 --- a/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemEfCoreQuerableExtensions.cs +++ b/src/EasyAbp.Abp.DynamicMenu.EntityFrameworkCore/EasyAbp/Abp/DynamicMenu/MenuItems/MenuItemEfCoreQuerableExtensions.cs @@ -13,7 +13,8 @@ public static IQueryable IncludeDetails(this IQueryable quer } return queryable - .Include(x => x.MenuItems); + .Include(x => x.MenuItems) + .ThenInclude(x => x.MenuItems); } } } \ No newline at end of file