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.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/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(" { 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); });