diff --git a/src/AbpDesk/AbpDesk.Web.Mvc/AbpDeskWebMvcModule.cs b/src/AbpDesk/AbpDesk.Web.Mvc/AbpDeskWebMvcModule.cs index fda6740474..1f15975a11 100644 --- a/src/AbpDesk/AbpDesk.Web.Mvc/AbpDeskWebMvcModule.cs +++ b/src/AbpDesk/AbpDesk.Web.Mvc/AbpDeskWebMvcModule.cs @@ -1,4 +1,5 @@ using AbpDesk.EntityFrameworkCore; +using AbpDesk.Web.Mvc.Navigation; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -8,6 +9,7 @@ using Volo.Abp; using Volo.Abp.AspNetCore.Modularity; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.Modularity; +using Volo.Abp.Ui.Navigation; namespace AbpDesk.Web.Mvc { @@ -23,7 +25,12 @@ namespace AbpDesk.Web.Mvc var configuration = BuildConfiguration(hostingEnvironment); AbpDeskDbConfigurer.Configure(services, configuration); - + + services.Configure(options => + { + options.MenuContributors.Add(new MainMenuContributor()); + }); + services.AddMvc(); services.AddAssemblyOf(); } diff --git a/src/AbpDesk/AbpDesk.Web.Mvc/Navigation/MainMenuContributor.cs b/src/AbpDesk/AbpDesk.Web.Mvc/Navigation/MainMenuContributor.cs new file mode 100644 index 0000000000..787ce6f723 --- /dev/null +++ b/src/AbpDesk/AbpDesk.Web.Mvc/Navigation/MainMenuContributor.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Volo.Abp.Ui.Navigation; + +namespace AbpDesk.Web.Mvc.Navigation +{ + public class MainMenuContributor : IMenuContributor + { + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name != StandardMenus.Main) + { + return Task.CompletedTask; + } + + context.Menu.DisplayName = "Main Menu"; + + context.Menu.AddItem( + new ApplicationMenuItem("TicketManagement", "Ticket Management") + .AddItem(new ApplicationMenuItem("Administration.UserManagement", "User Management")) + .AddItem(new ApplicationMenuItem("Administration.RoleManagement", "Role Management")) + ); + + return Task.CompletedTask; + } + } +} diff --git a/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/Components/HorizontalMenu/Default.cshtml b/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/Components/HorizontalMenu/Default.cshtml new file mode 100644 index 0000000000..a2968363af --- /dev/null +++ b/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/Components/HorizontalMenu/Default.cshtml @@ -0,0 +1,22 @@ +@using System.Threading.Tasks +@model Volo.Abp.Ui.Navigation.ApplicationMenu +

@Model.DisplayName

+ \ No newline at end of file diff --git a/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/Components/HorizontalMenu/HorizontalMenuViewComponent.cs b/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/Components/HorizontalMenu/HorizontalMenuViewComponent.cs new file mode 100644 index 0000000000..8d0b2d4802 --- /dev/null +++ b/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/Components/HorizontalMenu/HorizontalMenuViewComponent.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Ui.Navigation; + +namespace AbpDesk.Web.Mvc.Views.Shared.Components.HorizontalMenu +{ + public class HorizontalMenuViewComponent : ViewComponent + { + private readonly IMenuManager _menuManager; + + public HorizontalMenuViewComponent(IMenuManager menuManager) + { + //TODO: Create a INavigationAppService that can also be used remotely, instead of directly using IMenuManager! + + _menuManager = menuManager; + } + + public async Task InvokeAsync(string menuName = StandardMenus.Main) + { + var menu = await _menuManager.GetAsync(StandardMenus.Main); + return View(menu); + } + } +} diff --git a/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/_Layout.cshtml b/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/_Layout.cshtml index 40498afa95..3c83e69d41 100644 --- a/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/_Layout.cshtml +++ b/src/AbpDesk/AbpDesk.Web.Mvc/Views/Shared/_Layout.cshtml @@ -1,4 +1,6 @@ -@{ +@using AbpDesk.Web.Mvc.Views.Shared.Components +@using AbpDesk.Web.Mvc.Views.Shared.Components.HorizontalMenu +@{ Layout = null; } @@ -7,11 +9,13 @@ title - + @RenderSection("styles", false) - + + @await Component.InvokeAsync(typeof(HorizontalMenuViewComponent)) + @RenderBody() diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/ApplicationMenu.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/ApplicationMenu.cs new file mode 100644 index 0000000000..fff323edf4 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/ApplicationMenu.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.Ui.Navigation +{ + public class ApplicationMenu : IHasMenuItems + { + /// + /// Unique name of the menu in the application. + /// + [NotNull] + public string Name { get; } + + /// + /// Display name of the menu. + /// Default value is the . + /// + [NotNull] + public string DisplayName + { + get { return _displayName; } + set + { + Check.NotNullOrWhiteSpace(value, nameof(value)); + _displayName = value; + } + } + private string _displayName; + + /// + [NotNull] + public IList Items { get; } //TODO: Create a specialized collection (that can contain AddAfter for example) + + /// + /// Can be used to store a custom object related to this menu. + /// + [CanBeNull] + public object CustomData { get; set; } + + public ApplicationMenu( + [NotNull] string name, + string displayName = null) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); + + Name = name; + DisplayName = displayName ?? Name; + + Items = new List(); + } + + /// + /// Adds a to . + /// + /// to be added + /// This object + public ApplicationMenu AddItem([NotNull] ApplicationMenuItem menuItem) + { + Items.Add(menuItem); + return this; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs new file mode 100644 index 0000000000..9ad5a1db55 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Volo.ExtensionMethods.Collections.Generic; + +namespace Volo.Abp.Ui.Navigation +{ + public class ApplicationMenuItem : IHasMenuItems + { + private string _displayName; + + /// + /// Default value of a menu item. + /// + public const int DefaultOrder = 1000; + + /// + /// Unique name of the menu in the application. + /// + [NotNull] + public string Name { get; } + + /// + /// Display name of the menu item. + /// + [NotNull] + public string DisplayName + { + get { return _displayName; } + set + { + Check.NotNullOrWhiteSpace(value, nameof(value)); + _displayName = value; + } + } + + /// + /// The Display order of the menu. + /// Default value: 1000. + /// + public int Order { get; set; } + + /// + /// The URL to navigate when this menu item is selected. + /// + [CanBeNull] + public string Url { get; set; } + + /// + /// Icon of the menu item if exists. + /// + [CanBeNull] + public string Icon { get; set; } + + /// + /// Returns true if this menu item has no child . + /// + public bool IsLeaf => Items.IsNullOrEmpty(); + + /// + /// Target of the menu item. Can be null, "_blank", "_self", "_parent", "_top" or a frame name for web applications. + /// + [CanBeNull] + public string Target { get; set; } + + /// + [NotNull] + public IList Items { get; } + + /// + /// Can be used to store a custom object related to this menu item. Optional. + /// + public object CustomData { get; set; } + + public ApplicationMenuItem( + [NotNull] string name, + [NotNull] string displayName, + string url = null, + string icon = null, + int order = DefaultOrder, + object customData = null, + string target = null) + { + Check.NotNullOrWhiteSpace(name, nameof(name)); + Check.NotNullOrWhiteSpace(displayName, nameof(displayName)); + + Name = name; + DisplayName = displayName; + Url = url; + Icon = icon; + Order = order; + CustomData = customData; + Target = target; + + Items = new List(); + } + + /// + /// Adds a to . + /// + /// to be added + /// This object + public ApplicationMenuItem AddItem([NotNull] ApplicationMenuItem menuItem) + { + Items.Add(menuItem); + return this; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/IHasMenuItems.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IHasMenuItems.cs new file mode 100644 index 0000000000..82b3058902 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IHasMenuItems.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Volo.Abp.Ui.Navigation +{ + public interface IHasMenuItems + { + /// + /// Menu items. + /// + IList Items { get; } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuConfigurationContext.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuConfigurationContext.cs new file mode 100644 index 0000000000..f07b3cd0b9 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuConfigurationContext.cs @@ -0,0 +1,11 @@ +using Volo.DependencyInjection; + +namespace Volo.Abp.Ui.Navigation +{ + public interface IMenuConfigurationContext : IServiceProviderAccessor + { + ApplicationMenu Menu { get; set; } + + //TODO: Add Localization, Authorization components since they are most used components on menu creation! + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuContributor.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuContributor.cs new file mode 100644 index 0000000000..e38ad24a14 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuContributor.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Ui.Navigation +{ + public interface IMenuContributor + { + Task ConfigureMenuAsync(MenuConfigurationContext context); + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuManager.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuManager.cs new file mode 100644 index 0000000000..81ae9d9318 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/IMenuManager.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Volo.Abp.Ui.Navigation +{ + public interface IMenuManager + { + Task GetAsync(string name); + } +} diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/MenuConfigurationContext.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/MenuConfigurationContext.cs new file mode 100644 index 0000000000..98d650b758 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/MenuConfigurationContext.cs @@ -0,0 +1,17 @@ +using System; + +namespace Volo.Abp.Ui.Navigation +{ + public class MenuConfigurationContext : IMenuConfigurationContext + { + public ApplicationMenu Menu { get; set; } + + public IServiceProvider ServiceProvider { get; } + + public MenuConfigurationContext(ApplicationMenu menu, IServiceProvider serviceProvider) + { + Menu = menu; + ServiceProvider = serviceProvider; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/MenuManager.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/MenuManager.cs new file mode 100644 index 0000000000..f42c5a35ef --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/MenuManager.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.DependencyInjection; + +namespace Volo.Abp.Ui.Navigation +{ + public class MenuManager : IMenuManager, ITransientDependency + { + private readonly NavigationOptions _options; + private readonly IServiceProvider _serviceProvider; + + public MenuManager( + IOptions options, + IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _options = options.Value; + } + + public async Task GetAsync(string name) + { + var menu = new ApplicationMenu(name); + + using (var scope = _serviceProvider.CreateScope()) + { + var context = new MenuConfigurationContext(menu, scope.ServiceProvider); + + foreach (var contributor in _options.MenuContributors) + { + await contributor.ConfigureMenuAsync(context); + } + } + + return menu; + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/NavigationOptions.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/NavigationOptions.cs new file mode 100644 index 0000000000..b39f5d5df0 --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/NavigationOptions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace Volo.Abp.Ui.Navigation +{ + public class NavigationOptions + { + [NotNull] + public List MenuContributors { get; } + + public NavigationOptions() + { + MenuContributors = new List(); + } + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Ui/Navigation/StandardMenus.cs b/src/Volo.Abp/Volo/Abp/Ui/Navigation/StandardMenus.cs new file mode 100644 index 0000000000..5df07aec5c --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Ui/Navigation/StandardMenus.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Ui.Navigation +{ + public static class StandardMenus + { + public const string Main = "Main"; + public const string User = "User"; + } +} \ No newline at end of file diff --git a/src/Volo.Abp/Volo/ExtensionMethods/Collections/Generic/ListExtensions.cs b/src/Volo.Abp/Volo/ExtensionMethods/Collections/Generic/ListExtensions.cs index ed5729d8a1..b2db839574 100644 --- a/src/Volo.Abp/Volo/ExtensionMethods/Collections/Generic/ListExtensions.cs +++ b/src/Volo.Abp/Volo/ExtensionMethods/Collections/Generic/ListExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; namespace Volo.ExtensionMethods.Collections.Generic { @@ -26,6 +28,22 @@ namespace Volo.ExtensionMethods.Collections.Generic source.Insert(targetIndex, item); } + [NotNull] + public static T GetOrAdd([NotNull] this IList source, Func selector, Func factory) + { + Check.NotNull(source, nameof(source)); + + var item = source.FirstOrDefault(selector); + + if (item == null) + { + item = factory(); + source.Add(item); + } + + return item; + } + /// /// Sort a list by a topological sorting, which consider their dependencies. /// diff --git a/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/Data/MultiTenancy/MultiTenantConnectionStringResolver_Tests.cs b/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/Data/MultiTenancy/MultiTenantConnectionStringResolver_Tests.cs index e53d43ca8b..34ccfaf1c3 100644 --- a/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/Data/MultiTenancy/MultiTenantConnectionStringResolver_Tests.cs +++ b/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/Data/MultiTenancy/MultiTenantConnectionStringResolver_Tests.cs @@ -1,14 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using Shouldly; -using Volo.Abp.Guids; using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy.ConfigurationStore; -using Volo.ExtensionMethods.Collections.Generic; using Xunit; namespace Volo.Abp.Data.MultiTenancy diff --git a/test/Volo.Abp.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs new file mode 100644 index 0000000000..357707c40d --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs @@ -0,0 +1,102 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.TestBase; +using Volo.ExtensionMethods.Collections.Generic; +using Xunit; + +namespace Volo.Abp.Ui.Navigation +{ + public class MenuManager_Tests : AbpIntegratedTest + { + private readonly IMenuManager _menuManager; + + public MenuManager_Tests() + { + _menuManager = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task Should_Get_Menu() + { + var mainMenu = await _menuManager.GetAsync(StandardMenus.Main); + + mainMenu.Name.ShouldBe(StandardMenus.Main); + mainMenu.DisplayName.ShouldBe("Main Menu"); + mainMenu.Items.Count.ShouldBe(2); + mainMenu.Items[0].Name.ShouldBe("Dashboard"); + mainMenu.Items[1].Name.ShouldBe("Administration"); + mainMenu.Items[1].Items[0].Name.ShouldBe("Administration.UserManagement"); + mainMenu.Items[1].Items[1].Name.ShouldBe("Administration.RoleManagement"); + mainMenu.Items[1].Items[2].Name.ShouldBe("Administration.DashboardSettings"); + } + + public class TestModule : AbpModule + { + public override void ConfigureServices(IServiceCollection services) + { + services.Configure(options => + { + options.MenuContributors.Add(new TestMenuContributer1()); + options.MenuContributors.Add(new TestMenuContributer2()); + }); + } + } + + /* Adds menu items: + * - Administration + * - User Management + * - Role Management + */ + public class TestMenuContributer1 : IMenuContributor + { + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name != StandardMenus.Main) + { + return Task.CompletedTask; + } + + context.Menu.DisplayName = "Main Menu"; + + var administration = context.Menu.Items.GetOrAdd( + m => m.Name == "Administration", + () => new ApplicationMenuItem("Administration", "Administration") + ); + + administration.AddItem(new ApplicationMenuItem("Administration.UserManagement", "User Management")); + administration.AddItem(new ApplicationMenuItem("Administration.RoleManagement", "Role Management")); + + return Task.CompletedTask; + } + } + + /* Adds menu items: + * - Dashboard + * - Administration + * - Dashboard Settings + */ + public class TestMenuContributer2 : IMenuContributor + { + public Task ConfigureMenuAsync(MenuConfigurationContext context) + { + if (context.Menu.Name != StandardMenus.Main) + { + return Task.CompletedTask; + } + + context.Menu.Items.Insert(0, new ApplicationMenuItem("Dashboard", "Dashboard")); + + var administration = context.Menu.Items.GetOrAdd( + m => m.Name == "Administration", + () => new ApplicationMenuItem("Administration", "Administration") + ); + + administration.AddItem(new ApplicationMenuItem("Administration.DashboardSettings", "Dashboard Settings")); + + return Task.CompletedTask; + } + } + } +}