diff --git a/docs/en/UI/Angular/Account-Module.md b/docs/en/UI/Angular/Account-Module.md index 3b2363361a..d3934ab18b 100644 --- a/docs/en/UI/Angular/Account-Module.md +++ b/docs/en/UI/Angular/Account-Module.md @@ -95,6 +95,24 @@ export class AppRoutingModule {} Before v4.3, the "My account" link in the current user dropdown on the top bar redirected the user to MVC's profile management page. As of v4.3, if you added the account module to your project, the same link will land on a page in the Angular UI account module instead. +### Personal Info Page Confirm Message + +When the user changes their own data on the personal settings tab in My Account, The data can not update the CurrentUser key of Application-Configuration. The information of the user is stored in claims. The only way to apply this information to the CurrentUser of Application-Configuration is user should log out and log in. When the Refresh-Token feature is implemented, it will be fixed. So We've added a confirmation alert. + +If you want to disable these warning, You should set `isPersonalSettingsChangedConfirmationActive` false + +```js +// app-routing.module.ts +const routes: Routes = [ + //... + { + path: 'account', + loadChildren: () => import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy({ isPersonalSettingsChangedConfirmationActive:false })), + }, + //... +export class AppRoutingModule {} +``` + ### Security Logs Page [COMMERCIAL] Before v4.3, the "Security Logs" link in the current user dropdown on the top bar redirected the user to MVC's security logs page. As of v4.3, if you added the account module to your project, the same link will land on a page in the Angular UI account public module instead. diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs index 5c955ca005..8f6cc2abf8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs @@ -98,7 +98,15 @@ public class AbpUowActionFilter : IAsyncActionFilter, ITransientDependency var currentUow = unitOfWorkManager.Current; if (currentUow != null) { - await currentUow.SaveChangesAsync(context.HttpContext.RequestAborted); + try + { + await currentUow.SaveChangesAsync(context.HttpContext.RequestAborted); + } + catch (Exception e) + { + await currentUow.RollbackAsync(context.HttpContext.RequestAborted); + throw; + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs index af966fedce..57e1df8558 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs @@ -104,7 +104,15 @@ public class AbpUowPageFilter : IAsyncPageFilter, ITransientDependency var currentUow = unitOfWorkManager.Current; if (currentUow != null) { - await currentUow.SaveChangesAsync(context.HttpContext.RequestAborted); + try + { + await currentUow.SaveChangesAsync(context.HttpContext.RequestAborted); + } + catch (Exception e) + { + await currentUow.RollbackAsync(context.HttpContext.RequestAborted); + throw; + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/BundlingService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/BundlingService.cs index 65212fe69f..5f6f4a392d 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/BundlingService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/BundlingService.cs @@ -108,15 +108,15 @@ public class BundlingService : IBundlingService, ITransientDependency Parameters = parameters }; + scriptContext.BundleDefinitions.AddIfNotContains( + x => x.Source == "_framework/blazor.webassembly.js", + () => new BundleDefinition { Source = "_framework/blazor.webassembly.js" }); + foreach (var bundleDefinition in bundleDefinitions) { var contributor = CreateContributorInstance(bundleDefinition.BundleContributorType); contributor.AddScripts(scriptContext); } - - scriptContext.BundleDefinitions.AddIfNotContains( - x => x.Source == "_framework/blazor.webassembly.js", - () => new BundleDefinition { Source = "_framework/blazor.webassembly.js" }); return scriptContext; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs index a9fb23da7f..6a4e750880 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Licensing/DeveloperApiKeyResult.cs @@ -13,6 +13,7 @@ public class DeveloperApiKeyResult public string ErrorMessage { get; set; } public LicenseErrorType? ErrorType { get; set; } public LicenseType LicenseType { get; set; } + public bool IsTrialLicense { get; set; } public enum LicenseErrorType { diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/DatabaseManagementSystemChangeStep.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/DatabaseManagementSystemChangeStep.cs index f014942486..56a77c4929 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/DatabaseManagementSystemChangeStep.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/DatabaseManagementSystemChangeStep.cs @@ -98,7 +98,13 @@ public class DatabaseManagementSystemChangeStep : ProjectBuildPipelineStep var oldUseMethod = "UseSqlServer"; - var efCoreModuleClass = context.Files.First(f => f.Name.EndsWith("EntityFrameworkCoreModule.cs", StringComparison.OrdinalIgnoreCase)); + var efCoreModuleClass = context.Files.FirstOrDefault(f => f.Name.EndsWith("EntityFrameworkCoreModule.cs", StringComparison.OrdinalIgnoreCase)); + + if(efCoreModuleClass == null) + { + return; + } + efCoreModuleClass.ReplaceText(oldUseMethod, newUseMethodForEfModule); var dbContextFactoryFile = context.Files.FirstOrDefault(f => f.Name.EndsWith($"{(_hasDbMigrations ? "Migrations" : string.Empty)}DbContextFactoryBase.cs", StringComparison.OrdinalIgnoreCase)) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Files/FileEntryExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Files/FileEntryExtensions.cs index b209d54b93..98ddb005a9 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Files/FileEntryExtensions.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Files/FileEntryExtensions.cs @@ -120,7 +120,7 @@ public static class FileEntryExtensions } } - if ((i + 1 < lines.Length) && (lines[i + 1].Contains(" - + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionHandingUnitOfWork.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionHandingUnitOfWork.cs new file mode 100644 index 0000000000..94cef2e240 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionHandingUnitOfWork.cs @@ -0,0 +1,32 @@ +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.Data; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling; + +public class ExceptionHandingUnitOfWork : UnitOfWork +{ + public ExceptionHandingUnitOfWork( + IServiceProvider serviceProvider, + IUnitOfWorkEventPublisher unitOfWorkEventPublisher, + IOptions options) + : base(serviceProvider, unitOfWorkEventPublisher, options) + { + + } + public async override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + if (ServiceProvider.GetRequiredService().Id == Guid.Empty) + { + throw new AbpDbConcurrencyException(); + } + + await base.SaveChangesAsync(cancellationToken); + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController.cs index 0b5224ce8c..07daeb6d03 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Volo.Abp.Authorization; namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling; @@ -26,4 +27,11 @@ public class ExceptionTestController : AbpController { throw new AbpAuthorizationException("This is a sample exception!"); } + + [HttpGet] + [Route("ExceptionOnUowSaveChange")] + public Task ExceptionOnUowSaveChangeAsync() + { + return Task.FromResult("OK"); + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs index 8190547bb1..2f10f4b027 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestController_Tests.cs @@ -3,12 +3,14 @@ using System.Net; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using NSubstitute; using Shouldly; using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Security.Claims; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling; @@ -31,6 +33,8 @@ public class ExceptionTestController_Tests : AspNetCoreMvcTestBase _fakeExceptionSubscriber = Substitute.For(); services.AddSingleton(_fakeExceptionSubscriber); + + services.Replace(ServiceDescriptor.Transient()); } [Fact] @@ -93,4 +97,24 @@ public class ExceptionTestController_Tests : AspNetCoreMvcTestBase .HandleAsync(Arg.Any()); #pragma warning restore 4014 } + + [Fact] + public async Task Should_Handle_Exception_On_Uow_SaveChangeAsync() + { + FakeRequiredService.Claims.AddRange(new[] + { + new Claim(AbpClaimTypes.UserId, Guid.Empty.ToString()) + }); + + var result = await GetResponseAsObjectAsync("/api/exception-test/ExceptionOnUowSaveChange", HttpStatusCode.Conflict); + result.Error.ShouldNotBeNull(); + result.Error.Message.ShouldBe("The data you have submitted has already changed by another user/client. Please discard the changes you've done and try from the beginning."); + + #pragma warning disable 4014 + _fakeExceptionSubscriber + .Received() + .HandleAsync(Arg.Any()); + #pragma warning restore 4014 + + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml.cs index 087b75c3e0..a4da13dc6b 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage.cshtml.cs @@ -36,4 +36,9 @@ public class ExceptionTestPage : AbpPageModel { throw new AbpAuthorizationException("This is a sample exception!"); } + + public Task OnGetExceptionOnUowSaveChangeAsync() + { + return Task.FromResult(new JsonResult("OK")); + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage_Tests.cs index 316339239d..3290b37c95 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ExceptionHandling/ExceptionTestPage_Tests.cs @@ -3,12 +3,14 @@ using System.Net; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using NSubstitute; using Shouldly; using Volo.Abp.ExceptionHandling; using Volo.Abp.Http; using Volo.Abp.Security.Claims; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.AspNetCore.Mvc.ExceptionHandling; @@ -31,6 +33,8 @@ public class ExceptionTestPage_Tests : AspNetCoreMvcTestBase _fakeExceptionSubscriber = Substitute.For(); services.AddSingleton(_fakeExceptionSubscriber); + + services.Replace(ServiceDescriptor.Transient()); } [Fact] @@ -140,4 +144,25 @@ public class ExceptionTestPage_Tests : AspNetCoreMvcTestBase .HandleAsync(Arg.Any()); #pragma warning restore 4014 } + + + [Fact] + public async Task Should_Handle_Exception_On_Uow_SaveChangeAsync() + { + _fakeRequiredService.Claims.AddRange(new[] + { + new Claim(AbpClaimTypes.UserId, Guid.Empty.ToString()) + }); + + var result = await GetResponseAsObjectAsync("/api/exception-test/ExceptionOnUowSaveChange", HttpStatusCode.Conflict); + result.Error.ShouldNotBeNull(); + result.Error.Message.ShouldBe("The data you have submitted has already changed by another user/client. Please discard the changes you've done and try from the beginning."); + +#pragma warning disable 4014 + _fakeExceptionSubscriber + .Received() + .HandleAsync(Arg.Any()); +#pragma warning restore 4014 + + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Exception_Rollback_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Exception_Rollback_Tests.cs index 5f525c93df..068accad6a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Exception_Rollback_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkMiddleware_Exception_Rollback_Tests.cs @@ -2,15 +2,23 @@ using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Shouldly; using Volo.Abp.Http; using Volo.Abp.Json; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.AspNetCore.Mvc.Uow; public class UnitOfWorkMiddleware_Exception_Rollback_Tests : AspNetCoreMvcTestBase { + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + services.Replace(ServiceDescriptor.Transient()); + } + [Fact] public async Task Should_Rollback_Transaction_For_Handled_Exceptions() { diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Exception_Rollback_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Exception_Rollback_Tests.cs index 4d51451fbc..f5e3bf2123 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Exception_Rollback_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/UnitOfWorkPageFilter_Exception_Rollback_Tests.cs @@ -2,15 +2,23 @@ using System.Net; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Shouldly; using Volo.Abp.Http; using Volo.Abp.Json; +using Volo.Abp.Uow; using Xunit; namespace Volo.Abp.AspNetCore.Mvc.Uow; public class UnitOfWorkPageFilter_Exception_Rollback_Tests : AspNetCoreMvcTestBase { + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + services.Replace(ServiceDescriptor.Transient()); + } + [Fact] public async Task Should_Rollback_Transaction_For_Handled_Exceptions() { diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json index a046aad12a..9bd2955f3e 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json @@ -34,6 +34,8 @@ "DisplayName:PhoneNumber": "Phone number", "PersonalSettings": "Personal settings", "PersonalSettingsSaved": "Personal settings saved", + "PersonalSettingsChangedConfirmationModalTitle": "Personal info changed", + "PersonalSettingsChangedConfirmationModalDescription": "If you want to apply these changes, you have to login. Do you want to log out?", "PasswordChanged": "Password changed", "NewPasswordConfirmFailed": "Please confirm the new password.", "NewPasswordSameAsOld": "New password must be different from the old one.", diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json index 116ddd2508..13ac03fe5d 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json @@ -34,6 +34,8 @@ "DisplayName:PhoneNumber": "Telefon numarası", "PersonalSettings": "Kişisel ayarlar", "PersonalSettingsSaved": "Kişisel ayarlar kaydedildi", + "PersonalSettingsChangedConfirmationModalTitle": "Personal info changed", + "PersonalSettingsChangedConfirmationModalDescription": "If you want to apply these changes, you have to login. Do you want to log out?", "PasswordChanged": "Şifre değiştirildi", "NewPasswordConfirmFailed": "Lütfen yeni şifreyi onaylayın.", "NewPasswordSameAsOld": "Yeni şifre eski şifre ile aynı olamaz.", diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/RedirectToLogin.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/RedirectToLogin.razor index 07fb5967b8..629e9e2bf3 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/RedirectToLogin.razor +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/RedirectToLogin.razor @@ -1,17 +1,10 @@ @inject NavigationManager Navigation -@inject IJSRuntime JSRuntime -@code { +@code { + protected override void OnInitialized() { - bool isWebAssembly = JSRuntime is IJSInProcessRuntime; - if (isWebAssembly) - { - Navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}"); - } - else - { - Navigation.NavigateTo($"account/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}", true); - } - } -} + Navigation.NavigateTo($"account/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}", true); + } + +} \ No newline at end of file diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs index bee01d369b..92577ff937 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/LoginDisplay.razor.cs @@ -18,9 +18,6 @@ public partial class LoginDisplay : IDisposable [Inject] public AuthenticationStateProvider AuthenticationStateProvider { get; set; } - [CanBeNull] - protected SignOutSessionStateManager SignOutManager; - protected ApplicationMenu Menu { get; set; } protected override async Task OnInitializedAsync() @@ -29,8 +26,6 @@ public partial class LoginDisplay : IDisposable Navigation.LocationChanged += OnLocationChanged; - LazyGetService(ref SignOutManager); - AuthenticationStateProvider.AuthenticationStateChanged += AuthenticationStateProviderOnAuthenticationStateChanged; } @@ -65,12 +60,8 @@ public partial class LoginDisplay : IDisposable } } - private async Task BeginSignOut() + private void BeginSignOut() { - if (SignOutManager != null) - { - await SignOutManager.SetSignOutState(); - await NavigateToAsync("authentication/logout"); - } + Navigation.NavigateToLogout("authentication/logout"); } } diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor new file mode 100644 index 0000000000..ebae71d62a --- /dev/null +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/WebAssemblyRedirectToLogin.razor @@ -0,0 +1,17 @@ +@inject NavigationManager Navigation + +@using Volo.Abp.DependencyInjection +@using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Themes.Basic +@using Microsoft.AspNetCore.Components.WebAssembly.Authentication +@inherits RedirectToLogin +@attribute [ExposeServices(typeof(RedirectToLogin))] +@attribute [Dependency(ReplaceServices = true)] + +@code { + + protected override void OnInitialized() + { + Navigation.NavigateToLogin("authentication/login"); + } + +} \ No newline at end of file diff --git a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile index 86f13e6ce9..a755a4b690 100644 --- a/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile +++ b/modules/basic-theme/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/aspnet:6.0.0-bullseye-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:7.0-bullseye-slim AS base WORKDIR /app EXPOSE 80 COPY bin/Release/publish . diff --git a/npm/ng-packs/packages/account/src/lib/account.module.ts b/npm/ng-packs/packages/account/src/lib/account.module.ts index bdf3c39792..304cc31242 100644 --- a/npm/ng-packs/packages/account/src/lib/account.module.ts +++ b/npm/ng-packs/packages/account/src/lib/account.module.ts @@ -15,6 +15,7 @@ import { accountConfigOptionsFactory } from './utils/factory-utils'; import { AuthenticationFlowGuard } from './guards/authentication-flow.guard'; import { ForgotPasswordComponent } from './components/forgot-password/forgot-password.component'; import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; +import { RE_LOGIN_CONFIRMATION_TOKEN } from './tokens'; import { UiExtensionsModule } from '@abp/ng.theme.shared/extensions'; import { ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS } from './tokens/extensions.token'; import { AccountExtensionsGuard } from './guards/extensions.guard'; @@ -55,6 +56,10 @@ export class AccountModule { useFactory: accountConfigOptionsFactory, deps: [ACCOUNT_CONFIG_OPTIONS], }, + { + provide: RE_LOGIN_CONFIRMATION_TOKEN, + useValue: options.isPersonalSettingsChangedConfirmationActive ?? true, + }, { provide: ACCOUNT_EDIT_FORM_PROP_CONTRIBUTORS, useValue: options.editFormPropContributors, diff --git a/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts b/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts index 2ce627a34f..54b1770ac5 100644 --- a/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings.component.ts @@ -1,10 +1,12 @@ import { ProfileDto, ProfileService } from '@abp/ng.account.core/proxy'; -import { ToasterService } from '@abp/ng.theme.shared'; -import { Component, Injector, OnInit, ViewEncapsulation } from '@angular/core'; +import { Confirmation, ConfirmationService, ToasterService } from '@abp/ng.theme.shared'; +import { Component, Inject, Injector, OnInit, ViewEncapsulation } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { finalize } from 'rxjs/operators'; +import { finalize, filter } from 'rxjs/operators'; import { Account } from '../../models/account'; import { ManageProfileStateService } from '../../services/manage-profile.state.service'; +import { AuthService } from '@abp/ng.core'; +import { RE_LOGIN_CONFIRMATION_TOKEN } from '../../tokens'; import { EXTENSIONS_IDENTIFIER, FormPropData, @@ -34,12 +36,17 @@ export class PersonalSettingsComponent form: UntypedFormGroup; inProgress: boolean; + private profile: ProfileDto; constructor( private fb: UntypedFormBuilder, private toasterService: ToasterService, private profileService: ProfileService, private manageProfileState: ManageProfileStateService, + private readonly authService: AuthService, + private confirmationService: ConfirmationService, + @Inject(RE_LOGIN_CONFIRMATION_TOKEN) + private isPersonalSettingsChangedConfirmationActive: boolean, protected injector: Injector, ) {} @@ -58,6 +65,7 @@ export class PersonalSettingsComponent submit() { if (this.form.invalid) return; + const isLogOutConfirmMessageVisible = this.isLogoutConfirmMessageActive(); this.inProgress = true; this.profileService .update(this.form.value) @@ -65,6 +73,39 @@ export class PersonalSettingsComponent .subscribe(profile => { this.manageProfileState.setProfile(profile); this.toasterService.success('AbpAccount::PersonalSettingsSaved', 'Success', { life: 5000 }); + if (isLogOutConfirmMessageVisible) { + this.showLogoutConfirmMessage(); + } }); } + + isDataSame(oldValue, newValue) { + return Object.entries(oldValue).some(([key, value]) => { + if (key in newValue) { + return value !== newValue[key]; + } + return false; + }); + } + + logoutConfirmation = () => { + this.authService.logout().subscribe(); + }; + + private isLogoutConfirmMessageActive() { + if (!this.isPersonalSettingsChangedConfirmationActive) { + return false; + } + return this.isDataSame(this.profile, this.form.value); + } + + private showLogoutConfirmMessage() { + this.confirmationService + .info( + 'AbpAccount::PersonalSettingsChangedConfirmationModalDescription', + 'AbpAccount::PersonalSettingsChangedConfirmationModalTitle', + ) + .pipe(filter(status => status === Confirmation.Status.confirm)) + .subscribe(this.logoutConfirmation); + } } diff --git a/npm/ng-packs/packages/account/src/lib/models/config-options.ts b/npm/ng-packs/packages/account/src/lib/models/config-options.ts index f3af14a71e..c45389492f 100644 --- a/npm/ng-packs/packages/account/src/lib/models/config-options.ts +++ b/npm/ng-packs/packages/account/src/lib/models/config-options.ts @@ -4,6 +4,7 @@ import { UpdateProfileDto } from '@abp/ng.account.core/proxy'; export interface AccountConfigOptions { redirectUrl?: string; + isPersonalSettingsChangedConfirmationActive?: boolean; editFormPropContributors?: AccountEditFormPropContributors; } export type AccountEditFormPropContributors = Partial<{ diff --git a/npm/ng-packs/packages/account/src/lib/tokens/index.ts b/npm/ng-packs/packages/account/src/lib/tokens/index.ts index 4381d1bc77..5c967057cf 100644 --- a/npm/ng-packs/packages/account/src/lib/tokens/index.ts +++ b/npm/ng-packs/packages/account/src/lib/tokens/index.ts @@ -1,2 +1,3 @@ export * from './config-options.token'; +export * from './re-login-confirmation.token'; export * from './extensions.token'; diff --git a/npm/ng-packs/packages/account/src/lib/tokens/re-login-confirmation.token.ts b/npm/ng-packs/packages/account/src/lib/tokens/re-login-confirmation.token.ts new file mode 100644 index 0000000000..8ca5375bab --- /dev/null +++ b/npm/ng-packs/packages/account/src/lib/tokens/re-login-confirmation.token.ts @@ -0,0 +1,5 @@ +import { InjectionToken } from '@angular/core'; + +export const RE_LOGIN_CONFIRMATION_TOKEN = new InjectionToken( + 'RE_LOGIN_CONFIRMATION_TOKEN', +); diff --git a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html index 9cf973c103..34bb64ec1f 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html +++ b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.html @@ -16,7 +16,6 @@ >
this.selectedNode?.id === node.key; @Input() beforeDrop = (event: NzFormatBeforeDropEvent) => { this.dropPosition = event.pos; return of(false); }; + ngOnChanges() { + this.checkedKeys = [...this.changedNodeValues]; + } + onSelectedNodeChange(node) { this.selectedNode = node.origin.entity; - this.selectedNodeChange.emit(node.origin.entity); + if (this.changeCheckboxWithNode) { + this.selectedNodeChange.emit(node); + this.checkedKeys = [...this.changedNodeValues]; + this.checkedKeysChange.emit(this.changedNodeValues); + } else { + this.selectedNodeChange.emit(node.origin.entity); + } } onCheckboxChange(event) { - this.checkedKeys = [...event.keys]; + this.checkedKeys = this.changedNodeValues = [...event.keys]; this.checkedKeysChange.emit(event.keys); } diff --git a/templates/app-nolayers/angular/src/app/home/home.component.spec.ts b/templates/app-nolayers/angular/src/app/home/home.component.spec.ts new file mode 100644 index 0000000000..9622abc9f6 --- /dev/null +++ b/templates/app-nolayers/angular/src/app/home/home.component.spec.ts @@ -0,0 +1,62 @@ +import { CoreTestingModule } from '@abp/ng.core/testing'; +import { ThemeBasicTestingModule } from '@abp/ng.theme.basic/testing'; +import { ThemeSharedTestingModule } from '@abp/ng.theme.shared/testing'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { NgxValidateCoreModule } from '@ngx-validate/core'; +import { HomeComponent } from './home.component'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { AuthService } from '@abp/ng.core'; + +describe('HomeComponent', () => { + let fixture: ComponentFixture; + const mockOAuthService = jasmine.createSpyObj('OAuthService', ['hasValidAccessToken']); + const mockAuthService = jasmine.createSpyObj('AuthService', ['navigateToLogin']); + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [HomeComponent], + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + { + provide: OAuthService, + useValue: mockOAuthService, + }, + { + provide: AuthService, + useValue: mockAuthService, + }, + ], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + fixture.detectChanges(); + }); + + it('should be initiated', () => { + expect(fixture.componentInstance).toBeTruthy(); + }); + + describe('when login state is true', () => { + beforeAll(() => { + mockOAuthService.hasValidAccessToken.and.returnValue(true); + }); + + it('hasLoggedIn should be true', () => { + expect(fixture.componentInstance.hasLoggedIn).toBeTrue(); + expect(mockOAuthService.hasValidAccessToken).toHaveBeenCalled(); + }); + + it('button should not be exists', () => { + const element = fixture.nativeElement; + const cardTitle = element.querySelector('.card-title'); + expect(cardTitle).toBeTruthy(); + }); + }); +}); diff --git a/templates/app-nolayers/angular/src/test.ts b/templates/app-nolayers/angular/src/test.ts index 61925f9803..85c74fe531 100644 --- a/templates/app-nolayers/angular/src/test.ts +++ b/templates/app-nolayers/angular/src/test.ts @@ -1,11 +1,10 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files - +import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; -import 'zone.js/testing'; declare const require: { context( diff --git a/templates/app/angular/src/app/home/home.component.spec.ts b/templates/app/angular/src/app/home/home.component.spec.ts new file mode 100644 index 0000000000..3ceff9410b --- /dev/null +++ b/templates/app/angular/src/app/home/home.component.spec.ts @@ -0,0 +1,100 @@ +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { HomeComponent } from "./home.component"; +import { OAuthService } from 'angular-oauth2-oidc'; +import { AuthService } from '@abp/ng.core'; + + +describe("HomeComponent", () => { + let fixture: ComponentFixture; + const mockOAuthService = jasmine.createSpyObj('OAuthService', ['hasValidAccessToken']) + const mockAuthService = jasmine.createSpyObj('AuthService', ['navigateToLogin']) + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [HomeComponent], + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + { + provide: OAuthService, + useValue: mockOAuthService + }, + { + provide: AuthService, + useValue: mockAuthService + } + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + fixture.detectChanges(); + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); + + + + describe('when login state is true', () => { + beforeAll(() => { + mockOAuthService.hasValidAccessToken.and.returnValue(true) + }); + + it("hasLoggedIn should be true", () => { + + expect(fixture.componentInstance.hasLoggedIn).toBeTrue(); + expect(mockOAuthService.hasValidAccessToken).toHaveBeenCalled() + }) + + it("button should not be exists", () => { + const element = fixture.nativeElement + const button = element.querySelector('[role="button"]') + expect(button).toBeNull() + }) + + }) + + describe('when login state is false', () => { + beforeAll(() => { + mockOAuthService.hasValidAccessToken.and.returnValue(false) + }); + + it("hasLoggedIn should be false", () => { + + expect(fixture.componentInstance.hasLoggedIn).toBeFalse(); + expect(mockOAuthService.hasValidAccessToken).toHaveBeenCalled() + }) + + it("button should be exists", () => { + const element = fixture.nativeElement + const button = element.querySelector('[role="button"]') + expect(button).toBeDefined() + }) + describe('when button clicked', () => { + + beforeEach(() => { + const element = fixture.nativeElement + const button = element.querySelector('[role="button"]') + button.click() + }); + + it("navigateToLogin have been called", () => { + expect(mockAuthService.navigateToLogin).toHaveBeenCalled() + }) + }) + }) + +}); \ No newline at end of file diff --git a/templates/app/angular/src/test.ts b/templates/app/angular/src/test.ts index 61925f9803..5b643ec44d 100644 --- a/templates/app/angular/src/test.ts +++ b/templates/app/angular/src/test.ts @@ -1,11 +1,11 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files - +import 'zone.js/testing'; import { getTestBed } from '@angular/core/testing'; import { BrowserDynamicTestingModule, platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; -import 'zone.js/testing'; + declare const require: { context( diff --git a/templates/module/angular/projects/my-project-name/src/lib/my-project-name.component.spec.ts b/templates/module/angular/projects/my-project-name/src/lib/my-project-name.component.spec.ts index 88f67b1e65..4d6aae3ac8 100644 --- a/templates/module/angular/projects/my-project-name/src/lib/my-project-name.component.spec.ts +++ b/templates/module/angular/projects/my-project-name/src/lib/my-project-name.component.spec.ts @@ -1,16 +1,24 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; - -import { MyProjectNameComponent } from './my-project-name.component'; +import { MyProjectNameComponent } from './components/my-project-name.component'; +import { MyProjectNameService } from '@my-company-name/my-project-name'; +import { of } from 'rxjs'; describe('MyProjectNameComponent', () => { let component: MyProjectNameComponent; let fixture: ComponentFixture; - + const mockMyProjectNameService = jasmine.createSpyObj('MyProjectNameService', { + sample: of([]), + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [ MyProjectNameComponent ] - }) - .compileComponents(); + declarations: [MyProjectNameComponent], + providers: [ + { + provide: MyProjectNameService, + useValue: mockMyProjectNameService, + }, + ], + }).compileComponents(); })); beforeEach(() => { diff --git a/templates/module/angular/projects/my-project-name/src/lib/my-project-name.service.spec.ts b/templates/module/angular/projects/my-project-name/src/lib/my-project-name.service.spec.ts index 7955ce073c..ec7aeb6c68 100644 --- a/templates/module/angular/projects/my-project-name/src/lib/my-project-name.service.spec.ts +++ b/templates/module/angular/projects/my-project-name/src/lib/my-project-name.service.spec.ts @@ -1,12 +1,19 @@ import { TestBed } from '@angular/core/testing'; - -import { MyProjectNameService } from './my-project-name.service'; +import { MyProjectNameService } from './services/my-project-name.service'; +import { RestService } from '@abp/ng.core'; describe('MyProjectNameService', () => { let service: MyProjectNameService; - + const mockRestService = jasmine.createSpyObj('RestService', ['request']); beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + providers: [ + { + provide: RestService, + useValue: mockRestService, + }, + ], + }); service = TestBed.inject(MyProjectNameService); });