diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
index 8ba77e32a9..d06e505e40 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
@@ -208,7 +208,7 @@
"WhenShouldIRenewMyLicense": "When should I renew my license?",
"WhenShouldIRenewMyLicenseExplanation": "If you renew your license within 1 month after your license expires, the following discounts will be applied: Team License {0}; Business License {1}; Enterprise License {2}. However, if you renew your license after 1 month since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.",
"TrialPlan": "Do you have a trial plan?",
- "TrialPlanExplanation": "It has a 14 days trial period for the ABP Commercial team license. For more information visit here. Furthermore, for the Team licenses we provide a 30 days money-back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.",
+ "TrialPlanExplanation": "No, there is no trial version for ABP Commercial. You can check the community edition to understand the code quality and approaches. We also offer a 30-day money-back guarantee for the Team license, no questions asked! You can request a refund within the first 30 days. We provide a 60% refund within 30 days for Business and Enterprise licenses. This is because the Business and Enterprise licenses contain the full source-code of all the modules and themes.",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.
After sending the license fee via bank transfer, send your receipt and requested license type to accounting@volosoft.com.
Our international bank account information:",
"HowToUpgrade": "How to upgrade existing applications when a new version is available?",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
index 8cfb154295..953625f9cd 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
@@ -184,6 +184,8 @@
"Layout_MetaDescription": "ABP Community is an environment where people can share posts about ABP framework and follows the projects.",
"Index_Page_CommunityIntroduction": "This is a hub for ABP Framework, .NET and software development. You can read the articles, watch the video tutorials, get informed about ABP’s development progress and ABP-related events, help other developers and share your expertise with the ABP community.",
"TagsInArticle": "Tags in article",
- "IConsentToMedium": "I consent to the publication of this post at https://medium.com/volosoft."
+ "IConsentToMedium": "I consent to the publication of this post at https://medium.com/volosoft.",
+ "SearchResultsFor": "Search results for \"{0}\"",
+ "SeeMoreVideos": "See more videos"
}
}
diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md
index 6178ebbb8e..e71beccd6f 100644
--- a/docs/en/Tutorials/Part-10.md
+++ b/docs/en/Tutorials/Part-10.md
@@ -361,7 +361,7 @@ public class BookAppService :
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
- DeletePolicyName = BookStorePermissions.Books.Create;
+ DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task GetAsync(Guid id)
diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md
index 2f0eb9fc57..8df8b945d7 100644
--- a/docs/en/UI/Angular/List-Service.md
+++ b/docs/en/UI/Angular/List-Service.md
@@ -201,4 +201,4 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide
```
-ABP doesn't have a built-in filtering mechanism. You need to implement yourself and handle `filter` property in backend.
+ABP doesn't have a built-in filtering mechanism. You need to implement it yourself and handle the `filter` property in the backend.
diff --git a/docs/en/UI/AspNetCore/Navigation-Menu.md b/docs/en/UI/AspNetCore/Navigation-Menu.md
index f0776f47ec..60621f9906 100644
--- a/docs/en/UI/AspNetCore/Navigation-Menu.md
+++ b/docs/en/UI/AspNetCore/Navigation-Menu.md
@@ -104,6 +104,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications.
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute.
* `cssClass` (`string`): Additional string classes for the menu item.
+* `groupName` (`string`): Can be used to group menu items.
### Authorization
@@ -179,6 +180,58 @@ userMenu.Icon = "fa fa-users";
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors.
+### Menu Groups
+
+You can define groups and associate menu items with a group.
+
+Example:
+
+```csharp
+using System.Threading.Tasks;
+using MyProject.Localization;
+using Volo.Abp.UI.Navigation;
+
+namespace MyProject.Web.Menus
+{
+ public class MyProjectMenuContributor : IMenuContributor
+ {
+ public async Task ConfigureMenuAsync(MenuConfigurationContext context)
+ {
+ if (context.Menu.Name == StandardMenus.Main)
+ {
+ await ConfigureMainMenuAsync(context);
+ }
+ }
+
+ private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
+ {
+ var l = context.GetLocalizer();
+
+ context.Menu.AddGroup(
+ new ApplicationMenuGroup(
+ name: "Main",
+ displayName: l["Main"]
+ )
+ )
+ context.Menu.AddItem(
+ new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main")
+ .AddItem(new ApplicationMenuItem(
+ name: "MyProject.Crm.Customers",
+ displayName: l["Menu:Customers"],
+ url: "/crm/customers")
+ ).AddItem(new ApplicationMenuItem(
+ name: "MyProject.Crm.Orders",
+ displayName: l["Menu:Orders"],
+ url: "/crm/orders")
+ )
+ );
+ }
+ }
+}
+```
+
+> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group.
+
## Standard Menus
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus:
@@ -233,4 +286,3 @@ namespace MyProject.Web.Pages
}
}
```
-
diff --git a/docs/en/UI/Blazor/Navigation-Menu.md b/docs/en/UI/Blazor/Navigation-Menu.md
index 7f09a65dbb..0abac5fc68 100644
--- a/docs/en/UI/Blazor/Navigation-Menu.md
+++ b/docs/en/UI/Blazor/Navigation-Menu.md
@@ -104,6 +104,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications.
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute.
* `cssClass` (`string`): Additional string classes for the menu item.
+* `groupName` (`string`): Can be used to group menu items.
### Authorization
@@ -160,6 +161,58 @@ userMenu.Icon = "fa fa-users";
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors.
+### Menu Groups
+
+You can define groups and associate menu items with a group.
+
+Example:
+
+```csharp
+using System.Threading.Tasks;
+using MyProject.Localization;
+using Volo.Abp.UI.Navigation;
+
+namespace MyProject.Web.Menus
+{
+ public class MyProjectMenuContributor : IMenuContributor
+ {
+ public async Task ConfigureMenuAsync(MenuConfigurationContext context)
+ {
+ if (context.Menu.Name == StandardMenus.Main)
+ {
+ await ConfigureMainMenuAsync(context);
+ }
+ }
+
+ private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
+ {
+ var l = context.GetLocalizer();
+
+ context.Menu.AddGroup(
+ new ApplicationMenuGroup(
+ name: "Main",
+ displayName: l["Main"]
+ )
+ )
+ context.Menu.AddItem(
+ new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main")
+ .AddItem(new ApplicationMenuItem(
+ name: "MyProject.Crm.Customers",
+ displayName: l["Menu:Customers"],
+ url: "/crm/customers")
+ ).AddItem(new ApplicationMenuItem(
+ name: "MyProject.Crm.Orders",
+ displayName: l["Menu:Orders"],
+ url: "/crm/orders")
+ )
+ );
+ }
+ }
+}
+```
+
+> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group.
+
## Standard Menus
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus:
diff --git a/docs/zh-Hans/Tutorials/Part-10.md b/docs/zh-Hans/Tutorials/Part-10.md
index 170e3106a1..da5c23c78f 100644
--- a/docs/zh-Hans/Tutorials/Part-10.md
+++ b/docs/zh-Hans/Tutorials/Part-10.md
@@ -367,7 +367,7 @@ namespace Acme.BookStore.Books
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
- DeletePolicyName = BookStorePermissions.Books.Create;
+ DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task GetAsync(Guid id)
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs
new file mode 100644
index 0000000000..61c064b376
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs
@@ -0,0 +1,56 @@
+using System;
+using IdentityModel.Client;
+using Microsoft.AspNetCore.Authentication.OpenIdConnect;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.AspNetCore.Authentication.Cookies;
+
+public static class CookieAuthenticationOptionsExtensions
+{
+ ///
+ /// Introspect access token on validating the principal.
+ ///
+ ///
+ ///
+ ///
+ public static CookieAuthenticationOptions IntrospectAccessToken(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc")
+ {
+ var originalHandler = options.Events.OnValidatePrincipal;
+ options.Events.OnValidatePrincipal = async principalContext =>
+ {
+ originalHandler?.Invoke(principalContext);
+
+ if (principalContext.Principal != null && principalContext.Principal.Identity != null && principalContext.Principal.Identity.IsAuthenticated)
+ {
+ var accessToken = principalContext.Properties.GetTokenValue("access_token");
+ if (!accessToken.IsNullOrWhiteSpace())
+ {
+ var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService>().Get(oidcAuthenticationScheme);
+ if (openIdConnectOptions.Configuration == null && openIdConnectOptions.ConfigurationManager != null)
+ {
+ openIdConnectOptions.Configuration = await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(principalContext.HttpContext.RequestAborted);
+ }
+
+ var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
+ {
+ Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority.EnsureEndsWith('/') + "connect/introspect",
+ ClientId = openIdConnectOptions.ClientId,
+ ClientSecret = openIdConnectOptions.ClientSecret,
+ Token = accessToken
+ });
+
+ if (response.IsActive)
+ {
+ return;
+ }
+ }
+
+ principalContext.RejectPrincipal();
+ await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
+ }
+ };
+
+ return options;
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj
index 09c743bc2a..297b2a8576 100644
--- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj
+++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj
@@ -12,11 +12,13 @@
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs
new file mode 100644
index 0000000000..40ac508030
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/Configuration/BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService.cs
@@ -0,0 +1,23 @@
+using System.Threading.Tasks;
+using Volo.Abp.AspNetCore.Components.Web.Configuration;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.AspNetCore.Components.WebAssembly.Configuration;
+
+[Dependency(ReplaceServices = true)]
+public class BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService :
+ ICurrentApplicationConfigurationCacheResetService,
+ ITransientDependency
+{
+ private readonly WebAssemblyCachedApplicationConfigurationClient _webAssemblyCachedApplicationConfigurationClient;
+
+ public BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService(WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient)
+ {
+ _webAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient;
+ }
+
+ public async Task ResetAsync()
+ {
+ await _webAssemblyCachedApplicationConfigurationClient.InitializeAsync();
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Select2/Select2ScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Select2/Select2ScriptContributor.cs
index 4818158a97..b18767050b 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Select2/Select2ScriptContributor.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/Select2/Select2ScriptContributor.cs
@@ -1,16 +1,28 @@
using System.Collections.Generic;
+using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core;
using Volo.Abp.Modularity;
+using Volo.Abp.Localization;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2;
[DependsOn(typeof(CoreScriptContributor))]
public class Select2ScriptContributor : BundleContributor
{
+ public const string PackageName = "select2";
public override void ConfigureBundle(BundleConfigurationContext context)
{
- //TODO: Add select2.full.min.js or localize!
+ //TODO: Add select2.full.min.js
context.Files.AddIfNotContains("/libs/select2/js/select2.min.js");
}
+ public override void ConfigureDynamicResources(BundleConfigurationContext context)
+ {
+ var fileName = context.LazyServiceProvider.LazyGetRequiredService>().Value.GetCurrentUICultureLanguageFilesMap(PackageName);
+ var filePath = $"/libs/select2/js/i18n/{fileName}.js";
+ if (context.FileProvider.GetFileInfo(filePath).Exists)
+ {
+ context.Files.AddIfNotContains(filePath);
+ }
+ }
}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
index d1cbfa6b6b..6a9e6f7d80 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js
@@ -105,6 +105,7 @@
$select.select2({
ajax: {
url: url,
+ delay: 250,
dataType: "json",
data: function (params) {
let query = {};
@@ -132,6 +133,7 @@
width: '100%',
dropdownParent: parentSelector ? $(parentSelector) : $('body'),
allowClear: allowClear,
+ language: abp.localization.currentCulture.cultureName,
placeholder: {
id: '-1',
text: placeholder
diff --git a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj
index d8d2b83e5a..e03023a72a 100644
--- a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj
+++ b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj
@@ -15,7 +15,7 @@
-
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/LoginInfo.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/LoginInfo.cs
index aacb26e5f4..3b14c96330 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/LoginInfo.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/LoginInfo.cs
@@ -1,8 +1,11 @@
-
+using System;
+
namespace Volo.Abp.Cli.Auth;
public class LoginInfo
{
+ public Guid? Id { get; set; }
+
public string Name { get; set; }
public string Surname { get; set; }
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginInfoCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginInfoCommand.cs
index 9d17bf4c4f..d8f007dc2c 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginInfoCommand.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/LoginInfoCommand.cs
@@ -1,5 +1,4 @@
-using System;
-using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text;
using System.Threading.Tasks;
@@ -41,7 +40,7 @@ public class LoginInfoCommand : IConsoleCommand, ITransientDependency
var sb = new StringBuilder();
sb.AppendLine("");
- sb.AppendLine($"Login info:");
+ sb.AppendLine("Login info:");
sb.AppendLine($"Name: {loginInfo.Name}");
sb.AppendLine($"Surname: {loginInfo.Surname}");
sb.AppendLine($"Username: {loginInfo.Username}");
diff --git a/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs b/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs
index c7e1bef3b2..57f2c59d57 100644
--- a/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs
+++ b/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs
@@ -209,7 +209,22 @@ public static class AbpStringExtensions
return str;
}
- return str.Substring(0, pos) + replace + str.Substring(pos + search.Length);
+ var searchLength = search.Length;
+ var replaceLength = replace.Length;
+ var newLength = str.Length - searchLength + replaceLength;
+
+ Span buffer = newLength <= 1024 ? stackalloc char[newLength] : new char[newLength];
+
+ // Copy the part of the original string before the search term
+ str.AsSpan(0, pos).CopyTo(buffer);
+
+ // Copy the replacement text
+ replace.AsSpan().CopyTo(buffer.Slice(pos));
+
+ // Copy the remainder of the original string
+ str.AsSpan(pos + searchLength).CopyTo(buffer.Slice(pos + replaceLength));
+
+ return buffer.ToString();
}
///
diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
index 9cf92a3f77..2ead0da1f3 100644
--- a/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
+++ b/framework/src/Volo.Abp.EntityFrameworkCore.Oracle.Devart/Volo.Abp.EntityFrameworkCore.Oracle.Devart.csproj
@@ -19,8 +19,7 @@
-
-
+
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenu.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenu.cs
index 1a69de7f2c..9f48aebf97 100644
--- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenu.cs
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenu.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Data;
@@ -5,7 +6,7 @@ using Volo.Abp.UI.Navigation;
namespace Volo.Abp.UI.Navigation;
-public class ApplicationMenu : IHasMenuItems
+public class ApplicationMenu : IHasMenuItems, IHasMenuGroups
{
///
/// Unique name of the menu in the application.
@@ -31,6 +32,10 @@ public class ApplicationMenu : IHasMenuItems
[NotNull]
public ApplicationMenuItemList Items { get; }
+ ///
+ [NotNull]
+ public ApplicationMenuGroupList Groups { get; }
+
///
/// Can be used to store a custom object related to this menu.
///
@@ -47,6 +52,7 @@ public class ApplicationMenu : IHasMenuItems
DisplayName = displayName ?? Name;
Items = new ApplicationMenuItemList();
+ Groups = new ApplicationMenuGroupList();
}
///
@@ -60,6 +66,17 @@ public class ApplicationMenu : IHasMenuItems
return this;
}
+ ///
+ /// Adds a to .
+ ///
+ /// to be added
+ /// This object
+ public ApplicationMenu AddGroup([NotNull] ApplicationMenuGroup group)
+ {
+ Groups.Add(group);
+ return this;
+ }
+
///
/// Adds a custom data item to with given key & value.
///
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuExtensions.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuExtensions.cs
index eec549cf9c..624abde425 100644
--- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuExtensions.cs
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuExtensions.cs
@@ -64,4 +64,54 @@ public static class ApplicationMenuExtensions
return menuWithItems;
}
+
+ [NotNull]
+ public static ApplicationMenuGroup GetMenuGroup(
+ [NotNull] this IHasMenuGroups menuWithGroups,
+ string groupName)
+ {
+ var menuGroup = menuWithGroups.GetMenuGroupOrNull(groupName);
+ if (menuGroup == null)
+ {
+ throw new AbpException($"Could not find a group item with given name: {groupName}");
+ }
+
+ return menuGroup;
+ }
+
+ [CanBeNull]
+ public static ApplicationMenuGroup GetMenuGroupOrNull(
+ [NotNull] this IHasMenuGroups menuWithGroups,
+ string menuGroupName)
+ {
+ Check.NotNull(menuWithGroups, nameof(menuWithGroups));
+
+ return menuWithGroups.Groups.FirstOrDefault(group => group.Name == menuGroupName);
+ }
+
+ public static bool TryRemoveMenuGroup(
+ [NotNull] this IHasMenuGroups menuWithGroups,
+ string menuGroupName)
+ {
+ Check.NotNull(menuWithGroups, nameof(menuWithGroups));
+
+ return menuWithGroups.Groups.RemoveAll(group => group.Name == menuGroupName) > 0;
+ }
+
+ [NotNull]
+ public static IHasMenuGroups SetMenuGroupOrder(
+ [NotNull] this IHasMenuGroups menuWithGroups,
+ string menuGroupName,
+ int order)
+ {
+ Check.NotNull(menuWithGroups, nameof(menuWithGroups));
+
+ var menuGroup = menuWithGroups.GetMenuGroupOrNull(menuGroupName);
+ if (menuGroup != null)
+ {
+ menuGroup.Order = order;
+ }
+
+ return menuWithGroups;
+ }
}
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuGroup.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuGroup.cs
new file mode 100644
index 0000000000..469c0ad355
--- /dev/null
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuGroup.cs
@@ -0,0 +1,67 @@
+using JetBrains.Annotations;
+
+namespace Volo.Abp.UI.Navigation;
+
+public class ApplicationMenuGroup
+{
+ private string _displayName;
+
+ ///
+ /// Default value of a group item.
+ ///
+ public const int DefaultOrder = 1000;
+
+ ///
+ /// Unique name of the group in the application.
+ ///
+ [NotNull]
+ public string Name { get; }
+
+ ///
+ /// Display name of the group.
+ ///
+ [NotNull]
+ public string DisplayName {
+ get { return _displayName; }
+ set {
+ Check.NotNullOrWhiteSpace(value, nameof(value));
+ _displayName = value;
+ }
+ }
+
+ ///
+ /// Can be used to render the element with a specific Id for DOM selections.
+ ///
+ public string ElementId { get; set; }
+
+ ///
+ /// The Display order of the group.
+ /// Default value: 1000.
+ ///
+ public int Order { get; set; }
+
+ public ApplicationMenuGroup(
+ [NotNull] string name,
+ [NotNull] string displayName,
+ string elementId = null,
+ int order = DefaultOrder)
+ {
+ Check.NotNullOrWhiteSpace(name, nameof(name));
+ Check.NotNullOrWhiteSpace(displayName, nameof(displayName));
+
+ Name = name;
+ DisplayName = displayName;
+ ElementId = elementId;
+ Order = order;
+ }
+
+ private string GetDefaultElementId()
+ {
+ return "MenuGroup_" + Name;
+ }
+
+ public override string ToString()
+ {
+ return $"[ApplicationMenuGroup] Name = {Name}";
+ }
+}
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuGroupList.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuGroupList.cs
new file mode 100644
index 0000000000..f280f5d4f9
--- /dev/null
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuGroupList.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Volo.Abp.UI.Navigation;
+
+public class ApplicationMenuGroupList: List
+{
+ public ApplicationMenuGroupList()
+ {
+
+ }
+
+ public ApplicationMenuGroupList(int capacity)
+ : base(capacity)
+ {
+
+ }
+
+ public ApplicationMenuGroupList(IEnumerable collection)
+ : base(collection)
+ {
+
+ }
+
+ public void Normalize()
+ {
+ Order();
+ }
+
+ private void Order()
+ {
+ var orderedItems = this.OrderBy(item => item.Order).ToArray();
+ Clear();
+ AddRange(orderedItems);
+ }
+}
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs
index 32241ba7ac..dc1614e3e5 100644
--- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/ApplicationMenuItem.cs
@@ -92,6 +92,11 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers
public string CssClass { get; set; }
+ ///
+ /// Can be used to group menu items.
+ ///
+ public string GroupName { get; set; }
+
public ApplicationMenuItem(
[NotNull] string name,
[NotNull] string displayName,
@@ -101,6 +106,7 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers>();
Items = new ApplicationMenuItemList();
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IHasMenuGroups.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IHasMenuGroups.cs
new file mode 100644
index 0000000000..fbbe7a63a4
--- /dev/null
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/IHasMenuGroups.cs
@@ -0,0 +1,9 @@
+namespace Volo.Abp.UI.Navigation;
+
+public interface IHasMenuGroups
+{
+ ///
+ /// Menu groups.
+ ///
+ ApplicationMenuGroupList Groups { get; }
+}
diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs
index aa933c6e09..349aa8e5a5 100644
--- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs
+++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/MenuManager.cs
@@ -96,6 +96,7 @@ public class MenuManager : IMenuManager, ITransientDependency
}
NormalizeMenu(menu);
+ NormalizeMenuGroup(menu);
return menu;
}
@@ -159,4 +160,23 @@ public class MenuManager : IMenuManager, ITransientDependency
menuWithItems.Items.Normalize();
}
+
+ protected virtual void NormalizeMenuGroup(ApplicationMenu applicationMenu)
+ {
+ foreach (var menuGroup in applicationMenu.Items.Where(x => !x.GroupName.IsNullOrWhiteSpace()).GroupBy(x => x.GroupName))
+ {
+ var group = applicationMenu.GetMenuGroupOrNull(menuGroup.First().GroupName);
+ if (group != null)
+ {
+ continue;
+ }
+
+ foreach (var menuItem in menuGroup)
+ {
+ menuItem.GroupName = null;
+ }
+ }
+
+ applicationMenu.Groups.Normalize();
+ }
}
diff --git a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AbpUiNavigationTestModule.cs b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AbpUiNavigationTestModule.cs
index 6088984cb9..c482963d75 100644
--- a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AbpUiNavigationTestModule.cs
+++ b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/AbpUiNavigationTestModule.cs
@@ -16,6 +16,7 @@ public class AbpUiNavigationTestModule : AbpModule
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor1());
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor2());
options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor3());
+ options.MenuContributors.Add(new MenuManager_Tests.TestMenuContributor4());
options.MainMenuNames.Add(MenuManager_Tests.TestMenuContributor3.MenuName);
});
diff --git a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs
index b27de60c10..421e072b0f 100644
--- a/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs
+++ b/framework/test/Volo.Abp.UI.Navigation.Tests/Volo/Abp/Ui/Navigation/MenuManager_Tests.cs
@@ -46,7 +46,7 @@ public class MenuManager_Tests : AbpIntegratedTest
mainMenu.Name.ShouldBe(StandardMenus.Main);
mainMenu.DisplayName.ShouldBe("Main Menu");
- mainMenu.Items.Count.ShouldBe(2);
+ mainMenu.Items.Count.ShouldBe(5);
mainMenu.Items[0].Name.ShouldBe("Dashboard");
mainMenu.Items[1].Name.ShouldBe(DefaultMenuNames.Application.Main.Administration);
mainMenu.Items[1].Items[0].Name.ShouldBe("Administration.UserManagement");
@@ -63,12 +63,29 @@ public class MenuManager_Tests : AbpIntegratedTest
mainMenu.Name.ShouldBe(StandardMenus.Main);
- mainMenu.Items.Count.ShouldBe(3);
+ mainMenu.Items.Count.ShouldBe(6);
mainMenu.Items.ShouldContain(x => x.Name == "Products");
mainMenu.Items.ShouldContain(x => x.Name == "Dashboard");
}
+ [Fact]
+ public async Task GetMainMenuAsync_GroupMenuItems()
+ {
+ var mainMenu = await _menuManager.GetMainMenuAsync();
+
+ mainMenu.Name.ShouldBe(StandardMenus.Main);
+ mainMenu.Items.Count.ShouldBe(6);
+
+ mainMenu.Items[2].GroupName.ShouldBe("Layouts");
+ mainMenu.Items[3].GroupName.ShouldBe("Layouts");
+ mainMenu.Items[4].GroupName.ShouldBe(null); // No group defined
+
+ var layoutsGroup = mainMenu.GetMenuGroup("Layouts");
+ layoutsGroup.Name.ShouldBe("Layouts");
+ layoutsGroup.DisplayName.ShouldBe("Layouts");
+ }
+
/* Adds menu items:
* - Administration
* - User Management
@@ -149,4 +166,29 @@ public class MenuManager_Tests : AbpIntegratedTest
return Task.CompletedTask;
}
}
+
+ /* Adds group and menu items:
+ * - Layouts
+ * - Toolbars
+ * - Page Header
+ */
+ public class TestMenuContributor4 : IMenuContributor
+ {
+ public Task ConfigureMenuAsync(MenuConfigurationContext context)
+ {
+ if (context.Menu.Name != StandardMenus.Main)
+ {
+ return Task.CompletedTask;
+ }
+
+ context.Menu.AddGroup(new ApplicationMenuGroup("Layouts", "Layouts"));
+
+ context.Menu.AddItem(new ApplicationMenuItem("Toolbars", "Toolbars", url: "/layouts/toolbars", groupName: "Layouts"));
+ context.Menu.AddItem(new ApplicationMenuItem("PageHeader", "Page Header", url: "/layouts/page-header", groupName: "Layouts"));
+
+ context.Menu.AddItem(new ApplicationMenuItem("Branding", "Branding", url: "/layouts/branding", groupName: "NotDefinedGroup"));
+
+ return Task.CompletedTask;
+ }
+ }
}
diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
index d2a049dd84..d8b3543703 100644
--- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityUserRepository.cs
@@ -125,4 +125,10 @@ public interface IIdentityUserRepository : IBasicRepository
bool includeDetails = true,
CancellationToken cancellationToken = default
);
+
+ Task> GetListByIdsAsync(
+ IEnumerable ids,
+ bool includeDetails = false,
+ CancellationToken cancellationToken = default
+ );
}
diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs
index 6a0245fa4f..035904120c 100644
--- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityLinkUserRepository.cs
@@ -20,6 +20,7 @@ public class EfCoreIdentityLinkUserRepository : EfCoreRepository FindAsync(IdentityLinkUserInfo sourceLinkUserInfo, IdentityLinkUserInfo targetLinkUserInfo, CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync())
+ .AsNoTracking()
.OrderBy(x => x.Id).FirstOrDefaultAsync(x =>
x.SourceUserId == sourceLinkUserInfo.UserId && x.SourceTenantId == sourceLinkUserInfo.TenantId &&
x.TargetUserId == targetLinkUserInfo.UserId && x.TargetTenantId == targetLinkUserInfo.TenantId ||
@@ -31,7 +32,8 @@ public class EfCoreIdentityLinkUserRepository : EfCoreRepository> GetListAsync(IdentityLinkUserInfo linkUserInfo, List excludes = null,
CancellationToken cancellationToken = default)
{
- IQueryable query = (await GetDbSetAsync())
+ var query = (await GetDbSetAsync())
+ .AsNoTracking()
.Where(x =>
x.SourceUserId == linkUserInfo.UserId && x.SourceTenantId == linkUserInfo.TenantId ||
x.TargetUserId == linkUserInfo.UserId && x.TargetTenantId == linkUserInfo.TenantId);
@@ -51,7 +53,7 @@ public class EfCoreIdentityLinkUserRepository : EfCoreRepository
+ var linkUsers = await (await GetDbSetAsync()).AsNoTracking().Where(x =>
x.SourceUserId == linkUserInfo.UserId && x.SourceTenantId == linkUserInfo.TenantId ||
x.TargetUserId == linkUserInfo.UserId && x.TargetTenantId == linkUserInfo.TenantId)
.ToListAsync(cancellationToken: GetCancellationToken(cancellationToken));
diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
index e2527555b2..60abbb3054 100644
--- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs
@@ -348,4 +348,12 @@ public class EfCoreIdentityUserRepository : EfCoreRepository> GetListByIdsAsync(IEnumerable ids, bool includeDetails = false, CancellationToken cancellationToken = default)
+ {
+ return await (await GetDbSetAsync())
+ .IncludeDetails(includeDetails)
+ .Where(x => ids.Contains(x.Id))
+ .ToListAsync(GetCancellationToken(cancellationToken));
+ }
}
diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
index ac2ab63809..ae805d9a03 100644
--- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
+++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs
@@ -316,4 +316,11 @@ public class MongoIdentityUserRepository : MongoDbRepository> GetListByIdsAsync(IEnumerable ids, bool includeDetails = false, CancellationToken cancellationToken = default)
+ {
+ return await (await GetMongoQueryableAsync(cancellationToken))
+ .Where(x => ids.Contains(x.Id))
+ .ToListAsync(GetCancellationToken(cancellationToken));
+ }
}
diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts
index 209d2a881e..fefc9931eb 100644
--- a/npm/ng-packs/packages/core/src/lib/services/index.ts
+++ b/npm/ng-packs/packages/core/src/lib/services/index.ts
@@ -18,3 +18,4 @@ export * from './routes.service';
export * from './session-state.service';
export * from './subscription.service';
export * from './track-by.service';
+export * from './local-storage.service';
diff --git a/npm/ng-packs/packages/core/src/lib/services/local-storage.service.ts b/npm/ng-packs/packages/core/src/lib/services/local-storage.service.ts
new file mode 100644
index 0000000000..15ae8d53a3
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/services/local-storage.service.ts
@@ -0,0 +1,28 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class AbpLocalStorageService implements Storage {
+ constructor() {}
+ [name: string]: any;
+ get length(): number {
+ return localStorage.length;
+ }
+
+ clear(): void {
+ localStorage.clear();
+ }
+ getItem(key: string): string {
+ return localStorage.getItem(key);
+ }
+ key(index: number): string {
+ return localStorage.key(index);
+ }
+ removeItem(key: string): void {
+ localStorage.removeItem(key);
+ }
+ setItem(key: string, value: string): void {
+ localStorage.setItem(key, value);
+ }
+}
diff --git a/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts b/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts
index d3bac9a338..36f7c9e94a 100644
--- a/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts
+++ b/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts
@@ -5,6 +5,7 @@ import { Session } from '../models/session';
import { CurrentTenantDto } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models';
import { InternalStore } from '../utils/internal-store-utils';
import { ConfigStateService } from './config-state.service';
+import { AbpLocalStorageService } from './local-storage.service';
@Injectable({
providedIn: 'root',
@@ -13,16 +14,19 @@ export class SessionStateService {
private readonly store = new InternalStore({} as Session.State);
private updateLocalStorage = () => {
- localStorage.setItem('abpSession', JSON.stringify(this.store.state));
+ this.localStorageService.setItem('abpSession', JSON.stringify(this.store.state));
};
- constructor(private configState: ConfigStateService) {
+ constructor(
+ private configState: ConfigStateService,
+ private localStorageService: AbpLocalStorageService,
+ ) {
this.init();
this.setInitialLanguage();
}
private init() {
- const session = localStorage.getItem('abpSession');
+ const session = this.localStorageService.getItem('abpSession');
if (session) {
this.store.set(JSON.parse(session));
}
diff --git a/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts
new file mode 100644
index 0000000000..ad51bbda2b
--- /dev/null
+++ b/npm/ng-packs/packages/core/src/lib/tests/local-storage.service.spec.ts
@@ -0,0 +1,52 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AbpLocalStorageService } from '../services/local-storage.service';
+
+describe('LocalStorageService', () => {
+ let service: AbpLocalStorageService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(AbpLocalStorageService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ it('should be called getItem', () => {
+ const spy = jest.spyOn(service, 'getItem');
+ service.getItem('test');
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should be called setItem', () => {
+ const spy = jest.spyOn(service, 'setItem');
+ service.setItem('test', 'value');
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should be called removeItem', () => {
+ const spy = jest.spyOn(service, 'removeItem');
+ service.removeItem('test');
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should be called clear', () => {
+ const spy = jest.spyOn(service, 'clear');
+ service.clear();
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should be called key', () => {
+ const spy = jest.spyOn(service, 'key');
+ service.key(0);
+ expect(spy).toHaveBeenCalled();
+ });
+
+ it('should be called length', () => {
+ const spy = jest.spyOn(service, 'length', 'get');
+ service.length;
+ expect(spy).toHaveBeenCalled();
+ });
+});
diff --git a/npm/ng-packs/packages/oauth/src/lib/oauth.module.ts b/npm/ng-packs/packages/oauth/src/lib/oauth.module.ts
index 5680b9582d..ac800e7c8f 100644
--- a/npm/ng-packs/packages/oauth/src/lib/oauth.module.ts
+++ b/npm/ng-packs/packages/oauth/src/lib/oauth.module.ts
@@ -2,14 +2,14 @@ import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angul
import { CommonModule } from '@angular/common';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import {
+ AbpLocalStorageService,
ApiInterceptor,
AuthGuard,
AuthService,
CHECK_AUTHENTICATION_STATE_FN_KEY,
noop,
- PIPE_TO_LOGIN_FN_KEY
+ PIPE_TO_LOGIN_FN_KEY,
} from '@abp/ng.core';
-import { storageFactory } from './utils/storage.factory';
import { AbpOAuthService } from './services';
import { OAuthConfigurationHandler } from './handlers/oauth-configuration.handler';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
@@ -59,7 +59,7 @@ export class AbpOAuthModule {
useFactory: noop,
},
OAuthModule.forRoot().providers as Provider[],
- { provide: OAuthStorage, useFactory: storageFactory },
+ { provide: OAuthStorage, useClass: AbpLocalStorageService },
],
};
}
diff --git a/npm/ng-packs/packages/oauth/src/lib/providers/navigate-to-manage-profile.provider.ts b/npm/ng-packs/packages/oauth/src/lib/providers/navigate-to-manage-profile.provider.ts
index e20bf62fc2..e1c6d458a4 100644
--- a/npm/ng-packs/packages/oauth/src/lib/providers/navigate-to-manage-profile.provider.ts
+++ b/npm/ng-packs/packages/oauth/src/lib/providers/navigate-to-manage-profile.provider.ts
@@ -12,10 +12,10 @@ export const NavigateToManageProfileProvider: Provider = {
console.warn('The oAuthConfig env is missing on environment.ts');
return;
}
- window.open(
- `${env.oAuthConfig.issuer}/Account/Manage?returnUrl=${window.location.href}`,
- '_self',
- );
+
+ const { issuer } = env.oAuthConfig;
+ const path = issuer.endsWith('/') ? issuer : `${issuer}/`;
+ window.open(`${path}Account/Manage?returnUrl=${window.location.href}`, '_self');
};
},
};
diff --git a/npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts b/npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts
index 7a07cb8985..c1bf50e0b5 100644
--- a/npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts
+++ b/npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts
@@ -9,6 +9,7 @@ import {
import { Observable, of } from 'rxjs';
import { filter, switchMap, tap } from 'rxjs/operators';
import {
+ AbpLocalStorageService,
ConfigStateService,
EnvironmentService,
HttpErrorReporterService,
@@ -29,6 +30,7 @@ export abstract class AuthFlowStrategy {
protected oAuthService: OAuthService2;
protected oAuthConfig!: AuthConfig;
protected sessionState: SessionStateService;
+ protected localStorageService: AbpLocalStorageService;
protected tenantKey: string;
abstract checkIfInternalAuth(queryParams?: Params): boolean;
@@ -50,6 +52,7 @@ export abstract class AuthFlowStrategy {
this.configState = injector.get(ConfigStateService);
this.oAuthService = injector.get(OAuthService2);
this.sessionState = injector.get(SessionStateService);
+ this.localStorageService = injector.get(AbpLocalStorageService);
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig || {};
this.tenantKey = injector.get(TENANT_KEY);
diff --git a/npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts b/npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts
index 2e2e278985..fab747fe90 100644
--- a/npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts
+++ b/npm/ng-packs/packages/oauth/src/lib/strategies/auth-password-flow-strategy.ts
@@ -4,7 +4,7 @@ import { Params, Router } from '@angular/router';
import { from, Observable, pipe } from 'rxjs';
import { HttpHeaders } from '@angular/common/http';
import { AuthFlowStrategy } from './auth-flow-strategy';
-import { pipeToLogin, removeRememberMe, setRememberMe } from '../utils/auth-utils';
+import { pipeToLogin, removeRememberMe } from '../utils/auth-utils';
import { LoginParams } from '@abp/ng.core';
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
@@ -33,7 +33,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
this.refreshToken();
} else {
this.oAuthService.logOut();
- removeRememberMe();
+ removeRememberMe(this.localStorageService);
this.configState.refreshAppState().subscribe();
}
});
@@ -74,7 +74,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
switchMap(() => this.configState.refreshAppState()),
tap(() => {
router.navigateByUrl('/');
- removeRememberMe();
+ removeRememberMe(this.localStorageService);
}),
);
}
@@ -82,7 +82,7 @@ export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
protected refreshToken() {
return this.oAuthService.refreshToken().catch(() => {
clearOAuthStorage();
- removeRememberMe();
+ removeRememberMe(this.localStorageService);
});
}
}
diff --git a/npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts b/npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts
index 24f94fe129..f1dc5b6106 100644
--- a/npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts
+++ b/npm/ng-packs/packages/oauth/src/lib/utils/auth-utils.ts
@@ -7,7 +7,7 @@ import {
ConfigStateService,
LoginParams,
PipeToLoginFn,
- SetTokenResponseToStorageFn,
+ AbpLocalStorageService,
} from '@abp/ng.core';
const cookieKey = 'rememberMe';
@@ -19,25 +19,28 @@ export const pipeToLogin: PipeToLoginFn = function (
) {
const configState = injector.get(ConfigStateService);
const router = injector.get(Router);
-
+ const localStorage = injector.get(AbpLocalStorageService);
return pipe(
switchMap(() => configState.refreshAppState()),
tap(() => {
- setRememberMe(params.rememberMe);
+ setRememberMe(params.rememberMe, localStorage);
if (params.redirectUrl) router.navigate([params.redirectUrl]);
}),
);
};
-export function setRememberMe(remember: boolean | undefined) {
- removeRememberMe();
- localStorage.setItem(storageKey, 'true');
+export function setRememberMe(
+ remember: boolean | undefined,
+ localStorageService: AbpLocalStorageService,
+) {
+ removeRememberMe(localStorageService);
+ localStorageService.setItem(storageKey, 'true');
document.cookie = `${cookieKey}=true; path=/${
remember ? ' ;expires=Fri, 31 Dec 9999 23:59:59 GMT' : ''
}`;
}
-export function removeRememberMe() {
- localStorage.removeItem(storageKey);
+export function removeRememberMe(localStorageService: AbpLocalStorageService) {
+ localStorageService.removeItem(storageKey);
document.cookie = cookieKey + '= ; path=/; expires = Thu, 01 Jan 1970 00:00:00 GMT';
}
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts
index 095af5ef26..238bc19589 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts
+++ b/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts
@@ -12,3 +12,5 @@ export * from './toast-container/toast-container.component';
export * from './toast/toast.component';
export * from './password/password.component';
export * from './card/index';
+export * from './checkbox/checkbox.component';
+export * from './form-input/form-input.component';
diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs
index 1033c73a57..fa55b7d44f 100644
--- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs
+++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs
@@ -4,6 +4,7 @@ using Blazorise.Bootstrap5;
using Blazorise.Icons.FontAwesome;
using Medallion.Threading;
using Medallion.Threading.Redis;
+using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
@@ -163,6 +164,7 @@ public class MyProjectNameBlazorModule : AbpModule
.AddCookie("Cookies", options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(365);
+ options.IntrospectAccessToken();
})
.AddAbpOpenIdConnect("oidc", options =>
{
diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreTestModule.cs b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreTestModule.cs
index 9cf9342994..2f5d456244 100644
--- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreTestModule.cs
+++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/EntityFrameworkCore/MyProjectNameEntityFrameworkCoreTestModule.cs
@@ -6,7 +6,9 @@ using Microsoft.Extensions.DependencyInjection;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.Sqlite;
+using Volo.Abp.FeatureManagement;
using Volo.Abp.Modularity;
+using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
namespace MyCompanyName.MyProjectName.EntityFrameworkCore;
@@ -22,7 +24,18 @@ public class MyProjectNameEntityFrameworkCoreTestModule : AbpModule
public override void ConfigureServices(ServiceConfigurationContext context)
{
+ Configure(options =>
+ {
+ options.SaveStaticFeaturesToDatabase = false;
+ options.IsDynamicFeatureStoreEnabled = false;
+ });
+ Configure(options =>
+ {
+ options.SaveStaticPermissionsToDatabase = false;
+ options.IsDynamicPermissionStoreEnabled = false;
+ });
context.Services.AddAlwaysDisableUnitOfWorkTransaction();
+
ConfigureInMemorySqlite(context.Services);
}