Browse Source

Merge branch 'dev' of https://github.com/abpframework/abp into dev

pull/2612/head
Halil İbrahim Kalkan 6 years ago
parent
commit
7172e2f108
  1. 3
      docs/en/Virtual-File-System.md
  2. 3
      docs/zh-Hans/Virtual-File-System.md
  3. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs
  4. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpDictionaryValueComparer.cs
  5. 4
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json
  6. 8
      modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs
  7. 4
      npm/ng-packs/apps/dev-app/tsconfig.app.json
  8. 1
      npm/ng-packs/package.json
  9. 49
      npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts
  10. 35
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html
  11. 7
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts
  12. 39
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.html
  13. 9
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  14. 34
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html
  15. 9
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts
  16. 1
      npm/ng-packs/packages/theme-shared/ng-package.json
  17. 1
      npm/ng-packs/packages/theme-shared/package.json
  18. 5
      npm/ng-packs/packages/theme-shared/src/lib/components/index.ts
  19. 39
      npm/ng-packs/packages/theme-shared/src/lib/components/loading/loading.component.ts
  20. 38
      npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.html
  21. 52
      npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.ts
  22. 78
      npm/ng-packs/packages/theme-shared/src/lib/components/table/table.component.html
  23. 106
      npm/ng-packs/packages/theme-shared/src/lib/components/table/table.component.ts
  24. 1
      npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts
  25. 1
      npm/ng-packs/packages/theme-shared/src/lib/directives/index.ts
  26. 74
      npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts
  27. 39
      npm/ng-packs/packages/theme-shared/src/lib/directives/table-sort.directive.ts
  28. 82
      npm/ng-packs/packages/theme-shared/src/lib/tests/loading.directive.spec.ts
  29. 63
      npm/ng-packs/packages/theme-shared/src/lib/tests/pagination.component.spec.ts
  30. 74
      npm/ng-packs/packages/theme-shared/src/lib/tests/table.component.spec.ts
  31. 23
      npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

3
docs/en/Virtual-File-System.md

@ -38,7 +38,8 @@ If you want to add multiple files, this can be tedious. Alternatively, you can d
````C#
<ItemGroup>
<None Remove="MyResources\**\*.*" />
<EmbeddedResource Include="MyResources\**\*.*" />
<Content Remove="MyResources\**\*.*" />
</ItemGroup>
````

3
docs/zh-Hans/Virtual-File-System.md

@ -39,7 +39,8 @@ namespace MyCompany.MyProject
````C#
<ItemGroup>
<None Remove="MyResources\**\*.*" />
<EmbeddedResource Include="MyResources\**\*.*" />
<Content Remove="MyResources\**\*.*" />
</ItemGroup>
````

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs

@ -59,7 +59,7 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling
b.Property<Dictionary<string, object>>(nameof(IHasExtraProperties.ExtraProperties))
.HasColumnName(nameof(IHasExtraProperties.ExtraProperties))
.HasConversion(new AbpJsonValueConverter<Dictionary<string, object>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer());
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, object>());
}
}

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/ValueComparers/AbpDictionaryValueComparer.cs

@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace Volo.Abp.EntityFrameworkCore.ValueComparers
{
public class AbpDictionaryValueComparer : ValueComparer<Dictionary<string, object>>
public class AbpDictionaryValueComparer<TKey, TValue> : ValueComparer<Dictionary<TKey, TValue>>
{
public AbpDictionaryValueComparer()
: base(

4
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json

@ -7,7 +7,7 @@
"UserName": "User name",
"EmailAddress": "Email address",
"PhoneNumber": "Phone number",
"UserInformations": "User informations",
"UserInformations": "User information",
"DisplayName:IsDefault": "Default",
"DisplayName:IsStatic": "Static",
"DisplayName:IsPublic": "Public",
@ -99,4 +99,4 @@
"Description:Abp.Identity.User.IsUserNameUpdateEnabled": "Whether the username can be updated by the user.",
"Description:Abp.Identity.User.IsEmailUpdateEnabled": "Whether the email can be updated by the user."
}
}
}

8
modules/identityserver/src/Volo.Abp.IdentityServer.EntityFrameworkCore/Volo/Abp/IdentityServer/EntityFrameworkCore/IdentityServerDbContextModelCreatingExtensions.cs

@ -204,8 +204,8 @@ namespace Volo.Abp.IdentityServer.EntityFrameworkCore
identityResource.Property(x => x.DisplayName).HasMaxLength(IdentityResourceConsts.DisplayNameMaxLength);
identityResource.Property(x => x.Description).HasMaxLength(IdentityResourceConsts.DescriptionMaxLength);
identityResource.Property(x => x.Properties)
.HasConversion(new AbpJsonValueConverter<Dictionary<string, object>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer());
.HasConversion(new AbpJsonValueConverter<Dictionary<string, string>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, string>());
identityResource.HasMany(x => x.UserClaims).WithOne().HasForeignKey(x => x.IdentityResourceId).IsRequired();
});
@ -229,8 +229,8 @@ namespace Volo.Abp.IdentityServer.EntityFrameworkCore
apiResource.Property(x => x.DisplayName).HasMaxLength(ApiResourceConsts.DisplayNameMaxLength);
apiResource.Property(x => x.Description).HasMaxLength(ApiResourceConsts.DescriptionMaxLength);
apiResource.Property(x => x.Properties)
.HasConversion(new AbpJsonValueConverter<Dictionary<string, object>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer());
.HasConversion(new AbpJsonValueConverter<Dictionary<string, string>>())
.Metadata.SetValueComparer(new AbpDictionaryValueComparer<string, string>());
apiResource.HasMany(x => x.Secrets).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();
apiResource.HasMany(x => x.Scopes).WithOne().HasForeignKey(x => x.ApiResourceId).IsRequired();

4
npm/ng-packs/apps/dev-app/tsconfig.app.json

@ -6,8 +6,8 @@
"paths": {
"@abp/ng.core": ["packages/core/src/public-api.ts"],
"@abp/ng.core/*": ["packages/core/src/lib/*"],
// "@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"],
// "@abp/ng.theme.shared/*": ["packages/theme-shared/src/lib/*"],
"@abp/ng.theme.shared": ["packages/theme-shared/src/public-api.ts"],
"@abp/ng.theme.shared/*": ["packages/theme-shared/src/lib/*"],
"@abp/ng.theme.basic": ["packages/theme-basic/src/public-api.ts"],
"@abp/ng.theme.basic/*": ["packages/theme-basic/src/lib/*"],
"@abp/ng.account": ["packages/account/src/public-api.ts"],

1
npm/ng-packs/package.json

@ -74,7 +74,6 @@
"just-compare": "^1.3.0",
"lerna": "^3.19.0",
"ng-packagr": "^5.7.1",
"ngx-perfect-scrollbar": "^8.0.0",
"ngxs-reset-plugin": "^1.2.0",
"ngxs-schematic": "^1.1.9",
"prettier": "^1.18.2",

49
npm/ng-packs/packages/core/src/lib/abstracts/ng-model.component.ts

@ -1,27 +1,48 @@
import { ControlValueAccessor } from '@angular/forms';
import { ChangeDetectorRef, Component, Injector, Input, Type } from '@angular/core';
import { ChangeDetectorRef, Component, Injector, Input } from '@angular/core';
@Component({ selector: 'abp-abstract-ng-model', template: '' })
export class AbstractNgModelComponent<T = any> implements ControlValueAccessor {
@Input() disabled: boolean;
// Not an abstract class on purpose. Do not change!
// tslint:disable-next-line: use-component-selector
@Component({ template: '' })
export class AbstractNgModelComponent<T = any, U = T> implements ControlValueAccessor {
protected _value: T;
protected cdRef: ChangeDetectorRef;
onChange: (value: T) => {};
onTouched: () => {};
@Input()
disabled: boolean;
@Input()
readonly: boolean;
@Input()
valueFn: (value: U, previousValue?: T) => T = value => (value as any) as T;
@Input()
valueLimitFn: (value: T, previousValue?: T) => any = value => false;
@Input()
set value(value: T) {
value = this.valueFn((value as any) as U, this._value);
if (this.valueLimitFn(value, this._value) !== false || this.readonly) return;
@Input() set value(value: T) {
this._value = value;
this.notifyValueChange();
}
get value(): T {
return this._value;
return this._value || this.defaultValue;
}
onChange: (value: T) => {};
onTouched: () => {};
protected _value: T;
protected cdRef: ChangeDetectorRef;
get defaultValue(): T {
return this._value;
}
constructor(public injector: Injector) {
this.cdRef = injector.get<ChangeDetectorRef>(ChangeDetectorRef as Type<ChangeDetectorRef>);
// tslint:disable-next-line: deprecation
this.cdRef = injector.get(ChangeDetectorRef);
}
notifyValueChange(): void {
@ -31,8 +52,8 @@ export class AbstractNgModelComponent<T = any> implements ControlValueAccessor {
}
writeValue(value: T): void {
this._value = value;
setTimeout(() => this.cdRef.detectChanges(), 0);
this._value = this.valueLimitFn(value, this._value) || value;
setTimeout(() => this.cdRef.markForCheck(), 0);
}
registerOnChange(fn: any): void {

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

@ -20,37 +20,28 @@
</div>
<div class="card-body">
<p-table
<abp-table
*ngIf="[150, 0] as columnWidths"
[value]="data$ | async"
[abpLoading]="loading"
[abpTableSort]="{ key: sortKey, order: sortOrder }"
[lazy]="true"
[lazyLoadOnInit]="false"
[paginator]="true"
[rows]="10"
[colgroupTemplate]="tableColGroup"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[value]="data$ | async"
[rows]="pageQuery.maxResultCount"
[totalRecords]="totalCount$ | async"
[loading]="loading"
[resizableColumns]="true"
[scrollable]="true"
(onLazyLoad)="onPageChange($event)"
(pageChange)="onPageChange($event)"
>
<ng-template pTemplate="colgroup">
<ng-template #tableColGroup>
<colgroup>
<col *ngFor="let width of columnWidths" [ngStyle]="{ 'width.px': width || undefined }" />
</colgroup>
</ng-template>
<ng-template pTemplate="emptymessage" let-columns>
<tr
abp-table-empty-message
[attr.colspan]="columnWidths.length"
localizationResource="AbpIdentity"
localizationProp="NoDataAvailableInDatatable"
></tr>
</ng-template>
<ng-template pTemplate="header" let-columns>
<ng-template #tableHeader>
<tr>
<th>{{ 'AbpIdentity::Actions' | abpLocalization }}</th>
<th pResizableColumn (click)="sortOrderIcon.sort('name')">
<th (click)="sortOrderIcon.sort('name')">
{{ 'AbpIdentity::RoleName' | abpLocalization }}
<abp-sort-order-icon
#sortOrderIcon
@ -61,7 +52,7 @@
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-data>
<ng-template #tableBody let-data>
<tr>
<td class="text-center">
<div ngbDropdown container="body" class="d-inline-block">
@ -109,7 +100,7 @@
</td>
</tr>
</ng-template>
</p-table>
</abp-table>
</div>
</div>

7
npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.ts

@ -36,7 +36,7 @@ export class RolesComponent implements OnInit {
providerKey: string;
pageQuery: ABP.PageQueryParams = {};
pageQuery: ABP.PageQueryParams = { maxResultCount: 10 };
loading = false;
@ -123,9 +123,8 @@ export class RolesComponent implements OnInit {
});
}
onPageChange(data) {
this.pageQuery.skipCount = data.first;
this.pageQuery.maxResultCount = data.rows;
onPageChange(page: number) {
this.pageQuery.skipCount = (page - 1) * this.pageQuery.maxResultCount;
this.get();
}

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

@ -28,37 +28,28 @@
(input.debounce)="onSearch($event.target.value)"
/></label>
</div>
<p-table
<abp-table
*ngIf="[150, 250, 250, 250] as columnWidths"
[value]="data$ | async"
[abpLoading]="loading"
[abpTableSort]="{ key: sortKey, order: sortOrder }"
[lazy]="true"
[lazyLoadOnInit]="false"
[paginator]="true"
[rows]="10"
[colgroupTemplate]="tableColGroup"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[value]="data$ | async"
[rows]="pageQuery.maxResultCount"
[totalRecords]="totalCount$ | async"
[loading]="loading"
[resizableColumns]="true"
[scrollable]="true"
(onLazyLoad)="onPageChange($event)"
(pageChange)="onPageChange($event)"
>
<ng-template pTemplate="colgroup">
<ng-template #tableColGroup>
<colgroup>
<col *ngFor="let width of columnWidths" [ngStyle]="{ 'width.px': width || undefined }" />
</colgroup>
</ng-template>
<ng-template pTemplate="emptymessage" let-columns>
<tr
abp-table-empty-message
[attr.colspan]="columnWidths.length"
localizationResource="AbpIdentity"
localizationProp="NoDataAvailableInDatatable"
></tr>
</ng-template>
<ng-template pTemplate="header">
<ng-template #tableHeader>
<tr>
<th>{{ 'AbpIdentity::Actions' | abpLocalization }}</th>
<th pResizableColumn (click)="sortOrderIcon.sort('userName')">
<th (click)="sortOrderIcon.sort('userName')">
{{ 'AbpIdentity::UserName' | abpLocalization }}
<abp-sort-order-icon
#sortOrderIcon
@ -68,7 +59,7 @@
>
</abp-sort-order-icon>
</th>
<th pResizableColumn (click)="sortOrderIcon.sort('email')">
<th (click)="sortOrderIcon.sort('email')">
{{ 'AbpIdentity::EmailAddress' | abpLocalization }}
<abp-sort-order-icon
sortKey="email"
@ -76,7 +67,7 @@
[(order)]="sortOrder"
></abp-sort-order-icon>
</th>
<th pResizableColumn (click)="sortOrderIcon.sort('phoneNumber')">
<th (click)="sortOrderIcon.sort('phoneNumber')">
{{ 'AbpIdentity::PhoneNumber' | abpLocalization }}
<abp-sort-order-icon
sortKey="phoneNumber"
@ -87,7 +78,7 @@
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-data>
<ng-template #tableBody let-data>
<tr>
<td class="text-center">
<div ngbDropdown container="body" class="d-inline-block">
@ -129,7 +120,7 @@
<td>{{ data.phoneNumber }}</td>
</tr>
</ng-template>
</p-table>
</abp-table>
</div>
</div>

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

@ -51,7 +51,7 @@ export class UsersComponent implements OnInit {
providerKey: string;
pageQuery: ABP.PageQueryParams = {};
pageQuery: ABP.PageQueryParams = { maxResultCount: 10 };
isModalVisible: boolean;
@ -111,7 +111,7 @@ export class UsersComponent implements OnInit {
}
}
onSearch(value) {
onSearch(value: string) {
this.pageQuery.filter = value;
this.get();
}
@ -226,9 +226,8 @@ export class UsersComponent implements OnInit {
});
}
onPageChange(data) {
this.pageQuery.skipCount = data.first;
this.pageQuery.maxResultCount = data.rows;
onPageChange(page: number) {
this.pageQuery.skipCount = (page - 1) * this.pageQuery.maxResultCount;
this.get();
}

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

@ -28,37 +28,29 @@
(input.debounce)="onSearch($event.target.value)"
/></label>
</div>
<p-table
<abp-table
*ngIf="[150, 0] as columnWidths"
[abpLoading]="loading"
[abpTableSort]="{ key: sortKey, order: sortOrder }"
[colgroupTemplate]="tableColGroup"
[headerTemplate]="tableHeader"
[bodyTemplate]="tableBody"
[value]="data$ | async"
[abpTableSort]="{ key: sortKey, order: sortOrder }"
[lazy]="true"
[lazyLoadOnInit]="false"
[paginator]="true"
[rows]="10"
[rows]="pageQuery.maxResultCount"
[totalRecords]="totalCount$ | async"
[loading]="loading"
[resizableColumns]="true"
[scrollable]="true"
(onLazyLoad)="onPageChange($event)"
(pageChange)="onPageChange($event)"
>
<ng-template pTemplate="colgroup">
<ng-template #tableColGroup>
<colgroup>
<col *ngFor="let width of columnWidths" [ngStyle]="{ 'width.px': width || undefined }" />
</colgroup>
</ng-template>
<ng-template pTemplate="emptymessage" let-columns>
<tr
abp-table-empty-message
[attr.colspan]="columnWidths.length"
localizationResource="AbpTenantManagement"
localizationProp="NoDataAvailableInDatatable"
></tr>
</ng-template>
<ng-template pTemplate="header" let-columns>
<ng-template #tableHeader let-columns>
<tr>
<th>{{ 'AbpTenantManagement::Actions' | abpLocalization }}</th>
<th pResizableColumn (click)="sortOrderIcon.sort('name')">
<th (click)="sortOrderIcon.sort('name')">
{{ 'AbpTenantManagement::TenantName' | abpLocalization }}
<abp-sort-order-icon
#sortOrderIcon
@ -70,7 +62,7 @@
</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-data>
<ng-template #tableBody let-data>
<tr>
<td class="text-center">
<div ngbDropdown container="body" class="d-inline-block">
@ -117,7 +109,7 @@
<td>{{ data.name }}</td>
</tr>
</ng-template>
</p-table>
</abp-table>
</div>
</div>

9
npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.ts

@ -50,7 +50,7 @@ export class TenantsComponent implements OnInit {
_useSharedDatabase: boolean;
pageQuery: ABP.PageQueryParams = {};
pageQuery: ABP.PageQueryParams = { maxResultCount: 10 };
loading = false;
@ -109,7 +109,7 @@ export class TenantsComponent implements OnInit {
this.get();
}
onSearch(value) {
onSearch(value: string) {
this.pageQuery.filter = value;
this.get();
}
@ -246,9 +246,8 @@ export class TenantsComponent implements OnInit {
});
}
onPageChange(data) {
this.pageQuery.skipCount = data.first;
this.pageQuery.maxResultCount = data.rows;
onPageChange(page: number) {
this.pageQuery.skipCount = (page - 1) * this.pageQuery.maxResultCount;
this.get();
}

1
npm/ng-packs/packages/theme-shared/ng-package.json

@ -12,7 +12,6 @@
"@ngx-validate/core",
"bootstrap",
"font-awesome",
"ngx-perfect-scrollbar",
"primeicons",
"primeng",
"chart.js"

1
npm/ng-packs/packages/theme-shared/package.json

@ -15,7 +15,6 @@
"bootstrap": "^4.3.1",
"chart.js": "^2.9.2",
"font-awesome": "^4.7.0",
"ngx-perfect-scrollbar": "^8.0.0",
"primeicons": "^2.0.0",
"primeng": "^8.1.1"
},

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

@ -2,8 +2,11 @@ export * from './breadcrumb/breadcrumb.component';
export * from './button/button.component';
export * from './chart/chart.component';
export * from './confirmation/confirmation.component';
export * from './loading/loading.component';
export * from './loader-bar/loader-bar.component';
export * from './modal/modal.component';
export * from './pagination/pagination.component';
export * from './sort-order-icon/sort-order-icon.component';
export * from './table-empty-message/table-empty-message.component';
export * from './table/table.component';
export * from './toast/toast.component';
export * from './sort-order-icon/sort-order-icon.component';

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

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'abp-loading',
template: `
<div class="abp-loading">
<i class="fa fa-spinner fa-pulse abp-spinner"></i>
</div>
`,
styles: [
`
.abp-loading {
background: rgba(0, 0, 0, 0.2);
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1040;
}
.abp-loading .abp-spinner {
position: absolute;
top: 50%;
left: 50%;
-moz-transform: translateX(-50%) translateY(-50%);
-o-transform: translateX(-50%) translateY(-50%);
-ms-transform: translateX(-50%) translateY(-50%);
-webkit-transform: translateX(-50%) translateY(-50%);
transform: translateX(-50%) translateY(-50%);
}
`,
],
})
export class LoadingComponent implements OnInit {
constructor() {}
ngOnInit() {}
}

38
npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.html

@ -0,0 +1,38 @@
<div
class="ui-paginator-bottom ui-paginator ui-widget ui-widget-header ui-unselectable-text ui-helper-clearfix"
>
<a
class="ui-paginator-first ui-paginator-element ui-state-default ui-corner-all"
[class.ui-state-disabled]="value === 1"
tabindex="-1"
(click)="changePage(1)"
><span class="ui-paginator-icon pi pi-step-backward"></span></a
><a
class="ui-paginator-prev ui-paginator-element ui-state-default ui-corner-all"
[class.ui-state-disabled]="value === 1"
tabindex="-1"
(click)="changePage(value - 1)"
><span class="ui-paginator-icon pi pi-caret-left"></span></a
><span class="ui-paginator-pages"
><a
*ngFor="let page of pageArray; trackBy: trackByFn"
(click)="changePage(page)"
class="ui-paginator-page ui-paginator-element ui-state-default ui-corner-all"
[class.ui-state-active]="page === value"
tabindex="0"
>{{ page }}</a
></span
><a
class="ui-paginator-next ui-paginator-element ui-state-default ui-corner-all"
[class.ui-state-disabled]="value === totalPages"
tabindex="0"
(click)="changePage(value + 1)"
><span class="ui-paginator-icon pi pi-caret-right"></span></a
><a
class="ui-paginator-last ui-paginator-element ui-state-default ui-corner-all"
[class.ui-state-disabled]="value === totalPages"
tabindex="0"
(click)="changePage(totalPages)"
><span class="ui-paginator-icon pi pi-step-forward"></span
></a>
</div>

52
npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.ts

@ -0,0 +1,52 @@
import { Component, Input, OnInit, Output, EventEmitter, TrackByFunction } from '@angular/core';
@Component({
selector: 'abp-pagination',
templateUrl: 'pagination.component.html',
})
export class PaginationComponent implements OnInit {
private _value = 1;
@Input()
get value(): number {
return this._value;
}
set value(newValue: number) {
if (this._value === newValue) return;
this._value = newValue;
this.valueChange.emit(newValue);
}
@Output()
readonly valueChange = new EventEmitter<number>();
@Input()
totalPages = 0;
get pageArray(): number[] {
const count = this.totalPages < 5 ? this.totalPages : 5;
if (this.value === 1 || this.value === 2) {
return Array.from(new Array(count)).map((_, index) => index + 1);
} else if (this.value === this.totalPages || this.value === this.totalPages - 1) {
return Array.from(new Array(count)).map((_, index) => this.totalPages - count + 1 + index);
} else {
return [this.value - 2, this.value - 1, this.value, this.value + 1, this.value + 2];
}
}
trackByFn: TrackByFunction<number> = (_, page) => page;
ngOnInit() {
if (!this.value || this.value < 1 || this.value > this.totalPages) {
this.value = 1;
}
}
changePage(page: number) {
if (page < 1) return;
else if (page > this.totalPages) return;
this.value = page;
}
}

78
npm/ng-packs/packages/theme-shared/src/lib/components/table/table.component.html

@ -0,0 +1,78 @@
<div #wrapper class="ui-table ui-widget">
<div class="ui-table-wrapper">
<ng-container
*ngTemplateOutlet="scrollable ? scrollableTemplate : defaultTemplate"
></ng-container>
<abp-pagination
*ngIf="rows"
[totalPages]="totalPages"
[(value)]="page"
(valueChange)="pageChange.emit($event)"
></abp-pagination>
</div>
</div>
<ng-template #scrollableTemplate>
<div class="ui-table-scrollable-wrapper">
<div class="ui-table-scrollable-view"></div>
<div class="ui-table-scrollable-header ui-widget-header">
<div [style.margin-left.px]="-bodyScrollLeft" class="ui-table-scrollable-header-box">
<table class="ui-table-scrollable-header-table">
<ng-container *ngTemplateOutlet="colGroup"></ng-container>
<ng-container *ngTemplateOutlet="head"></ng-container>
<tbody></tbody>
</table>
</div>
</div>
<div
#scrollableBody
(scroll)="bodyScrollLeft = scrollableBody.scrollLeft"
class="ui-table-scrollable-body"
[style.max-height]="scrollHeight"
>
<table class="ui-table-scrollable-body-table">
<ng-container *ngTemplateOutlet="colGroup"></ng-container>
<ng-container *ngTemplateOutlet="body"></ng-container>
</table>
</div>
</div>
</ng-template>
<ng-template #defaultTemplate>
<table>
<ng-container *ngTemplateOutlet="colGroup"></ng-container>
<ng-container *ngTemplateOutlet="head"></ng-container>
<ng-container *ngTemplateOutlet="body"></ng-container>
</table>
</ng-template>
<ng-template #colGroup>
<ng-container *ngTemplateOutlet="colgroupTemplate"></ng-container>
</ng-template>
<ng-template #head>
<thead class="ui-table-thead">
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</thead>
</ng-template>
<ng-template #body>
<tbody class="ui-table-tbody">
<ng-container *ngIf="value && value.length; else emptyTemplate">
<ng-template
#bodyTemplateWrapper
*ngFor="let val of slicedValue; let index = index; trackBy: trackByFn"
[ngTemplateOutlet]="bodyTemplate"
[ngTemplateOutletContext]="{ $implicit: val, rowIndex: index }"
></ng-template>
</ng-container>
</tbody>
</ng-template>
<ng-template #emptyTemplate>
<tr class="empty-row" #emptyRow>
<div [style.width.px]="emptyRow.offsetWidth">
{{ emptyMessage | abpLocalization }}
</div>
</tr>
</ng-template>

106
npm/ng-packs/packages/theme-shared/src/lib/components/table/table.component.ts

@ -0,0 +1,106 @@
import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
Output,
TemplateRef,
TrackByFunction,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
@Component({
selector: 'abp-table',
templateUrl: 'table.component.html',
styles: [
`
.ui-table .ui-table-tbody > tr:nth-child(even):hover,
.ui-table .ui-table-tbody > tr:hover {
filter: brightness(90%);
}
.ui-table .ui-table-tbody > tr.empty-row:hover {
filter: none;
}
.ui-table .ui-table-tbody > tr.empty-row > div {
margin: 10px;
text-align: center;
}
`,
],
encapsulation: ViewEncapsulation.None,
})
export class TableComponent {
private _totalRecords: number;
bodyScrollLeft = 0;
@Input()
value: any[];
@Input()
headerTemplate: TemplateRef<any>;
@Input()
bodyTemplate: TemplateRef<any>;
@Input()
colgroupTemplate: TemplateRef<any>;
@Input()
scrollHeight: string;
@Input()
scrollable: boolean;
@Input()
rows: number;
@Input()
page = 1;
@Input()
trackingProp = 'id';
@Input()
emptyMessage = 'AbpAccount::NoDataAvailableInDatatable';
@Output()
readonly pageChange = new EventEmitter<number>();
@ViewChild('wrapper', { read: ElementRef, static: false })
wrapperRef: ElementRef<HTMLDivElement>;
@Input()
get totalRecords(): number {
return this._totalRecords || this.value.length;
}
set totalRecords(newValue: number) {
if (newValue < 0) this._totalRecords = 0;
this._totalRecords = newValue;
}
get totalPages(): number {
if (!this.rows) {
return;
}
return Math.ceil(this.totalRecords / this.rows);
}
get slicedValue(): any[] {
if (!this.rows || this.rows >= this.value.length) {
return this.value;
}
const start = (this.page - 1) * this.rows;
return this.value.slice(start, start + this.rows);
}
trackByFn: TrackByFunction<any> = (_, value) => {
return typeof value === 'object' ? value[this.trackingProp] || value : value;
};
}

1
npm/ng-packs/packages/theme-shared/src/lib/constants/styles.ts

@ -30,6 +30,7 @@ export default `
.ui-table-scrollable-body::-webkit-scrollbar {
height: 5px !important;
width: 5px !important;
}
.ui-table-scrollable-body::-webkit-scrollbar-track {

1
npm/ng-packs/packages/theme-shared/src/lib/directives/index.ts

@ -1 +1,2 @@
export * from './loading.directive';
export * from './table-sort.directive';

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

@ -0,0 +1,74 @@
import {
Directive,
ElementRef,
AfterViewInit,
ViewContainerRef,
ComponentFactoryResolver,
Input,
Injector,
ComponentRef,
ComponentFactory,
HostBinding,
EmbeddedViewRef,
Renderer2,
OnInit,
} from '@angular/core';
import { LoadingComponent } from '../components/loading/loading.component';
@Directive({ selector: '[abpLoading]' })
export class LoadingDirective implements OnInit {
private _loading: boolean;
@HostBinding('style.position')
position = 'relative';
@Input('abpLoading')
get loading(): boolean {
return this._loading;
}
set loading(newValue: boolean) {
setTimeout(() => {
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;
}, 0);
}
@Input('abpLoadingTargetElement')
targetElement: HTMLElement;
componentRef: ComponentRef<LoadingComponent>;
rootNode: HTMLDivElement;
constructor(
private elRef: ElementRef<HTMLElement>,
private vcRef: ViewContainerRef,
private cdRes: ComponentFactoryResolver,
private injector: Injector,
private renderer: Renderer2,
) {}
ngOnInit() {
if (!this.targetElement) {
const { offsetHeight, offsetWidth } = this.elRef.nativeElement;
if (!offsetHeight && !offsetWidth && this.elRef.nativeElement.children.length) {
this.targetElement = this.elRef.nativeElement.children[0] as HTMLElement;
} else {
this.targetElement = this.elRef.nativeElement;
}
}
}
}

39
npm/ng-packs/packages/theme-shared/src/lib/directives/table-sort.directive.ts

@ -1,7 +1,17 @@
import { Directive, Input, Optional, Self, SimpleChanges, OnChanges } from '@angular/core';
import { Table } from 'primeng/table';
import { SortOrder, SortPipe } from '@abp/ng.core';
import {
ChangeDetectorRef,
Directive,
Host,
Input,
OnChanges,
Optional,
Self,
SimpleChanges,
} from '@angular/core';
import clone from 'just-clone';
import { SortPipe, SortOrder } from '@abp/ng.core';
import snq from 'snq';
import { TableComponent } from '../components/table/table.component';
export interface TableSortOptions {
key: string;
@ -15,13 +25,30 @@ export interface TableSortOptions {
export class TableSortDirective implements OnChanges {
@Input()
abpTableSort: TableSortOptions;
@Input()
value: any[] = [];
constructor(@Optional() @Self() private table: Table, private sortPipe: SortPipe) {}
get table(): TableComponent | any {
return (
this.abpTable || snq(() => this.cdRef['_view'].component) || snq(() => this.cdRef['context']) // 'context' for ivy
);
}
constructor(
@Host() @Optional() @Self() private abpTable: TableComponent,
private sortPipe: SortPipe,
private cdRef: ChangeDetectorRef,
) {}
ngOnChanges({ value, abpTableSort }: SimpleChanges) {
if (value || abpTableSort) {
if (this.table && (value || abpTableSort)) {
this.abpTableSort = this.abpTableSort || ({} as TableSortOptions);
this.table.value = this.sortPipe.transform(clone(this.value), this.abpTableSort.order, this.abpTableSort.key);
this.table.value = this.sortPipe.transform(
clone(this.value),
this.abpTableSort.order,
this.abpTableSort.key,
);
}
}
}

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

@ -0,0 +1,82 @@
import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest';
import { LoadingDirective } from '../directives';
import { LoadingComponent } from '../components';
import { Component } from '@angular/core';
@Component({
selector: 'abp-dummy',
template: '<div id="dummy">Testing Loading Directive</div>',
})
export class DummyComponent {}
describe('LoadingDirective', () => {
let spectator: SpectatorDirective<LoadingDirective>;
const createDirective = createDirectiveFactory({
directive: LoadingDirective,
declarations: [LoadingComponent, DummyComponent],
entryComponents: [LoadingComponent],
});
describe('default', () => {
beforeEach(() => {
spectator = createDirective('<div [abpLoading]="status">Testing Loading Directive</div>', {
hostProps: { status: true },
});
});
it('should create the loading component', done => {
setTimeout(() => {
expect(spectator.directive.rootNode).toBeTruthy();
expect(spectator.directive.componentRef).toBeTruthy();
done();
}, 0);
});
});
describe('with custom target', () => {
const mockTarget = document.createElement('div');
const spy = jest.spyOn(mockTarget, 'appendChild');
beforeEach(() => {
spectator = createDirective(
'<div [abpLoading]="status" [abpLoadingTargetElement]="target">Testing Loading Directive</div>',
{
hostProps: { status: true, target: mockTarget },
},
);
});
it('should add the loading component to the DOM', done => {
setTimeout(() => {
expect(spy).toHaveBeenCalled();
done();
}, 0);
});
it('should remove the loading component to the DOM', done => {
const rendererSpy = jest.spyOn(spectator.directive['renderer'], 'removeChild');
spectator.setHostInput({ status: false });
setTimeout(() => {
expect(rendererSpy).toHaveBeenCalled();
expect(spectator.directive.rootNode).toBeFalsy();
done();
}, 0);
});
});
describe('with a component selector', () => {
beforeEach(() => {
spectator = createDirective('<abp-dummy [abpLoading]="status"></abp-dummy>', {
hostProps: { status: true },
});
});
it('should select the child element', done => {
setTimeout(() => {
expect(spectator.directive.targetElement.id).toBe('dummy');
done();
}, 0);
});
});
});

63
npm/ng-packs/packages/theme-shared/src/lib/tests/pagination.component.spec.ts

@ -0,0 +1,63 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { PaginationComponent } from '../components';
describe('PaginationComponent', () => {
let spectator: SpectatorHost<PaginationComponent>;
const createHost = createHostFactory({
component: PaginationComponent,
});
beforeEach(() => {
spectator = createHost(
'<abp-pagination [totalPages]="totalPages" [value]="value"></abp-pagination>',
{
hostProps: {
value: 5,
totalPages: 12,
},
},
);
});
it('should add ui-state-active class to current page', () => {
expect(spectator.query('.ui-state-active').textContent).toBe('5');
});
it('should display the correct pages', () => {
expect(spectator.queryAll('.ui-paginator-page').map(node => node.textContent)).toEqual([
'3',
'4',
'5',
'6',
'7',
]);
spectator.click('.ui-paginator-first');
expect(spectator.queryAll('.ui-paginator-page').map(node => node.textContent)).toEqual([
'1',
'2',
'3',
'4',
'5',
]);
spectator.setHostInput({ value: 12 });
expect(spectator.queryAll('.ui-paginator-page').map(node => node.textContent)).toEqual([
'8',
'9',
'10',
'11',
'12',
]);
spectator.setHostInput({ value: 1, totalPages: 3 });
expect(spectator.queryAll('.ui-paginator-page').map(node => node.textContent)).toEqual([
'1',
'2',
'3',
]);
});
});

74
npm/ng-packs/packages/theme-shared/src/lib/tests/table.component.spec.ts

@ -0,0 +1,74 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { PaginationComponent, TableComponent } from '../components';
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'abpLocalization',
})
export class DummyLocalizationPipe implements PipeTransform {
transform(value: any, ...args: any[]): any {
return value;
}
}
describe('TableComponent', () => {
let spectator: SpectatorHost<TableComponent>;
const createHost = createHostFactory({
component: TableComponent,
declarations: [PaginationComponent, DummyLocalizationPipe],
});
describe('without value', () => {
beforeEach(() => {
spectator = createHost(
`<abp-table
[headerTemplate]="header"
[colgroupTemplate]="colgroup"
[value]="value">
</abp-table>
<ng-template #colgroup><colgroup><col /></colgroup></ng-template>
<ng-template #header><th>name</th></ng-template>`,
{
hostProps: {
value: [],
},
},
);
});
it('should display the empty message', () => {
expect(spectator.query('tr.empty-row>div')).toHaveText(
'AbpAccount::NoDataAvailableInDatatable',
);
});
it('should display the header', () => {
expect(spectator.query('thead')).toBeTruthy();
expect(spectator.query('th')).toHaveText('name');
});
it('should place the colgroup template', () => {
expect(spectator.query('colgroup')).toBeTruthy();
expect(spectator.query('col')).toBeTruthy();
});
});
describe('with value', () => {
// TODO
beforeEach(() => {
spectator = createHost(
`<abp-table
[headerTemplate]="header"
[value]="value"></abp-table>
<ng-template #header><th>name</th></ng-template>
`,
{
hostProps: {
value: [],
},
},
);
});
});
});

23
npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts

@ -1,9 +1,10 @@
import { CoreModule, LazyLoadService } from '@abp/ng.core';
import { DatePipe } from '@angular/common';
import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { MessageService } from 'primeng/components/common/messageservice';
import { ToastModule } from 'primeng/toast';
import { forkJoin } from 'rxjs';
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
import { ButtonComponent } from './components/button/button.component';
import { ChartComponent } from './components/chart/chart.component';
@ -13,16 +14,18 @@ import { LoaderBarComponent } from './components/loader-bar/loader-bar.component
import { ModalComponent } from './components/modal/modal.component';
import { SortOrderIconComponent } from './components/sort-order-icon/sort-order-icon.component';
import { TableEmptyMessageComponent } from './components/table-empty-message/table-empty-message.component';
import { TableComponent } from './components/table/table.component';
import { ToastComponent } from './components/toast/toast.component';
import styles from './constants/styles';
import { TableSortDirective } from './directives/table-sort.directive';
import { ErrorHandler } from './handlers/error.handler';
import { chartJsLoaded$ } from './utils/widget-utils';
import { RootParams } from './models/common';
import { HTTP_ERROR_CONFIG, httpErrorConfigFactory } from './tokens/http-error.token';
import { NgbDateParserFormatter } from '@ng-bootstrap/ng-bootstrap';
import { httpErrorConfigFactory, HTTP_ERROR_CONFIG } from './tokens/http-error.token';
import { DateParserFormatter } from './utils/date-parser-formatter';
import { DatePipe } from '@angular/common';
import { chartJsLoaded$ } from './utils/widget-utils';
import { PaginationComponent } from './components/pagination/pagination.component';
import { LoadingComponent } from './components/loading/loading.component';
import { LoadingDirective } from './directives/loading.directive';
export function appendScript(injector: Injector) {
const fn = () => {
@ -44,10 +47,14 @@ export function appendScript(injector: Injector) {
ConfirmationComponent,
HttpErrorWrapperComponent,
LoaderBarComponent,
LoadingComponent,
ModalComponent,
PaginationComponent,
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
SortOrderIconComponent,
LoadingDirective,
TableSortDirective,
],
exports: [
@ -56,14 +63,18 @@ export function appendScript(injector: Injector) {
ChartComponent,
ConfirmationComponent,
LoaderBarComponent,
LoadingComponent,
ModalComponent,
PaginationComponent,
TableComponent,
TableEmptyMessageComponent,
ToastComponent,
SortOrderIconComponent,
LoadingDirective,
TableSortDirective,
],
providers: [DatePipe],
entryComponents: [HttpErrorWrapperComponent],
entryComponents: [HttpErrorWrapperComponent, LoadingComponent],
})
export class ThemeSharedModule {
constructor(private errorHandler: ErrorHandler) {}

Loading…
Cancel
Save