diff --git a/docs/en/Entities.md b/docs/en/Entities.md index 8e0a039dfd..b5d196cccb 100644 --- a/docs/en/Entities.md +++ b/docs/en/Entities.md @@ -21,6 +21,66 @@ public class Book : Entity `Entity` class just defines an `Id` property with the given primary **key type**, which is `Guid` in the example above. It can be other types like `string`, `int`, `long` or whatever you need. +### Entities with GUID Keys + +If your entity's Id type is `Guid`, there are some good practices to implement: + +* Create a constructor that gets the Id as a parameter and passes to the base class. + * If you don't set a GUID Id, ABP Framework sets it on save, but it is good to have a valid Id on the entity even before saving it to the database. +* If you create an entity with a constructor that takes parameters, also create a `protected` empty constructor. This is used while your database provider reads your entity from the database (on deserialization). +* Don't use the `Guid.NewGuid()` to set the Id! Use [the `IGuidGenerator` service](Guid-Generation.md) while passing the Id from the code that creates the entity. `IGuidGenerator` optimized to generate sequential GUIDs, which is critical for clustered indexes in the relational databases. + +An example entity: + +````csharp +public class Book : Entity +{ + public string Name { get; set; } + + public float Price { get; set; } + + protected Book() + { + + } + + public Book(Guid id) + : base(id) + { + + } +} +```` + +Example usage in an [application service](Application-Services.md): + +````csharp +public class BookAppService : ApplicationService, IBookAppService +{ + private readonly IRepository _bookRepository; + + public BookAppService(IRepository bookRepository) + { + _bookRepository = bookRepository; + } + + public async Task CreateAsync(CreateBookDto input) + { + await _bookRepository.InsertAsync( + new Book(GuidGenerator.Create()) + { + Name = input.Name, + Price = input.Price + } + ); + } +} +```` + +* `BookAppService` injects the default [repository](Repositories.md) for the book entity and uses its `InsertAsync` method to insert a `Book` to the database. +* `GuidGenerator` is type of `IGuidGenerator` which is a property defined in the `ApplicationService` base class. ABP defines such frequently used base properties as pre-injected for you, so you don't need to manually [inject](Dependency-Injection.md) them. +* If you want to follow the DDD best practices, see the *Aggregate Example* section below. + ### Entities with Composite Keys Some entities may need to have **composite keys**. In that case, you can derive your entity from the non-generic `Entity` class. Example: diff --git a/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html b/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html index 57858686fa..99a7554c05 100644 --- a/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html +++ b/npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html @@ -23,6 +23,7 @@ this.fb.group({ [role.name]: [ - !!snq(() => this.selectedUserRoles.find(userRole => userRole.id === role.id)), + this.selectedUserRoles.length + ? !!snq(() => this.selectedUserRoles.find(userRole => userRole.id === role.id)) + : role.isDefault, ], }), ), diff --git a/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html b/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html index d142489474..6a4690e797 100644 --- a/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html +++ b/npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html @@ -31,6 +31,7 @@ .card { box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; } - @media screen and (min-width: 768px) { .navbar .dropdown:hover > .dropdown-menu { display: block; @@ -64,13 +61,66 @@ export default ` .field-validation-error { font-size: 0.8em; } - .ui-table .ui-table-tbody > tr.empty-row > div.empty-row-content { border: 1px solid #c8c8c8; } - +.abp-loading { + background: rgba(0, 0, 0, 0.1); +} .modal-backdrop { background-color: rgba(0, 0, 0, 0.6); } +.confirmation .confirmation-backdrop { + background: rgba(0, 0, 0, 0.7) !important; +} + .confirmation .confirmation-dialog { + border: none; + border-radius: 10px; + background-color: #fff; + box-shadow: 0 0 10px -5px rgba(0, 0, 0, 0.5); +} + .confirmation .confirmation-dialog .icon-container .icon { + stroke: #fff; + color: #fff; +} + .confirmation .confirmation-dialog .icon-container.info .icon { + stroke: #2f96b4; + color: #2f96b4; +} + .confirmation .confirmation-dialog .icon-container.success .icon { + stroke: #51a351; + color: #51a351; +} + .confirmation .confirmation-dialog .icon-container.warning .icon { + stroke: #f89406; + color: #f89406; +} + .confirmation .confirmation-dialog .icon-container.error .icon { + stroke: #bd362f; + color: #bd362f; +} + .confirmation .confirmation-dialog .content .title { + color: #222; +} + .confirmation .confirmation-dialog .content .message { + color: #777; +} + .confirmation .confirmation-dialog .footer { + background: transparent; +} + .confirmation .confirmation-dialog .footer .confirmation-button { + background-color: #eee; + color: #777; +} + .confirmation .confirmation-dialog .footer .confirmation-button:hover, .confirmation .confirmation-dialog .footer .confirmation-button:focus, .confirmation .confirmation-dialog .footer .confirmation-button:active { + background-color: #bbb; +} + .confirmation .confirmation-dialog .footer .confirmation-button--confirm { + background-color: #2f96b4; + color: #fff; +} + .confirmation .confirmation-dialog .footer .confirmation-button--confirm:hover { + background-color: #2e819b; +} `; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html index 07faa3a8a7..f2b1417f14 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html @@ -1,4 +1,4 @@ -
+
@@ -15,7 +15,7 @@ `, + encapsulation: ViewEncapsulation.None, styles: [ ` .abp-loading { - background: rgba(0, 0, 0, 0.2); position: absolute; width: 100%; height: 100%; @@ -23,6 +23,7 @@ import { Component, OnInit } from '@angular/core'; position: absolute; top: 50%; left: 50%; + font-size: 14px; -moz-transform: translateX(-50%) translateY(-50%); -o-transform: translateX(-50%) translateY(-50%); -ms-transform: translateX(-50%) translateY(-50%); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts index d38b3713b7..40b76cbfc8 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts @@ -1,22 +1,23 @@ import { - Directive, - ElementRef, - AfterViewInit, - ViewContainerRef, ComponentFactoryResolver, - Input, - Injector, ComponentRef, - ComponentFactory, - HostBinding, + Directive, + ElementRef, EmbeddedViewRef, - Renderer2, + HostBinding, + Injector, + Input, OnInit, + OnDestroy, + Renderer2, + ViewContainerRef, } from '@angular/core'; +import { Subscription, timer } from 'rxjs'; +import { take } from 'rxjs/operators'; import { LoadingComponent } from '../components/loading/loading.component'; @Directive({ selector: '[abpLoading]' }) -export class LoadingDirective implements OnInit { +export class LoadingDirective implements OnInit, OnDestroy { private _loading: boolean; @HostBinding('style.position') @@ -29,29 +30,50 @@ export class LoadingDirective implements OnInit { set loading(newValue: boolean) { setTimeout(() => { - if (!this.componentRef) { - this.componentRef = this.cdRes - .resolveComponentFactory(LoadingComponent) - .create(this.injector); - } + if (!newValue && this.timerSubscription) { + this.timerSubscription.unsubscribe(); + this.timerSubscription = null; + this._loading = newValue; - if (newValue && !this.rootNode) { - this.rootNode = (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0]; - this.targetElement.appendChild(this.rootNode); - } else { - this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); - this.rootNode = null; + if (this.rootNode) { + this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); + this.rootNode = null; + } + return; } - this._loading = newValue; + this.timerSubscription = timer(this.delay) + .pipe(take(1)) + .subscribe(() => { + if (!this.componentRef) { + this.componentRef = this.cdRes + .resolveComponentFactory(LoadingComponent) + .create(this.injector); + } + + if (newValue && !this.rootNode) { + this.rootNode = (this.componentRef.hostView as EmbeddedViewRef).rootNodes[0]; + this.targetElement.appendChild(this.rootNode); + } else { + this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); + this.rootNode = null; + } + + this._loading = newValue; + this.timerSubscription = null; + }); }, 0); } @Input('abpLoadingTargetElement') targetElement: HTMLElement; + @Input('abpLoadingDelay') + delay = 0; + componentRef: ComponentRef; rootNode: HTMLDivElement; + timerSubscription: Subscription; constructor( private elRef: ElementRef, @@ -71,4 +93,10 @@ export class LoadingDirective implements OnInit { } } } + + ngOnDestroy() { + if (this.timerSubscription) { + this.timerSubscription.unsubscribe(); + } + } } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts index 1b49b8581c..b359a2000c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts @@ -20,8 +20,8 @@ describe('LoadingDirective', () => { describe('default', () => { beforeEach(() => { - spectator = createDirective('
Testing Loading Directive
', { - hostProps: { status: true }, + spectator = createDirective('
Testing Loading Directive
', { + hostProps: { loading: true }, }); }); @@ -30,7 +30,7 @@ describe('LoadingDirective', () => { expect(spectator.directive.rootNode).toBeTruthy(); expect(spectator.directive.componentRef).toBeTruthy(); done(); - }, 0); + }, 20); }); }); @@ -40,9 +40,9 @@ describe('LoadingDirective', () => { beforeEach(() => { spectator = createDirective( - '
Testing Loading Directive
', + '
Testing Loading Directive
', { - hostProps: { status: true, target: mockTarget }, + hostProps: { loading: true, target: mockTarget, delay: 0 }, }, ); }); @@ -51,24 +51,35 @@ describe('LoadingDirective', () => { setTimeout(() => { expect(spy).toHaveBeenCalled(); done(); - }, 0); + }, 20); }); it('should remove the loading component to the DOM', done => { const rendererSpy = jest.spyOn(spectator.directive['renderer'], 'removeChild'); - spectator.setHostInput({ status: false }); + setTimeout(() => spectator.setHostInput({ loading: false }), 0); setTimeout(() => { expect(rendererSpy).toHaveBeenCalled(); expect(spectator.directive.rootNode).toBeFalsy(); done(); - }, 0); + }, 20); + }); + + it('should appear with delay', done => { + spectator.setHostInput({ loading: false, delay: 20 }); + spectator.detectChanges(); + setTimeout(() => spectator.setHostInput({ loading: true }), 0); + setTimeout(() => expect(spectator.directive.loading).toBe(false), 15); + setTimeout(() => { + expect(spectator.directive.loading).toBe(true); + done(); + }, 50); }); }); describe('with a component selector', () => { beforeEach(() => { - spectator = createDirective('', { - hostProps: { status: true }, + spectator = createDirective('', { + hostProps: { loading: true }, }); }); @@ -76,7 +87,7 @@ describe('LoadingDirective', () => { setTimeout(() => { expect(spectator.directive.targetElement.id).toBe('dummy'); done(); - }, 0); + }, 20); }); }); }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts index d7fbd7219e..23e99b8204 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts @@ -41,7 +41,7 @@ describe('ToasterService', () => { expect(spectator.query('div.toast')).toBeTruthy(); expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle'); expect(spectator.query('div.toast-title')).toHaveText('title'); - expect(spectator.query('div.toast-message')).toHaveText('test'); + expect(spectator.query('p.toast-message')).toHaveText('test'); }); test('should display a warning toast', () => { @@ -71,7 +71,7 @@ describe('ToasterService', () => { 'summary1', 'summary2', ]); - expect(spectator.queryAll('div.toast-message').map(node => node.textContent.trim())).toEqual([ + expect(spectator.queryAll('p.toast-message').map(node => node.textContent.trim())).toEqual([ 'detail1', 'detail2', ]);