diff --git a/docs/en/AspNetCore/Client-Side-Package-Management.md b/docs/en/AspNetCore/Client-Side-Package-Management.md index 0d98fe1dc5..f6dea58ff3 100644 --- a/docs/en/AspNetCore/Client-Side-Package-Management.md +++ b/docs/en/AspNetCore/Client-Side-Package-Management.md @@ -93,7 +93,8 @@ An example mapping configuration is shown below: ````js mappings: { "@node_modules/bootstrap/dist/css/bootstrap.css": "@libs/bootstrap/css/", - "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/" + "@node_modules/bootstrap/dist/js/bootstrap.bundle.js": "@libs/bootstrap/js/", + "@node_modules/bootstrap-datepicker/dist/locales/*.*": "@libs/bootstrap-datepicker/locales/" } ```` diff --git a/npm/ng-packs/package.json b/npm/ng-packs/package.json index 42e9c733f2..3774a5705d 100644 --- a/npm/ng-packs/package.json +++ b/npm/ng-packs/package.json @@ -92,6 +92,7 @@ "prettier --write", "tslint --fix", "git add" - ] + ], + "dist/*": [] } } diff --git a/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts index 932ff11b8e..86a564cbbe 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts @@ -9,7 +9,7 @@ import { OnChanges, TemplateRef, TrackByFunction, - ViewContainerRef + ViewContainerRef, } from '@angular/core'; import compare from 'just-compare'; import clone from 'just-clone'; @@ -25,7 +25,7 @@ class RecordView { } @Directive({ - selector: '[abpFor]' + selector: '[abpFor]', }) export class ForDirective implements OnChanges { @Input('abpForOf') @@ -67,7 +67,7 @@ export class ForDirective implements OnChanges { constructor( private tempRef: TemplateRef, private vcRef: ViewContainerRef, - private differs: IterableDiffers + private differs: IterableDiffers, ) {} private iterateOverAppliedOperations(changes: IterableChanges) { @@ -78,7 +78,7 @@ export class ForDirective implements OnChanges { const view = this.vcRef.createEmbeddedView( this.tempRef, new AbpForContext(null, -1, -1, this.items), - currentIndex + currentIndex, ); rw.push(new RecordView(record, view)); @@ -155,7 +155,7 @@ export class ForDirective implements OnChanges { const compareFn = this.compareFn; - if (typeof this.filterBy !== 'undefined') { + if (typeof this.filterBy !== 'undefined' && this.filterVal) { items = items.filter(item => compareFn(item[this.filterBy], this.filterVal)); } diff --git a/npm/ng-packs/packages/core/src/lib/directives/visibility.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/visibility.directive.ts index 2428b736a3..bbbc0417d5 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/visibility.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/visibility.directive.ts @@ -3,7 +3,7 @@ import { Subject } from 'rxjs'; import snq from 'snq'; @Directive({ - selector: '[abpVisibility]' + selector: '[abpVisibility]', }) export class VisibilityDirective implements AfterViewInit { @Input('abpVisibility') @@ -17,6 +17,10 @@ export class VisibilityDirective implements AfterViewInit { constructor(@Optional() private elRef: ElementRef, private renderer: Renderer2) {} ngAfterViewInit() { + if (!this.focusedElement && this.elRef) { + this.focusedElement = this.elRef.nativeElement; + } + let observer: MutationObserver; if (this.mutationObserverEnabled) { observer = new MutationObserver(mutations => { @@ -25,7 +29,7 @@ export class VisibilityDirective implements AfterViewInit { const htmlNodes = snq( () => Array.from(mutation.target.childNodes).filter(node => node instanceof HTMLElement), - [] + [], ); if (!htmlNodes.length) { @@ -40,13 +44,13 @@ export class VisibilityDirective implements AfterViewInit { }); observer.observe(this.focusedElement, { - childList: true + childList: true, }); } else { setTimeout(() => { const htmlNodes = snq( () => Array.from(this.focusedElement.childNodes).filter(node => node instanceof HTMLElement), - [] + [], ); if (!htmlNodes.length) this.removeFromDOM(); diff --git a/npm/ng-packs/packages/core/src/lib/states/session.state.ts b/npm/ng-packs/packages/core/src/lib/states/session.state.ts index 544bd42b2b..9f90e0ded9 100644 --- a/npm/ng-packs/packages/core/src/lib/states/session.state.ts +++ b/npm/ng-packs/packages/core/src/lib/states/session.state.ts @@ -35,7 +35,7 @@ export class SessionState { } @Action(SetTenant) - setTenantId({ patchState }: StateContext, { payload }: SetTenant) { + setTenant({ patchState }: StateContext, { payload }: SetTenant) { patchState({ tenant: payload, }); diff --git a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts index 2d4bd69c49..c01064152e 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/config.state.spec.ts @@ -109,7 +109,7 @@ export const CONFIG_STATE_DATA = { }, } as Config.State; -describe('ConfigService', () => { +describe('ConfigState', () => { let spectator: SpectatorService; let store: SpyObject; let service: ConfigService; diff --git a/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts new file mode 100644 index 0000000000..1405935b62 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/for.directive.spec.ts @@ -0,0 +1,191 @@ +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { ForDirective } from '../directives/for.directive'; +import { uuid } from '../utils'; + +describe('ForDirective', () => { + let spectator: SpectatorDirective; + let directive: ForDirective; + const items = [0, 1, 2, 3, 4, 5]; + const createDirective = createDirectiveFactory({ + directive: ForDirective, + }); + + describe('basic', () => { + beforeEach(() => { + spectator = createDirective('
  • {{ item }}
', { + hostProps: { items }, + }); + directive = spectator.directive; + }); + + test('should be created', () => { + expect(directive).toBeTruthy(); + }); + + test('should be iterated', () => { + const elements = spectator.queryAll('li'); + + expect(elements[3]).toHaveText('3'); + expect(elements).toHaveLength(6); + }); + + test('should sync the DOM when change items', () => { + (spectator.hostComponent as any).items = [10, 11, 12]; + spectator.detectChanges(); + const elements = spectator.queryAll('li'); + + expect(elements[1]).toHaveText('11'); + expect(elements).toHaveLength(3); + }); + + test('should sync the DOM when add an item', () => { + (spectator.hostComponent as any).items = [...items, 6]; + spectator.detectChanges(); + const elements = spectator.queryAll('li'); + + expect(elements[6]).toHaveText('6'); + expect(elements).toHaveLength(7); + }); + }); + + describe('trackBy', () => { + const trackByFn = (_, item) => item; + beforeEach(() => { + spectator = createDirective('
  • {{ item }}
', { + hostProps: { items, trackByFn }, + }); + directive = spectator.directive; + }); + + test('should be setted the trackBy', () => { + expect(directive.trackBy).toEqual(trackByFn); + }); + }); + + describe('with basic order', () => { + beforeEach(() => { + spectator = createDirective( + `
    +
  • + {{ item }} +
  • +
`, + ); + directive = spectator.directive; + }); + + test('should order by asc', () => { + const elements = spectator.queryAll('li'); + expect(elements.map(el => el.textContent.trim())).toEqual(['2', '3', '6']); + }); + }); + + describe('with order', () => { + beforeEach(() => { + spectator = createDirective( + `
    +
  • + {{ item.value }} +
  • +
`, + { + hostProps: { orderDir: 'ASC' }, + }, + ); + directive = spectator.directive; + }); + + test('should order by asc', () => { + const elements = spectator.queryAll('li'); + expect(elements.map(el => el.textContent.trim())).toEqual(['2', '3', '6']); + }); + + test('should order by desc', () => { + (spectator.hostComponent as any).orderDir = 'DESC'; + spectator.detectChanges(); + + const elements = spectator.queryAll('li'); + expect(elements.map(el => el.textContent.trim())).toEqual(['6', '3', '2']); + }); + }); + + describe('with filter', () => { + beforeEach(() => { + spectator = createDirective( + `
    +
  • + {{ item.value }} +
  • +
`, + { + hostProps: { filterVal: '' }, + }, + ); + directive = spectator.directive; + }); + + test('should not filter when filterVal is empty,', () => { + const elements = spectator.queryAll('li'); + expect(elements.map(el => el.textContent.trim())).toEqual(['test', 'abp', 'volo']); + }); + + test('should be filtered', () => { + (spectator.hostComponent as any).filterVal = 'volo'; + spectator.detectChanges(); + + expect(spectator.query('li')).toHaveText('volo'); + }); + + test('should not show an element when filter value not match to any text', () => { + (spectator.hostComponent as any).filterVal = 'volos'; + spectator.detectChanges(); + + const elements = spectator.queryAll('li'); + expect(elements).toHaveLength(0); + }); + }); + + describe('with empty ref', () => { + beforeEach(() => { + spectator = createDirective( + `
    +
  • + {{ item.value }} +
  • + + No records found +
`, + { + hostProps: { items: [] }, + }, + ); + directive = spectator.directive; + }); + + test('should display the empty ref', () => { + expect(spectator.query('ul')).toHaveText('No records found'); + expect(spectator.queryAll('li')).toHaveLength(0); + }); + + test('should not display the empty ref', () => { + expect(spectator.query('ul')).toHaveText('No records found'); + expect(spectator.queryAll('li')).toHaveLength(0); + + (spectator.hostComponent as any).items = [0]; + spectator.detectChanges(); + + expect(spectator.query('ul')).not.toHaveText('No records found'); + expect(spectator.queryAll('li')).toHaveLength(1); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/profile.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/profile.state.spec.ts new file mode 100644 index 0000000000..1ebdda217a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/profile.state.spec.ts @@ -0,0 +1,74 @@ +import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; +import { Session } from '../models/session'; +import { ProfileService } from '../services'; +import { ProfileState } from '../states'; +import { GetAppConfiguration } from '../actions/config.actions'; +import { of } from 'rxjs'; +import { Profile } from '../models/profile'; + +export class DummyClass {} + +export const PROFILE_STATE_DATA = { + profile: { userName: 'admin', email: 'info@abp.io', name: 'Admin' }, +} as Profile.State; + +describe('ProfileState', () => { + let spectator: SpectatorService; + let state: ProfileState; + let profileService: SpyObject; + let patchedData; + const patchState = jest.fn(data => (patchedData = data)); + + const createService = createServiceFactory({ + service: DummyClass, + mocks: [ProfileService], + }); + + beforeEach(() => { + spectator = createService(); + profileService = spectator.get(ProfileService); + state = new ProfileState(profileService); + }); + + describe('#getProfile', () => { + it('should return the current language', () => { + expect(ProfileState.getProfile(PROFILE_STATE_DATA)).toEqual(PROFILE_STATE_DATA.profile); + }); + }); + + describe('#GetProfile', () => { + it('should call the profile service get method and update the state', () => { + const mockData = { userName: 'test', email: 'test@abp.io' }; + const spy = jest.spyOn(profileService, 'get'); + spy.mockReturnValue(of(mockData as any)); + + state.profileGet({ patchState } as any).subscribe(); + + expect(patchedData).toEqual({ profile: mockData }); + }); + }); + + describe('#UpdateProfile', () => { + it('should call the profile service update method and update the state', () => { + const mockData = { userName: 'test2', email: 'test@abp.io' }; + const spy = jest.spyOn(profileService, 'update'); + spy.mockReturnValue(of(mockData as any)); + + state.profileUpdate({ patchState } as any, { payload: mockData as any }).subscribe(); + + expect(patchedData).toEqual({ profile: mockData }); + }); + }); + + describe('#ChangePassword', () => { + it('should call the profile service changePassword method', () => { + const mockData = { currentPassword: 'test123', newPassword: 'test123' }; + const spy = jest.spyOn(profileService, 'changePassword'); + spy.mockReturnValue(of(null)); + + state.changePassword(null, { payload: mockData }).subscribe(); + + expect(spy).toHaveBeenCalledWith(mockData, true); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts new file mode 100644 index 0000000000..412ed8d9ab --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts @@ -0,0 +1,70 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { Session } from '../models/session'; +import { LocalizationService } from '../services'; +import { SessionState } from '../states'; +import { GetAppConfiguration } from '../actions/config.actions'; +import { of } from 'rxjs'; + +export class DummyClass {} + +export const SESSION_STATE_DATA = { + language: 'tr', + tenant: { id: 'd5692aef-2ac6-49cd-9f3e-394c0bd4f8b3', name: 'Test' }, +} as Session.State; + +describe('SessionState', () => { + let spectator: SpectatorService; + let state: SessionState; + + const createService = createServiceFactory({ + service: DummyClass, + mocks: [LocalizationService], + }); + + beforeEach(() => { + spectator = createService(); + state = new SessionState(spectator.get(LocalizationService)); + }); + + describe('#getLanguage', () => { + it('should return the current language', () => { + expect(SessionState.getLanguage(SESSION_STATE_DATA)).toEqual(SESSION_STATE_DATA.language); + }); + }); + + describe('#getTenant', () => { + it('should return the tenant object', () => { + expect(SessionState.getTenant(SESSION_STATE_DATA)).toEqual(SESSION_STATE_DATA.tenant); + }); + }); + + describe('#SetLanguage', () => { + it('should set the language and dispatch the GetAppConfiguration action', () => { + let patchedData; + let dispatchedData; + const patchState = jest.fn(data => (patchedData = data)); + const dispatch = jest.fn(action => { + dispatchedData = action; + return of({}); + }); + const spy = jest.spyOn(spectator.get(LocalizationService), 'registerLocale'); + + state.setLanguage({ patchState, dispatch } as any, { payload: 'en' }).subscribe(); + + expect(patchedData).toEqual({ language: 'en' }); + expect(dispatchedData instanceof GetAppConfiguration).toBeTruthy(); + expect(spy).toHaveBeenCalledWith('en'); + }); + }); + + describe('#setTenantId', () => { + it('should set the tenant', () => { + let patchedData; + const patchState = jest.fn(data => (patchedData = data)); + const testTenant = { id: '54ae02ba-9289-4c1b-8521-0ea437756288', name: 'Test Tenant' }; + state.setTenant({ patchState } as any, { payload: testTenant }); + + expect(patchedData).toEqual({ tenant: testTenant }); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/visibility.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/visibility.directive.spec.ts new file mode 100644 index 0000000000..bbb669f70a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/visibility.directive.spec.ts @@ -0,0 +1,128 @@ +import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest'; +import { VisibilityDirective } from '../directives/visibility.directive'; + +describe('VisibilityDirective', () => { + let spectator: SpectatorDirective; + let directive: VisibilityDirective; + const createDirective = createDirectiveFactory({ + directive: VisibilityDirective, + }); + + describe('without mutation observer and without content', () => { + beforeEach(() => { + spectator = createDirective('
'); + directive = spectator.directive; + }); + + it('should be created', () => { + expect(directive).toBeTruthy(); + }); + + it('should be removed', done => { + setTimeout(() => { + expect(spectator.query('div')).toBeFalsy(); + done(); + }, 0); + }); + }); + + describe('without mutation observer and with content', () => { + beforeEach(() => { + spectator = createDirective( + '

Content

', + ); + directive = spectator.directive; + }); + + it('should not removed', done => { + setTimeout(() => { + expect(spectator.query('div')).toBeTruthy(); + done(); + }, 0); + }); + }); + + describe('without mutation observer and with focused element', () => { + beforeEach(() => { + spectator = createDirective( + '

Content

', + ); + directive = spectator.directive; + }); + + it('should not removed', done => { + setTimeout(() => { + expect(spectator.query('#main')).toBeTruthy(); + done(); + }, 0); + }); + }); + + describe('without content and with focused element', () => { + beforeEach(() => { + spectator = createDirective( + '
', + ); + directive = spectator.directive; + }); + + it('should be removed', done => { + setTimeout(() => { + expect(spectator.query('#main')).toBeFalsy(); + done(); + }, 0); + }); + }); + + describe('with mutation observer and with content', () => { + beforeEach(() => { + spectator = createDirective('
Content
'); + directive = spectator.directive; + }); + + it('should remove the main div element when content removed', done => { + spectator.query('#content').remove(); + + setTimeout(() => { + expect(spectator.query('div')).toBeFalsy(); + done(); + }, 0); + }); + + it('should not remove the main div element', done => { + spectator.query('div').appendChild(document.createElement('div')); + + setTimeout(() => { + expect(spectator.query('div')).toBeTruthy(); + done(); + }, 100); + }); + }); + + describe('with mutation observer and with focused element', () => { + beforeEach(() => { + spectator = createDirective( + '

Content

', + ); + directive = spectator.directive; + }); + + it('should remove the main div element when content removed', done => { + spectator.query('#content').remove(); + + setTimeout(() => { + expect(spectator.query('#main')).toBeFalsy(); + done(); + }, 0); + }); + + it('should not remove the main div element', done => { + spectator.query('#content').appendChild(document.createElement('div')); + + setTimeout(() => { + expect(spectator.query('#main')).toBeTruthy(); + done(); + }, 100); + }); + }); +}); diff --git a/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts b/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts index f98963722f..96cf0efdc5 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts +++ b/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts @@ -1,6 +1,6 @@ import { ABP } from '@abp/ng.core'; import { ConfirmationService, Toaster } from '@abp/ng.theme.shared'; -import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Component, TemplateRef, ViewChild, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; @@ -13,7 +13,7 @@ import { IdentityState } from '../../states/identity.state'; selector: 'abp-roles', templateUrl: './roles.component.html', }) -export class RolesComponent { +export class RolesComponent implements OnInit { @Select(IdentityState.getRoles) data$: Observable; @@ -45,6 +45,10 @@ export class RolesComponent { constructor(private confirmationService: ConfirmationService, private fb: FormBuilder, private store: Store) {} + ngOnInit() { + this.get(); + } + onSearch(value) { this.pageQuery.filter = value; this.get(); diff --git a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html index db1397870d..af1381e000 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html +++ b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.html @@ -112,7 +112,11 @@ -
+
+ + @@ -210,7 +214,7 @@ - {{ + {{ 'AbpIdentity::Save' | abpLocalization }} diff --git a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts index ffc69c58ed..230365cb68 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts +++ b/npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts @@ -1,6 +1,6 @@ import { ABP } from '@abp/ng.core'; import { ConfirmationService, Toaster } from '@abp/ng.theme.shared'; -import { Component, TemplateRef, TrackByFunction, ViewChild } from '@angular/core'; +import { Component, TemplateRef, TrackByFunction, ViewChild, OnInit } from '@angular/core'; import { AbstractControl, FormArray, FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { Select, Store } from '@ngxs/store'; import { Observable } from 'rxjs'; @@ -13,6 +13,7 @@ import { GetUserRoles, GetUsers, UpdateUser, + GetRoles, } from '../../actions/identity.actions'; import { Identity } from '../../models/identity'; import { IdentityState } from '../../states/identity.state'; @@ -20,7 +21,7 @@ import { IdentityState } from '../../states/identity.state'; selector: 'abp-users', templateUrl: './users.component.html', }) -export class UsersComponent { +export class UsersComponent implements OnInit { @Select(IdentityState.getUsers) data$: Observable; @@ -62,32 +63,39 @@ export class UsersComponent { constructor(private confirmationService: ConfirmationService, private fb: FormBuilder, private store: Store) {} + ngOnInit() { + this.get(); + } + onSearch(value) { this.pageQuery.filter = value; this.get(); } buildForm() { - this.roles = this.store.selectSnapshot(IdentityState.getRoles); - this.form = this.fb.group({ - userName: [this.selected.userName || '', [Validators.required, Validators.maxLength(256)]], - email: [this.selected.email || '', [Validators.required, Validators.email, Validators.maxLength(256)]], - name: [this.selected.name || '', [Validators.maxLength(64)]], - surname: [this.selected.surname || '', [Validators.maxLength(64)]], - phoneNumber: [this.selected.phoneNumber || '', [Validators.maxLength(16)]], - lockoutEnabled: [this.selected.twoFactorEnabled || (this.selected.id ? false : true)], - twoFactorEnabled: [this.selected.twoFactorEnabled || (this.selected.id ? false : true)], - roleNames: this.fb.array( - this.roles.map(role => - this.fb.group({ - [role.name]: [!!snq(() => this.selectedUserRoles.find(userRole => userRole.id === role.id))], - }), + this.store.dispatch(new GetRoles()).subscribe(() => { + this.roles = this.store.selectSnapshot(IdentityState.getRoles); + this.form = this.fb.group({ + userName: [this.selected.userName || '', [Validators.required, Validators.maxLength(256)]], + email: [this.selected.email || '', [Validators.required, Validators.email, Validators.maxLength(256)]], + name: [this.selected.name || '', [Validators.maxLength(64)]], + surname: [this.selected.surname || '', [Validators.maxLength(64)]], + phoneNumber: [this.selected.phoneNumber || '', [Validators.maxLength(16)]], + lockoutEnabled: [this.selected.twoFactorEnabled || (this.selected.id ? false : true)], + twoFactorEnabled: [this.selected.twoFactorEnabled || (this.selected.id ? false : true)], + roleNames: this.fb.array( + this.roles.map(role => + this.fb.group({ + [role.name]: [!!snq(() => this.selectedUserRoles.find(userRole => userRole.id === role.id))], + }), + ), ), - ), + }); + + if (!this.selected.userName) { + this.form.addControl('password', new FormControl('', [Validators.required, Validators.maxLength(32)])); + } }); - if (!this.selected.userName) { - this.form.addControl('password', new FormControl('', [Validators.required, Validators.maxLength(32)])); - } } openModal() { diff --git a/npm/ng-packs/packages/identity/src/lib/identity-routing.module.ts b/npm/ng-packs/packages/identity/src/lib/identity-routing.module.ts index ffe155a94f..96764a09ad 100644 --- a/npm/ng-packs/packages/identity/src/lib/identity-routing.module.ts +++ b/npm/ng-packs/packages/identity/src/lib/identity-routing.module.ts @@ -1,10 +1,8 @@ +import { AuthGuard, DynamicLayoutComponent, PermissionGuard } from '@abp/ng.core'; import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { RolesComponent } from './components/roles/roles.component'; -import { RoleResolver } from './resolvers/roles.resolver'; -import { DynamicLayoutComponent, AuthGuard, PermissionGuard } from '@abp/ng.core'; import { UsersComponent } from './components/users/users.component'; -import { UserResolver } from './resolvers/users.resolver'; const routes: Routes = [ { path: '', redirectTo: 'roles', pathMatch: 'full' }, @@ -16,14 +14,12 @@ const routes: Routes = [ { path: 'roles', component: RolesComponent, - resolve: [RoleResolver], data: { requiredPolicy: 'AbpIdentity.Roles' }, }, { path: 'users', component: UsersComponent, data: { requiredPolicy: 'AbpIdentity.Users' }, - resolve: [RoleResolver, UserResolver], }, ], }, @@ -32,6 +28,5 @@ const routes: Routes = [ @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], - providers: [RoleResolver, UserResolver], }) export class IdentityRoutingModule {} diff --git a/npm/ng-packs/packages/identity/src/lib/resolvers/roles.resolver.ts b/npm/ng-packs/packages/identity/src/lib/resolvers/roles.resolver.ts deleted file mode 100644 index 8e7a556989..0000000000 --- a/npm/ng-packs/packages/identity/src/lib/resolvers/roles.resolver.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; -import { Store } from '@ngxs/store'; -import { GetRoles } from '../actions/identity.actions'; -import { Identity } from '../models/identity'; -import { IdentityState } from '../states/identity.state'; - -@Injectable() -export class RoleResolver implements Resolve { - constructor(private store: Store) {} - - resolve() { - const roles = this.store.selectSnapshot(IdentityState.getRoles); - return roles && roles.length ? null : this.store.dispatch(new GetRoles()); - } -} diff --git a/npm/ng-packs/packages/identity/src/lib/resolvers/users.resolver.ts b/npm/ng-packs/packages/identity/src/lib/resolvers/users.resolver.ts deleted file mode 100644 index 66fe2d2c0c..0000000000 --- a/npm/ng-packs/packages/identity/src/lib/resolvers/users.resolver.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; -import { Store } from '@ngxs/store'; -import { GetUsers } from '../actions/identity.actions'; -import { Identity } from '../models/identity'; -import { IdentityState } from '../states/identity.state'; - -@Injectable() -export class UserResolver implements Resolve { - constructor(private store: Store) {} - - resolve() { - const users = this.store.selectSnapshot(IdentityState.getUsers); - return users && users.length ? null : this.store.dispatch(new GetUsers()); - } -} diff --git a/npm/ng-packs/packages/identity/src/lib/states/identity.state.ts b/npm/ng-packs/packages/identity/src/lib/states/identity.state.ts index 3576637094..2d99f11c98 100644 --- a/npm/ng-packs/packages/identity/src/lib/states/identity.state.ts +++ b/npm/ng-packs/packages/identity/src/lib/states/identity.state.ts @@ -23,22 +23,22 @@ import { IdentityService } from '../services/identity.service'; export class IdentityState { @Selector() static getRoles({ roles }: Identity.State): Identity.RoleItem[] { - return roles.items; + return roles.items || []; } @Selector() static getRolesTotalCount({ roles }: Identity.State): number { - return roles.totalCount; + return roles.totalCount || 0; } @Selector() static getUsers({ users }: Identity.State): Identity.UserItem[] { - return users.items; + return users.items || []; } @Selector() static getUsersTotalCount({ users }: Identity.State): number { - return users.totalCount; + return users.totalCount || 0; } constructor(private identityService: IdentityService) {} diff --git a/npm/ng-packs/packages/identity/src/public-api.ts b/npm/ng-packs/packages/identity/src/public-api.ts index be547b1c34..1366e9c333 100644 --- a/npm/ng-packs/packages/identity/src/public-api.ts +++ b/npm/ng-packs/packages/identity/src/public-api.ts @@ -7,6 +7,5 @@ export * from './lib/actions/identity.actions'; export * from './lib/components/roles/roles.component'; export * from './lib/constants/routes'; export * from './lib/models/identity'; -export * from './lib/resolvers/roles.resolver'; export * from './lib/services/identity.service'; export * from './lib/states/identity.state'; diff --git a/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts b/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts index 439a6dadc1..a87a82717f 100644 --- a/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts +++ b/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts @@ -1,15 +1,15 @@ import { ABP } from '@abp/ng.core'; import { ConfirmationService, Toaster } from '@abp/ng.theme.shared'; -import { Component, TemplateRef, ViewChild } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Select, Store } from '@ngxs/store'; -import { Observable, Subject } from 'rxjs'; -import { debounceTime, finalize, pluck, switchMap, take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { finalize, pluck, switchMap, take } from 'rxjs/operators'; import { CreateTenant, DeleteTenant, - GetTenants, GetTenantById, + GetTenants, UpdateTenant, } from '../../actions/tenant-management.actions'; import { TenantManagementService } from '../../services/tenant-management.service'; @@ -25,7 +25,7 @@ interface SelectedModalContent { selector: 'abp-tenants', templateUrl: './tenants.component.html', }) -export class TenantsComponent { +export class TenantsComponent implements OnInit { @Select(TenantManagementState.get) data$: Observable; @@ -81,6 +81,10 @@ export class TenantsComponent { private store: Store, ) {} + ngOnInit() { + this.get(); + } + onSearch(value) { this.pageQuery.filter = value; this.get(); diff --git a/npm/ng-packs/packages/tenant-management/src/lib/resolvers/index.ts b/npm/ng-packs/packages/tenant-management/src/lib/resolvers/index.ts deleted file mode 100644 index 3c5a42a2ac..0000000000 --- a/npm/ng-packs/packages/tenant-management/src/lib/resolvers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './tenants.resolver'; diff --git a/npm/ng-packs/packages/tenant-management/src/lib/resolvers/tenants.resolver.ts b/npm/ng-packs/packages/tenant-management/src/lib/resolvers/tenants.resolver.ts deleted file mode 100644 index 3476b977d1..0000000000 --- a/npm/ng-packs/packages/tenant-management/src/lib/resolvers/tenants.resolver.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; -import { Store } from '@ngxs/store'; -import { GetTenants } from '../actions/tenant-management.actions'; -import { TenantManagement } from '../models/tenant-management'; -import { TenantManagementState } from '../states/tenant-management.state'; - -@Injectable() -export class TenantsResolver implements Resolve { - constructor(private store: Store) {} - - resolve() { - const data = this.store.selectSnapshot(TenantManagementState.get); - return data && data.length ? null : this.store.dispatch(new GetTenants()); - } -} diff --git a/npm/ng-packs/packages/tenant-management/src/lib/tenant-management-routing.module.ts b/npm/ng-packs/packages/tenant-management/src/lib/tenant-management-routing.module.ts index f705d4d689..6723357c75 100644 --- a/npm/ng-packs/packages/tenant-management/src/lib/tenant-management-routing.module.ts +++ b/npm/ng-packs/packages/tenant-management/src/lib/tenant-management-routing.module.ts @@ -1,7 +1,6 @@ import { AuthGuard, DynamicLayoutComponent, PermissionGuard } from '@abp/ng.core'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { TenantsResolver } from './resolvers/tenants.resolver'; import { TenantsComponent } from './components/tenants/tenants.component'; const routes: Routes = [ @@ -11,13 +10,12 @@ const routes: Routes = [ component: DynamicLayoutComponent, canActivate: [AuthGuard, PermissionGuard], data: { requiredPolicy: 'AbpTenantManagement.Tenants' }, - children: [{ path: '', component: TenantsComponent, resolve: [TenantsResolver] }], + children: [{ path: '', component: TenantsComponent }], }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], - providers: [TenantsResolver], }) export class TenantManagementRoutingModule {} diff --git a/npm/ng-packs/packages/tenant-management/src/public-api.ts b/npm/ng-packs/packages/tenant-management/src/public-api.ts index 9bae9a9edb..67fcf7f195 100644 --- a/npm/ng-packs/packages/tenant-management/src/public-api.ts +++ b/npm/ng-packs/packages/tenant-management/src/public-api.ts @@ -3,6 +3,5 @@ export * from './lib/actions'; export * from './lib/components'; export * from './lib/constants'; export * from './lib/models'; -export * from './lib/resolvers'; export * from './lib/services'; export * from './lib/states'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts index 59c6b3c13d..99d58d3124 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts @@ -1,14 +1,24 @@ -import { Component, Input } from '@angular/core'; +import { Component, EventEmitter, Input, Output, ViewChild, ElementRef, Renderer2, OnInit } from '@angular/core'; +import { ABP } from '@abp/ng.core'; @Component({ selector: 'abp-button', + // tslint:disable-next-line: component-max-inline-declarations template: ` - `, }) -export class ButtonComponent { +export class ButtonComponent implements OnInit { @Input() buttonClass = 'btn btn-primary'; @@ -24,6 +34,21 @@ export class ButtonComponent { @Input() disabled = false; + @Input() + attributes: ABP.Dictionary; + + // tslint:disable-next-line: no-output-native + @Output() readonly click = new EventEmitter(); + + // tslint:disable-next-line: no-output-native + @Output() readonly focus = new EventEmitter(); + + // tslint:disable-next-line: no-output-native + @Output() readonly blur = new EventEmitter(); + + @ViewChild('button', { static: true }) + buttonRef: ElementRef; + /** * @deprecated Use buttonType instead. To be deleted in v1 */ @@ -32,4 +57,14 @@ export class ButtonComponent { get icon(): string { return `${this.loading ? 'fa fa-pulse fa-spinner' : this.iconClass || 'd-none'}`; } + + constructor(private renderer: Renderer2) {} + + ngOnInit() { + if (this.attributes) { + Object.keys(this.attributes).forEach(key => { + this.renderer.setAttribute(this.buttonRef.nativeElement, key, this.attributes[key]); + }); + } + } } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts index 59d49e68c4..c272b665e7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/button.component.spec.ts @@ -2,46 +2,55 @@ import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; import { ButtonComponent } from '../components'; describe('ButtonComponent', () => { - let host: SpectatorHost; + let spectator: SpectatorHost; const createHost = createHostFactory(ButtonComponent); - beforeEach(() => (host = createHost('Button'))); + beforeEach( + () => + (spectator = createHost('Button', { + hostProps: { attributes: { autofocus: '', name: 'abp-button' } }, + })), + ); it('should display the button', () => { - expect(host.query('button')).toBeTruthy(); + expect(spectator.query('button')).toBeTruthy(); }); it('should equal the default classes to btn btn-primary', () => { - expect(host.query('button')).toHaveClass('btn btn-primary'); + expect(spectator.query('button')).toHaveClass('btn btn-primary'); }); it('should equal the default type to button', () => { - expect(host.query('button')).toHaveAttribute('type', 'button'); + expect(spectator.query('button')).toHaveAttribute('type', 'button'); }); it('should enabled', () => { - expect(host.query('[disabled]')).toBeFalsy(); + expect(spectator.query('[disabled]')).toBeFalsy(); }); it('should have the text content', () => { - expect(host.query('button')).toHaveText('Button'); + expect(spectator.query('button')).toHaveText('Button'); }); it('should display the icon', () => { - expect(host.query('i.d-none')).toBeFalsy(); - expect(host.query('i')).toHaveClass('fa'); + expect(spectator.query('i.d-none')).toBeFalsy(); + expect(spectator.query('i')).toHaveClass('fa'); }); it('should display the spinner icon', () => { - host.component.loading = true; - host.detectComponentChanges(); - expect(host.query('i')).toHaveClass('fa-spinner'); + spectator.component.loading = true; + spectator.detectComponentChanges(); + expect(spectator.query('i')).toHaveClass('fa-spinner'); }); it('should disabled when the loading input is true', () => { - host.component.loading = true; - host.detectComponentChanges(); - expect(host.query('[disabled]')).toBeDefined(); + spectator.component.loading = true; + spectator.detectComponentChanges(); + expect(spectator.query('[disabled]')).toBeTruthy(); + }); + + it('should disabled when the loading input is true', () => { + expect(spectator.query('[autofocus][name="abp-button"]')).toBeTruthy(); }); }); diff --git a/npm/ng-packs/scripts/build.js b/npm/ng-packs/scripts/build.js index 92e145077e..a74863a2a0 100644 --- a/npm/ng-packs/scripts/build.js +++ b/npm/ng-packs/scripts/build.js @@ -37,7 +37,7 @@ import fse from 'fs-extra'; }); await execa('git', ['add', '../dist/*', '../package.json'], { stdout: 'inherit' }); - await execa('git', ['commit', '-m', 'Build ng packages'], { stdout: 'inherit' }); + await execa('git', ['commit', '--no-verify', '-m', 'Build ng packages'], { stdout: 'inherit' }); process.exit(0); })(); diff --git a/templates/app/angular/package.json b/templates/app/angular/package.json index b2b92257b9..f930a49741 100644 --- a/templates/app/angular/package.json +++ b/templates/app/angular/package.json @@ -18,17 +18,14 @@ "@abp/ng.setting-management": "^0.9.1", "@abp/ng.tenant-management": "^0.9.1", "@abp/ng.theme.basic": "^0.9.1", - "@angular/animations": "~8.2.10", - "@angular/common": "~8.2.10", - "@angular/compiler": "~8.2.10", - "@angular/core": "~8.2.10", - "@angular/forms": "~8.2.10", - "@angular/platform-browser": "~8.2.10", - "@angular/platform-browser-dynamic": "~8.2.10", - "@angular/router": "~8.2.10", - "@angularclass/hmr": "^2.1.3", - "@ngxs/devtools-plugin": "^3.5.0", - "@ngxs/hmr-plugin": "^3.5.0", + "@angular/animations": "~8.2.11", + "@angular/common": "~8.2.11", + "@angular/compiler": "~8.2.11", + "@angular/core": "~8.2.11", + "@angular/forms": "~8.2.11", + "@angular/platform-browser": "~8.2.11", + "@angular/platform-browser-dynamic": "~8.2.11", + "@angular/router": "~8.2.11", "rxjs": "~6.4.0", "tslib": "^1.10.0", "zone.js": "~0.9.1" @@ -36,19 +33,22 @@ "devDependencies": { "@angular-devkit/build-angular": "~0.803.9", "@angular/cli": "~8.3.9", - "@angular/compiler-cli": "~8.2.10", - "@angular/language-service": "~8.2.10", + "@angular/compiler-cli": "~8.2.11", + "@angular/language-service": "~8.2.11", + "@angularclass/hmr": "^2.1.3", + "@ngxs/hmr-plugin": "^3.5.0", + "@ngxs/logger-plugin": "^3.5.1", "@types/jasmine": "~3.3.8", "@types/jasminewd2": "~2.0.3", "@types/node": "~8.9.4", "codelyzer": "^5.0.0", "jasmine-core": "~3.4.0", "jasmine-spec-reporter": "~4.2.1", - "karma": "~4.1.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "~2.0.1", - "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^1.4.0", + "karma-jasmine": "~2.0.1", + "karma": "~4.1.0", "ngxs-schematic": "^1.1.9", "protractor": "~5.4.0", "ts-node": "~7.0.0", diff --git a/templates/app/angular/src/app/app.module.ts b/templates/app/angular/src/app/app.module.ts index 26f3b24e1c..0b2e9a93ec 100644 --- a/templates/app/angular/src/app/app.module.ts +++ b/templates/app/angular/src/app/app.module.ts @@ -3,7 +3,7 @@ import { LAYOUTS } from '@abp/ng.theme.basic'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin'; +import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin'; import { NgxsModule } from '@ngxs/store'; import { OAuthModule } from 'angular-oauth2-oidc'; import { environment } from '../environments/environment'; @@ -16,16 +16,18 @@ import { IdentityConfigModule } from '@abp/ng.identity.config'; import { TenantManagementConfigModule } from '@abp/ng.tenant-management.config'; import { SettingManagementConfigModule } from '@abp/ng.setting-management.config'; +const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })]; + @NgModule({ declarations: [AppComponent], imports: [ - ThemeSharedModule.forRoot(), CoreModule.forRoot({ environment, requirements: { layouts: LAYOUTS, }, }), + ThemeSharedModule.forRoot(), OAuthModule.forRoot(), NgxsModule.forRoot([]), AccountConfigModule.forRoot({ redirectUrl: '/' }), @@ -37,7 +39,7 @@ import { SettingManagementConfigModule } from '@abp/ng.setting-management.config AppRoutingModule, SharedModule, - NgxsReduxDevtoolsPluginModule.forRoot({ disabled: environment.production }), + ...(environment.production ? [] : LOGGERS), ], bootstrap: [AppComponent], })