Browse Source

Merge branch 'dev' into pr/2519

pull/2670/head
Galip Tolga Erdem 7 years ago
parent
commit
a74bbdd252
  1. 60
      docs/en/Entities.md
  2. 1
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html
  3. 3
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.html
  4. 4
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  5. 1
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html
  6. 78
      npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts
  7. 6
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html
  8. 47
      npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.scss
  9. 5
      npm/ng-packs/packages/theme-shared/src/lib/components/loading/loading.component.ts
  10. 72
      npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts
  11. 33
      npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts
  12. 4
      npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts

60
docs/en/Entities.md

@ -21,6 +21,66 @@ public class Book : Entity<Guid>
`Entity<TKey>` 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. `Entity<TKey>` 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<Guid>
{
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<Book> _bookRepository;
public BookAppService(IRepository<Book> 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 ### 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: Some entities may need to have **composite keys**. In that case, you can derive your entity from the non-generic `Entity` class. Example:

1
npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html

@ -23,6 +23,7 @@
<abp-table <abp-table
*ngIf="[150, 0] as columnWidths" *ngIf="[150, 0] as columnWidths"
[abpLoading]="loading" [abpLoading]="loading"
[abpLoadingDelay]="500"
[abpTableSort]="{ key: sortKey, order: sortOrder }" [abpTableSort]="{ key: sortKey, order: sortOrder }"
[colgroupTemplate]="tableColGroup" [colgroupTemplate]="tableColGroup"
[headerTemplate]="tableHeader" [headerTemplate]="tableHeader"

3
npm/ng-packs/packages/identity/src/lib/components/users/users.component.html

@ -31,6 +31,7 @@
<abp-table <abp-table
*ngIf="[150, 250, 250, 250] as columnWidths" *ngIf="[150, 250, 250, 250] as columnWidths"
[abpLoading]="loading" [abpLoading]="loading"
[abpLoadingDelay]="500"
[abpTableSort]="{ key: sortKey, order: sortOrder }" [abpTableSort]="{ key: sortKey, order: sortOrder }"
[colgroupTemplate]="tableColGroup" [colgroupTemplate]="tableColGroup"
[headerTemplate]="tableHeader" [headerTemplate]="tableHeader"
@ -226,8 +227,6 @@
> >
<input <input
type="checkbox" type="checkbox"
name="Roles[0].IsAssigned"
value="true"
class="custom-control-input" class="custom-control-input"
[attr.id]="'roles-' + i" [attr.id]="'roles-' + i"
[formControl]="roleGroup.controls[roles[i].name]" [formControl]="roleGroup.controls[roles[i].name]"

4
npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts

@ -134,7 +134,9 @@ export class UsersComponent implements OnInit {
this.roles.map(role => this.roles.map(role =>
this.fb.group({ this.fb.group({
[role.name]: [ [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,
], ],
}), }),
), ),

1
npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html

@ -31,6 +31,7 @@
<abp-table <abp-table
*ngIf="[150, 0] as columnWidths" *ngIf="[150, 0] as columnWidths"
[abpLoading]="loading" [abpLoading]="loading"
[abpLoadingDelay]="500"
[abpTableSort]="{ key: sortKey, order: sortOrder }" [abpTableSort]="{ key: sortKey, order: sortOrder }"
[colgroupTemplate]="tableColGroup" [colgroupTemplate]="tableColGroup"
[headerTemplate]="tableHeader" [headerTemplate]="tableHeader"

78
npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts

@ -6,12 +6,10 @@ export default `
.entry-row { .entry-row {
margin-bottom: 15px; margin-bottom: 15px;
} }
#main-navbar-tools a.dropdown-toggle { #main-navbar-tools a.dropdown-toggle {
text-decoration: none; text-decoration: none;
color: #fff; color: #fff;
} }
.navbar .dropdown-submenu { .navbar .dropdown-submenu {
position: relative; position: relative;
} }
@ -19,15 +17,15 @@ export default `
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.navbar .dropdown-menu a { .navbar .dropdown-menu a {
font-size: .9em; font-size: .9em;
padding: 10px 15px; padding: 10px 15px;
display: block; display: block;
min-width: 210px; min-width: 210px;
text-align: left; text-align: left;
border-radius: 0.25rem; border-radius: 0.25rem;
min-height: 44px; min-height: 44px;
} }
.navbar .dropdown-submenu a::after { .navbar .dropdown-submenu a::after {
transform: rotate(-90deg); transform: rotate(-90deg);
position: absolute; position: absolute;
@ -48,7 +46,6 @@ export default `
.container > .card { .container > .card {
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
} }
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.navbar .dropdown:hover > .dropdown-menu { .navbar .dropdown:hover > .dropdown-menu {
display: block; display: block;
@ -64,13 +61,66 @@ export default `
.field-validation-error { .field-validation-error {
font-size: 0.8em; font-size: 0.8em;
} }
.ui-table .ui-table-tbody > tr.empty-row > div.empty-row-content { .ui-table .ui-table-tbody > tr.empty-row > div.empty-row-content {
border: 1px solid #c8c8c8; border: 1px solid #c8c8c8;
} }
.abp-loading {
background: rgba(0, 0, 0, 0.1);
}
.modal-backdrop { .modal-backdrop {
background-color: rgba(0, 0, 0, 0.6); 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;
}
`; `;

6
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.html

@ -1,4 +1,4 @@
<div class="confirmation show" *ngIf="visible"> <div class="confirmation" *ngIf="visible">
<div class="confirmation-backdrop"></div> <div class="confirmation-backdrop"></div>
<div class="confirmation-dialog"> <div class="confirmation-dialog">
<div class="icon-container" [ngClass]="data.severity" *ngIf="data.severity"> <div class="icon-container" [ngClass]="data.severity" *ngIf="data.severity">
@ -15,7 +15,7 @@
<div class="footer"> <div class="footer">
<button <button
id="cancel" id="cancel"
class="confirmation-button confirmation-button-reject" class="confirmation-button confirmation-button--reject"
*ngIf="!data?.options?.hideCancelBtn" *ngIf="!data?.options?.hideCancelBtn"
(click)="close(reject)" (click)="close(reject)"
> >
@ -23,7 +23,7 @@
</button> </button>
<button <button
id="confirm" id="confirm"
class="confirmation-button confirmation-button-approve" class="confirmation-button confirmation-button--approve"
*ngIf="!data?.options?.hideYesBtn" *ngIf="!data?.options?.hideYesBtn"
(click)="close(confirm)" (click)="close(confirm)"
> >

47
npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.scss

@ -4,19 +4,16 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
display: none; display: flex;
place-items: center; align-items: center;
justify-content: center;
z-index: 1060; z-index: 1060;
&.show {
display: grid;
}
.confirmation-backdrop { .confirmation-backdrop {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background-color: rgba(#000, 0.7);
z-index: 1061 !important; z-index: 1061 !important;
} }
.confirmation-dialog { .confirmation-dialog {
@ -24,12 +21,8 @@
flex-direction: column; flex-direction: column;
margin: 20px auto; margin: 20px auto;
padding: 0; padding: 0;
border: none;
border-radius: 10px;
width: 450px; width: 450px;
min-height: 300px; min-height: 300px;
background-color: #fff;
box-shadow: 0 0 10px -5px rgba(#000, 0.5);
z-index: 1062 !important; z-index: 1062 !important;
.icon-container { .icon-container {
display: flex; display: flex;
@ -41,28 +34,9 @@
width: 100px; width: 100px;
height: 100px; height: 100px;
stroke-width: 1; stroke-width: 1;
fill: #fff;
font-size: 80px; font-size: 80px;
text-align: center; text-align: center;
} }
&.neutral .icon {
}
&.info .icon {
stroke: #2f96b4;
color: #2f96b4;
}
&.success .icon {
stroke: #51a351;
color: #51a351;
}
&.warning .icon {
stroke: #f89406;
color: #f89406;
}
&.error .icon {
stroke: #bd362f;
color: #bd362f;
}
} }
.content { .content {
flex-grow: 1; flex-grow: 1;
@ -79,7 +53,6 @@
display: block; display: block;
margin: 10px auto; margin: 10px auto;
padding: 20px; padding: 20px;
color: #777;
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
text-align: center; text-align: center;
@ -98,22 +71,8 @@
padding: 10px 20px; padding: 10px 20px;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
color: #777;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
background-color: #eee;
&:hover {
background-color: darken(#eee, 5);
}
&-reject {
}
&-approve {
background-color: #2f96b4;
color: #fff;
&:hover {
background-color: darken(#2f96b4, 5);
}
}
} }
} }
} }

5
npm/ng-packs/packages/theme-shared/src/lib/components/loading/loading.component.ts

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewEncapsulation } from '@angular/core';
@Component({ @Component({
selector: 'abp-loading', selector: 'abp-loading',
@ -7,10 +7,10 @@ import { Component, OnInit } from '@angular/core';
<i class="fa fa-spinner fa-pulse abp-spinner"></i> <i class="fa fa-spinner fa-pulse abp-spinner"></i>
</div> </div>
`, `,
encapsulation: ViewEncapsulation.None,
styles: [ styles: [
` `
.abp-loading { .abp-loading {
background: rgba(0, 0, 0, 0.2);
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -23,6 +23,7 @@ import { Component, OnInit } from '@angular/core';
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
font-size: 14px;
-moz-transform: translateX(-50%) translateY(-50%); -moz-transform: translateX(-50%) translateY(-50%);
-o-transform: translateX(-50%) translateY(-50%); -o-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%); -ms-transform: translateX(-50%) translateY(-50%);

72
npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts

@ -1,22 +1,23 @@
import { import {
Directive,
ElementRef,
AfterViewInit,
ViewContainerRef,
ComponentFactoryResolver, ComponentFactoryResolver,
Input,
Injector,
ComponentRef, ComponentRef,
ComponentFactory, Directive,
HostBinding, ElementRef,
EmbeddedViewRef, EmbeddedViewRef,
Renderer2, HostBinding,
Injector,
Input,
OnInit, OnInit,
OnDestroy,
Renderer2,
ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { Subscription, timer } from 'rxjs';
import { take } from 'rxjs/operators';
import { LoadingComponent } from '../components/loading/loading.component'; import { LoadingComponent } from '../components/loading/loading.component';
@Directive({ selector: '[abpLoading]' }) @Directive({ selector: '[abpLoading]' })
export class LoadingDirective implements OnInit { export class LoadingDirective implements OnInit, OnDestroy {
private _loading: boolean; private _loading: boolean;
@HostBinding('style.position') @HostBinding('style.position')
@ -29,29 +30,50 @@ export class LoadingDirective implements OnInit {
set loading(newValue: boolean) { set loading(newValue: boolean) {
setTimeout(() => { setTimeout(() => {
if (!this.componentRef) { if (!newValue && this.timerSubscription) {
this.componentRef = this.cdRes this.timerSubscription.unsubscribe();
.resolveComponentFactory(LoadingComponent) this.timerSubscription = null;
.create(this.injector); this._loading = newValue;
}
if (newValue && !this.rootNode) { if (this.rootNode) {
this.rootNode = (this.componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0]; this.renderer.removeChild(this.rootNode.parentElement, this.rootNode);
this.targetElement.appendChild(this.rootNode); this.rootNode = null;
} else { }
this.renderer.removeChild(this.rootNode.parentElement, this.rootNode); return;
this.rootNode = null;
} }
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<any>).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); }, 0);
} }
@Input('abpLoadingTargetElement') @Input('abpLoadingTargetElement')
targetElement: HTMLElement; targetElement: HTMLElement;
@Input('abpLoadingDelay')
delay = 0;
componentRef: ComponentRef<LoadingComponent>; componentRef: ComponentRef<LoadingComponent>;
rootNode: HTMLDivElement; rootNode: HTMLDivElement;
timerSubscription: Subscription;
constructor( constructor(
private elRef: ElementRef<HTMLElement>, private elRef: ElementRef<HTMLElement>,
@ -71,4 +93,10 @@ export class LoadingDirective implements OnInit {
} }
} }
} }
ngOnDestroy() {
if (this.timerSubscription) {
this.timerSubscription.unsubscribe();
}
}
} }

33
npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts

@ -20,8 +20,8 @@ describe('LoadingDirective', () => {
describe('default', () => { describe('default', () => {
beforeEach(() => { beforeEach(() => {
spectator = createDirective('<div [abpLoading]="status">Testing Loading Directive</div>', { spectator = createDirective('<div [abpLoading]="loading">Testing Loading Directive</div>', {
hostProps: { status: true }, hostProps: { loading: true },
}); });
}); });
@ -30,7 +30,7 @@ describe('LoadingDirective', () => {
expect(spectator.directive.rootNode).toBeTruthy(); expect(spectator.directive.rootNode).toBeTruthy();
expect(spectator.directive.componentRef).toBeTruthy(); expect(spectator.directive.componentRef).toBeTruthy();
done(); done();
}, 0); }, 20);
}); });
}); });
@ -40,9 +40,9 @@ describe('LoadingDirective', () => {
beforeEach(() => { beforeEach(() => {
spectator = createDirective( spectator = createDirective(
'<div [abpLoading]="status" [abpLoadingTargetElement]="target">Testing Loading Directive</div>', '<div [abpLoading]="loading" [abpLoadingDelay]="delay" [abpLoadingTargetElement]="target">Testing Loading Directive</div>',
{ {
hostProps: { status: true, target: mockTarget }, hostProps: { loading: true, target: mockTarget, delay: 0 },
}, },
); );
}); });
@ -51,24 +51,35 @@ describe('LoadingDirective', () => {
setTimeout(() => { setTimeout(() => {
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
done(); done();
}, 0); }, 20);
}); });
it('should remove the loading component to the DOM', done => { it('should remove the loading component to the DOM', done => {
const rendererSpy = jest.spyOn(spectator.directive['renderer'], 'removeChild'); const rendererSpy = jest.spyOn(spectator.directive['renderer'], 'removeChild');
spectator.setHostInput({ status: false }); setTimeout(() => spectator.setHostInput({ loading: false }), 0);
setTimeout(() => { setTimeout(() => {
expect(rendererSpy).toHaveBeenCalled(); expect(rendererSpy).toHaveBeenCalled();
expect(spectator.directive.rootNode).toBeFalsy(); expect(spectator.directive.rootNode).toBeFalsy();
done(); 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', () => { describe('with a component selector', () => {
beforeEach(() => { beforeEach(() => {
spectator = createDirective('<abp-dummy [abpLoading]="status"></abp-dummy>', { spectator = createDirective('<abp-dummy [abpLoading]="loading"></abp-dummy>', {
hostProps: { status: true }, hostProps: { loading: true },
}); });
}); });
@ -76,7 +87,7 @@ describe('LoadingDirective', () => {
setTimeout(() => { setTimeout(() => {
expect(spectator.directive.targetElement.id).toBe('dummy'); expect(spectator.directive.targetElement.id).toBe('dummy');
done(); done();
}, 0); }, 20);
}); });
}); });
}); });

4
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('div.toast')).toBeTruthy();
expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle'); expect(spectator.query('.toast-icon i')).toHaveClass('fa-times-circle');
expect(spectator.query('div.toast-title')).toHaveText('title'); 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', () => { test('should display a warning toast', () => {
@ -71,7 +71,7 @@ describe('ToasterService', () => {
'summary1', 'summary1',
'summary2', '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', 'detail1',
'detail2', 'detail2',
]); ]);

Loading…
Cancel
Save