From e98faf0571d395c854a2bb7e7a86ef008566288c Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 5 Jun 2020 18:22:11 +0800 Subject: [PATCH 001/135] Allow set datatables default configuration --- .../datatables/datatables-extensions.js | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js index 40aebbc71f..e8489ad62c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js @@ -8,8 +8,8 @@ }; /************************************************************************ - * RECORD-ACTIONS extension for datatables * - *************************************************************************/ + * RECORD-ACTIONS extension for datatables * + *************************************************************************/ (function () { if (!$.fn.dataTableExt) { return; @@ -226,8 +226,8 @@ })(); /************************************************************************ - * AJAX extension for datatables * - *************************************************************************/ + * AJAX extension for datatables * + *************************************************************************/ (function () { datatables.createAjax = function (serverMethod, inputAction) { return function (requestData, callback, settings) { @@ -275,8 +275,8 @@ })(); /************************************************************************ - * Configuration/Options normalizer for datatables * - *************************************************************************/ + * Configuration/Options normalizer for datatables * + *************************************************************************/ (function () { var customizeRowActionColumn = function (column) { @@ -291,7 +291,7 @@ datatables.normalizeConfiguration = function (configuration) { - configuration.scrollX = true; + configuration.scrollX = datatables.defaultConfigurations.scrollX; for (var i = 0; i < configuration.columnDefs.length; i++) { var column = configuration.columnDefs[i]; @@ -304,34 +304,22 @@ } } - configuration.language = { - info: localize("PagerInfo"), - infoFiltered: localize("PagerInfoFiltered"), - infoEmpty: localize("PagerInfoEmpty"), - search: localize("PagerSearch"), - processing: localize("ProcessingWithThreeDot"), - loadingRecords: localize("LoadingWithThreeDot"), - lengthMenu: localize("PagerShowMenuEntries"), - emptyTable: localize("NoDataAvailableInDatatable"), - paginate: { - first: localize("PagerFirst"), - last: localize("PagerLast"), - previous: localize("PagerPrevious"), - next: localize("PagerNext") - } - }; + configuration.language = datatables.defaultConfigurations.language; - configuration.dom = '<"dataTable_filters"f>rt<"row dataTable_footer"<"col-auto"l><"col-auto"i><"col"p>>'; + if(configuration.dom){ + configuration.dom += datatables.defaultConfigurations.dom; + }else{ + configuration.dom = datatables.defaultConfigurations.dom; + } return configuration; }; - })(); /************************************************************************ - * Default Renderers * - *************************************************************************/ - + * Default Renderers * + *************************************************************************/ + datatables.defaultRenderers = datatables.defaultRenderers || {}; datatables.defaultRenderers['boolean'] = function(value) { @@ -356,4 +344,31 @@ .toLocaleString(luxon.DateTime.DATETIME_SHORT); }; -})(jQuery); \ No newline at end of file + /************************************************************************ + * Default Configurations * + *************************************************************************/ + + datatables.defaultConfigurations = datatables.defaultConfigurations || {}; + + datatables.defaultConfigurations.scrollX = true; + + datatables.defaultConfigurations.language = { + info: localize("PagerInfo"), + infoFiltered: localize("PagerInfoFiltered"), + infoEmpty: localize("PagerInfoEmpty"), + search: localize("PagerSearch"), + processing: localize("ProcessingWithThreeDot"), + loadingRecords: localize("LoadingWithThreeDot"), + lengthMenu: localize("PagerShowMenuEntries"), + emptyTable: localize("NoDataAvailableInDatatable"), + paginate: { + first: localize("PagerFirst"), + last: localize("PagerLast"), + previous: localize("PagerPrevious"), + next: localize("PagerNext") + } + }; + + datatables.defaultConfigurations.dom = '<"dataTable_filters"f>rt<"row dataTable_footer"<"col-auto"l><"col-auto"i><"col"p>>'; + +})(jQuery); From c2fc5f1671f34ffea60c6ef5ddba02c9d1627573 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Sat, 13 Jun 2020 00:29:05 +0800 Subject: [PATCH 002/135] Update IIdentityClaimTypeRepository & IIdentityRoleRepository --- .../Identity/IIDentityClaimTypeRepository.cs | 23 ++++++++++-- .../Abp/Identity/IIdentityRoleRepository.cs | 13 ++++--- .../EfCoreIdentityClaimTypeRepository.cs | 36 ++++++++++++++----- .../EfCoreIdentityRoleRepository.cs | 11 ++++++ .../MongoIdentityClaimTypeRepository.cs | 33 ++++++++++++++--- .../MongoDB/MongoIdentityRoleRepository.cs | 15 +++++++- .../IdentityClaimTypeRepository_Tests.cs | 6 ++++ .../Identity/IdentityRoleRepository_Tests.cs | 6 ++++ 8 files changed, 122 insertions(+), 21 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIDentityClaimTypeRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIDentityClaimTypeRepository.cs index 0e952bded9..0b803760fb 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIDentityClaimTypeRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIDentityClaimTypeRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Volo.Abp.Domain.Repositories; @@ -15,8 +16,24 @@ namespace Volo.Abp.Identity /// An Id value to ignore on checking. /// If there is an entity with given it's ignored. /// - Task AnyAsync(string name, Guid? ignoredId = null); + /// Cancel token + Task AnyAsync( + string name, + Guid? ignoredId = null, + CancellationToken cancellationToken = default + ); - Task> GetListAsync(string sorting, int maxResultCount, int skipCount, string filter); + Task> GetListAsync( + string sorting, + int maxResultCount, + int skipCount, + string filter, + CancellationToken cancellationToken = default + ); + + Task GetCountAsync( + string filter = null, + CancellationToken cancellationToken = default + ); } -} +} \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs index 590003ca7d..e4d9ed55de 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IIdentityRoleRepository.cs @@ -21,15 +21,20 @@ namespace Volo.Abp.Identity string filter = null, bool includeDetails = false, CancellationToken cancellationToken = default - ); - Task> GetListAsync( - IEnumerable ids, - CancellationToken cancellationToken = default + ); + Task> GetListAsync( + IEnumerable ids, + CancellationToken cancellationToken = default ); Task> GetDefaultOnesAsync( bool includeDetails = false, CancellationToken cancellationToken = default ); + + Task GetCountAsync( + string filter = null, + CancellationToken cancellationToken = default + ); } } \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityClaimTypeRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityClaimTypeRepository.cs index 6d6dcf3d50..0d0ff499c1 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityClaimTypeRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityClaimTypeRepository.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; @@ -11,20 +12,27 @@ namespace Volo.Abp.Identity.EntityFrameworkCore { public class EfCoreIdentityClaimTypeRepository : EfCoreRepository, IIdentityClaimTypeRepository { - public EfCoreIdentityClaimTypeRepository(IDbContextProvider dbContextProvider) + public EfCoreIdentityClaimTypeRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) { - } - public virtual async Task AnyAsync(string name, Guid? ignoredId = null) + public virtual async Task AnyAsync( + string name, + Guid? ignoredId = null, + CancellationToken cancellationToken = default) { return await DbSet - .WhereIf(ignoredId != null, ct => ct.Id != ignoredId) - .CountAsync(ct => ct.Name == name) > 0; + .WhereIf(ignoredId != null, ct => ct.Id != ignoredId) + .CountAsync(ct => ct.Name == name, GetCancellationToken(cancellationToken)) > 0; } - public virtual async Task> GetListAsync(string sorting, int maxResultCount, int skipCount, string filter) + public virtual async Task> GetListAsync( + string sorting, + int maxResultCount, + int skipCount, + string filter, + CancellationToken cancellationToken = default) { var identityClaimTypes = await DbSet .WhereIf( @@ -34,9 +42,21 @@ namespace Volo.Abp.Identity.EntityFrameworkCore ) .OrderBy(sorting ?? "name desc") .PageBy(skipCount, maxResultCount) - .ToListAsync(); + .ToListAsync(GetCancellationToken(cancellationToken)); return identityClaimTypes; } + + public async Task GetCountAsync( + string filter = null, + CancellationToken cancellationToken = default) + { + return await DbSet + .WhereIf( + !filter.IsNullOrWhiteSpace(), + u => + u.Name.Contains(filter) + ).LongCountAsync(GetCancellationToken(cancellationToken)); + } } -} +} \ No newline at end of file diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs index 2814abd891..c0271fa9e9 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs @@ -60,6 +60,17 @@ namespace Volo.Abp.Identity.EntityFrameworkCore return await DbSet.IncludeDetails(includeDetails).Where(r => r.IsDefault).ToListAsync(GetCancellationToken(cancellationToken)); } + public async Task GetCountAsync( + string filter = null, + CancellationToken cancellationToken = default) + { + return await DbSet + .WhereIf(!filter.IsNullOrWhiteSpace(), + x => x.Name.Contains(filter) || + x.NormalizedName.Contains(filter)) + .LongCountAsync(GetCancellationToken(cancellationToken)); + } + public override IQueryable WithDetails() { return GetQueryable().IncludeDetails(); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityClaimTypeRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityClaimTypeRepository.cs index 8880bcaf1c..a123868799 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityClaimTypeRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityClaimTypeRepository.cs @@ -5,6 +5,7 @@ using MongoDB.Driver; using MongoDB.Driver.Linq; using System.Linq; using System.Linq.Dynamic.Core; +using System.Threading; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.MongoDB; @@ -16,23 +17,31 @@ namespace Volo.Abp.Identity.MongoDB { } - public virtual async Task AnyAsync(string name, Guid? ignoredId = null) + public virtual async Task AnyAsync( + string name, + Guid? ignoredId = null, + CancellationToken cancellationToken = default) { if (ignoredId == null) { return await GetMongoQueryable() .Where(ct => ct.Name == name) - .AnyAsync(); + .AnyAsync(GetCancellationToken(cancellationToken)); } else { return await GetMongoQueryable() .Where(ct => ct.Id != ignoredId && ct.Name == name) - .AnyAsync(); + .AnyAsync(GetCancellationToken(cancellationToken)); } } - public virtual async Task> GetListAsync(string sorting, int maxResultCount, int skipCount, string filter) + public virtual async Task> GetListAsync( + string sorting, + int maxResultCount, + int skipCount, + string filter, + CancellationToken cancellationToken = default) { return await GetMongoQueryable() .WhereIf>( @@ -43,7 +52,21 @@ namespace Volo.Abp.Identity.MongoDB .OrderBy(sorting ?? nameof(IdentityClaimType.Name)) .As>() .PageBy>(skipCount, maxResultCount) - .ToListAsync(); + .ToListAsync(GetCancellationToken(cancellationToken)); + } + + public async Task GetCountAsync( + string filter = null, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + u => + u.Name.Contains(filter) + ) + .As>() + .LongCountAsync(GetCancellationToken(cancellationToken)); } } } diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs index 2651fbfa6c..f4b0a365c1 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs @@ -54,9 +54,22 @@ namespace Volo.Abp.Identity.MongoDB } public virtual async Task> GetDefaultOnesAsync( - bool includeDetails = false, CancellationToken cancellationToken = default) + bool includeDetails = false, + CancellationToken cancellationToken = default) { return await GetMongoQueryable().Where(r => r.IsDefault).ToListAsync(cancellationToken: GetCancellationToken(cancellationToken)); } + + public async Task GetCountAsync( + string filter = null, + CancellationToken cancellationToken = default) + { + return await GetMongoQueryable() + .WhereIf(!filter.IsNullOrWhiteSpace(), + x => x.Name.Contains(filter) || + x.NormalizedName.Contains(filter)) + .As>() + .LongCountAsync(GetCancellationToken(cancellationToken)); + } } } \ No newline at end of file diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityClaimTypeRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityClaimTypeRepository_Tests.cs index 3250c0b04a..c4483aeefa 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityClaimTypeRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityClaimTypeRepository_Tests.cs @@ -36,5 +36,11 @@ namespace Volo.Abp.Identity result2.ShouldBe(false); } + + [Fact] + public async Task GetCountAsync_With_Filter() + { + (await ClaimTypeRepository.GetCountAsync("Age")).ShouldBe(1); + } } } diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityRoleRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityRoleRepository_Tests.cs index 1f65dd821f..d170a5b838 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityRoleRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityRoleRepository_Tests.cs @@ -53,6 +53,12 @@ namespace Volo.Abp.Identity (await RoleRepository.GetCountAsync()).ShouldBeGreaterThan(0); } + [Fact] + public async Task GetCountAsync_With_Filter() + { + (await RoleRepository.GetCountAsync("admin")).ShouldBe(1); + } + [Fact] public async Task Should_Eager_Load_Role_Collections() { From e5634fcfa592b796c3b28c9157567173848df3e4 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Mon, 15 Jun 2020 12:38:06 +0300 Subject: [PATCH 003/135] feat: add LazyModuleFactory --- .../core/src/lib/tests/factory-utils.spec.ts | 38 +++++++++++++++++++ .../core/src/lib/utils/factory-utils.ts | 31 +++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + 3 files changed, 70 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts new file mode 100644 index 0000000000..06e8a8d044 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/factory-utils.spec.ts @@ -0,0 +1,38 @@ +import { Injector, ModuleWithProviders, NgModule } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { LazyModuleFactory } from '../utils/factory-utils'; + +@NgModule() +class Module { + static forChild(): ModuleWithProviders { + return { + ngModule: Module, + providers: [{ provide: 'foo', useValue: 'bar' }], + }; + } +} + +describe('LazyModuleFactory', () => { + const factory = new LazyModuleFactory(Module.forChild()); + + describe('#moduleType', () => { + it('should return the ngModule property of given ModuleWithProviders', () => { + expect(factory.moduleType).toBe(Module); + }); + }); + + describe('#create', () => { + it('should return an instance of NgModuleRef_', () => { + TestBed.configureTestingModule({}); + + const injector = TestBed.inject(Injector); + const moduleRef = factory.create(injector); + + expect('componentFactoryResolver' in moduleRef).toBe(true); + expect('destroy' in moduleRef).toBe(true); + expect('injector' in moduleRef).toBe(true); + expect('instance' in moduleRef).toBe(true); + expect('onDestroy' in moduleRef).toBe(true); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts new file mode 100644 index 0000000000..bc6273fa82 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/factory-utils.ts @@ -0,0 +1,31 @@ +import { + Compiler, + Injector, + ModuleWithProviders, + NgModuleFactory, + NgModuleRef, + StaticProvider, + Type, +} from '@angular/core'; + +export class LazyModuleFactory extends NgModuleFactory { + get moduleType(): Type { + return this.moduleWithProviders.ngModule; + } + + constructor(private moduleWithProviders: ModuleWithProviders) { + super(); + } + + create(parentInjector: Injector | null): NgModuleRef { + const injector = Injector.create({ + parent: parentInjector, + providers: this.moduleWithProviders.providers as StaticProvider[], + }); + + const compiler = injector.get(Compiler); + const factory = compiler.compileModuleSync(this.moduleType); + + return factory.create(injector); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index 4dde8134ef..b75fef8efe 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -1,4 +1,5 @@ export * from './common-utils'; +export * from './factory-utils'; export * from './form-utils'; export * from './generator-utils'; export * from './initial-utils'; From 117b47bba633c205f616516e8366ad37cfc0b7ac Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 16 Jun 2020 10:35:51 +0300 Subject: [PATCH 004/135] feat: add pushValueTo array util to core --- .../core/src/lib/tests/array-utils.spec.ts | 17 +++++++++++++++++ .../packages/core/src/lib/utils/array-utils.ts | 6 ++++++ .../packages/core/src/lib/utils/index.ts | 1 + 3 files changed, 24 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/array-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts new file mode 100644 index 0000000000..3ccc91461c --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/array-utils.spec.ts @@ -0,0 +1,17 @@ +import { pushValueTo } from '../utils/array-utils'; + +describe('Array Utils', () => { + describe('#pushValueTo', () => { + test.each` + source | target | expected + ${[]} | ${[0, 1, 2, 3]} | ${[0, 1, 2, 3]} + ${[3]} | ${[0, 1, 2]} | ${[0, 1, 2, 3]} + ${[2, 3]} | ${[0, 1]} | ${[0, 1, 2, 3]} + ${[1, 2, 3]} | ${[0]} | ${[0, 1, 2, 3]} + ${[0, 1, 2, 3]} | ${[]} | ${[0, 1, 2, 3]} + `('should push $source to $target when called in forEach', ({ source, target, expected }) => { + source.forEach(pushValueTo(target)); + expect(target).toEqual(expected); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/array-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/array-utils.ts new file mode 100644 index 0000000000..d2d74105b3 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/array-utils.ts @@ -0,0 +1,6 @@ +export function pushValueTo(array: T[]) { + return (element: T) => { + array.push(element); + return array; + }; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index b75fef8efe..ce89b4dddb 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -1,3 +1,4 @@ +export * from './array-utils'; export * from './common-utils'; export * from './factory-utils'; export * from './form-utils'; From 12995cd1f4df9183b533a3885f292611d9c582ee Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:24:05 +0300 Subject: [PATCH 005/135] chore: remove pagination.component --- .../theme-shared/src/lib/components/index.ts | 1 - .../pagination/pagination.component.html | 38 ------------- .../pagination/pagination.component.ts | 55 ------------------- .../lib/tests/table-sort.directive.spec.ts | 3 +- .../src/lib/tests/table.component.spec.ts | 4 +- .../src/lib/theme-shared.module.ts | 3 - 6 files changed, 3 insertions(+), 101 deletions(-) delete mode 100644 npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.html delete mode 100644 npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.ts diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts index 7461bd3841..3163d963a7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/index.ts @@ -5,7 +5,6 @@ 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'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.html deleted file mode 100644 index b42f45a148..0000000000 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.html +++ /dev/null @@ -1,38 +0,0 @@ - diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.ts deleted file mode 100644 index d6249d4f39..0000000000 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/pagination/pagination.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component, Input, OnInit, Output, EventEmitter, TrackByFunction } from '@angular/core'; - -@Component({ - selector: 'abp-pagination', - templateUrl: 'pagination.component.html', -}) -/** - * @deprecated - */ -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(); - - @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 = (_, 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; - } -} diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/table-sort.directive.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/table-sort.directive.spec.ts index 5298af4dbd..ad43a11b87 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/table-sort.directive.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/table-sort.directive.spec.ts @@ -2,7 +2,6 @@ import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/je import { TableSortDirective } from '../directives/table-sort.directive'; import { TableComponent } from '../components/table/table.component'; import { DummyLocalizationPipe } from './table.component.spec'; -import { PaginationComponent } from '../components'; import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; describe('TableSortDirective', () => { @@ -10,7 +9,7 @@ describe('TableSortDirective', () => { let directive: TableSortDirective; const createDirective = createDirectiveFactory({ directive: TableSortDirective, - declarations: [TableComponent, DummyLocalizationPipe, PaginationComponent], + declarations: [TableComponent, DummyLocalizationPipe], imports: [NgbPaginationModule], }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/tests/table.component.spec.ts b/npm/ng-packs/packages/theme-shared/src/lib/tests/table.component.spec.ts index d2bf2829c7..d456cebd34 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/tests/table.component.spec.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/tests/table.component.spec.ts @@ -1,7 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest'; -import { PaginationComponent, TableComponent } from '../components'; +import { TableComponent } from '../components'; @Pipe({ name: 'abpLocalization', @@ -16,7 +16,7 @@ describe('TableComponent', () => { let spectator: SpectatorHost; const createHost = createHostFactory({ component: TableComponent, - declarations: [PaginationComponent, DummyLocalizationPipe], + declarations: [DummyLocalizationPipe], imports: [NgbPaginationModule], }); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts index 6afd044725..ab9afc5a56 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/theme-shared.module.ts @@ -14,7 +14,6 @@ import { LoaderBarComponent } from './components/loader-bar/loader-bar.component import { LoadingComponent } from './components/loading/loading.component'; import { ModalContainerComponent } from './components/modal/modal-container.component'; import { ModalComponent } from './components/modal/modal.component'; -import { PaginationComponent } from './components/pagination/pagination.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'; @@ -59,7 +58,6 @@ export function ngxDatatableMessageFactory(store: Store) { LoadingComponent, ModalComponent, ModalContainerComponent, - PaginationComponent, TableComponent, TableEmptyMessageComponent, ToastComponent, @@ -80,7 +78,6 @@ export function ngxDatatableMessageFactory(store: Store) { LoaderBarComponent, LoadingComponent, ModalComponent, - PaginationComponent, TableComponent, TableEmptyMessageComponent, ToastComponent, From d83f008113d8ad3419b969f51d1e9c067a1c981b Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:24:39 +0300 Subject: [PATCH 006/135] chore: remove requirements option from CoreModule's Root type --- npm/ng-packs/packages/core/src/lib/models/common.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 773233857f..08f7681b5e 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -7,11 +7,6 @@ import { Config } from './config'; export namespace ABP { export interface Root { environment: Partial; - /** - * - * @deprecated To be deleted in v3.0 - */ - requirements?: Config.Requirements; skipGetAppConfiguration?: boolean; sendNullsAsQueryParam?: boolean; } From a7382b5cc99a6edcce181e6ca6a012b7fe1e0752 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:25:04 +0300 Subject: [PATCH 007/135] refactor: remove old load method of LazyLoadService --- .../src/lib/services/lazy-load.service.ts | 115 ++---------------- .../src/lib/tests/lazy-load.service.spec.ts | 94 +------------- 2 files changed, 14 insertions(+), 195 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts b/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts index 4ba89949fd..5d44bb0879 100644 --- a/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts @@ -1,8 +1,7 @@ import { Injectable } from '@angular/core'; -import { concat, Observable, of, ReplaySubject, throwError } from 'rxjs'; +import { concat, Observable, of, throwError } from 'rxjs'; import { delay, retryWhen, shareReplay, take, tap } from 'rxjs/operators'; import { LoadingStrategy } from '../strategies'; -import { uuid } from '../utils'; @Injectable({ providedIn: 'root', @@ -10,108 +9,20 @@ import { uuid } from '../utils'; export class LazyLoadService { readonly loaded = new Map(); - loadedLibraries: { [url: string]: ReplaySubject } = {}; + load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable { + if (this.loaded.has(strategy.path)) return of(new CustomEvent('load')); - load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable; - /** - * - * @deprecated Use other overload that requires a strategy as first param - */ - load( - urlOrUrls: string | string[], - type: 'script' | 'style', - content?: string, - targetQuery?: string, - position?: InsertPosition, - ): Observable; - load( - strategyOrUrl: LoadingStrategy | string | string[], - retryTimesOrType?: number | 'script' | 'style', - retryDelayOrContent?: number | string, - targetQuery: string = 'body', - position: InsertPosition = 'beforeend', - ): Observable { - if (strategyOrUrl instanceof LoadingStrategy) { - const strategy = strategyOrUrl; - const retryTimes = typeof retryTimesOrType === 'number' ? retryTimesOrType : 2; - const retryDelay = typeof retryDelayOrContent === 'number' ? retryDelayOrContent : 1000; - - if (this.loaded.has(strategy.path)) return of(new CustomEvent('load')); - - return strategy.createStream().pipe( - retryWhen(error$ => - concat( - error$.pipe(delay(retryDelay), take(retryTimes)), - throwError(new CustomEvent('error')), - ), + return strategy.createStream().pipe( + retryWhen(error$ => + concat( + error$.pipe(delay(retryDelay), take(retryTimes)), + throwError(new CustomEvent('error')), ), - tap(() => this.loaded.set(strategy.path, strategy.element)), - delay(100), - shareReplay({ bufferSize: 1, refCount: true }), - ); - } - - let urlOrUrls = strategyOrUrl; - const content = (retryDelayOrContent as string) || ''; - const type = retryTimesOrType as 'script' | 'style'; - - if (!urlOrUrls && !content) { - return throwError('Should pass url or content'); - } else if (!urlOrUrls && content) { - urlOrUrls = [null]; - } - - if (!Array.isArray(urlOrUrls)) { - urlOrUrls = [urlOrUrls]; - } - - return new Observable(subscriber => { - (urlOrUrls as string[]).forEach((url, index) => { - const key = url ? url.slice(url.lastIndexOf('/') + 1) : uuid(); - - if (this.loadedLibraries[key]) { - subscriber.next(); - subscriber.complete(); - return; - } - - this.loadedLibraries[key] = new ReplaySubject(); - - let library; - if (type === 'script') { - library = document.createElement('script'); - library.type = 'text/javascript'; - if (url) { - (library as HTMLScriptElement).src = url; - } - - (library as HTMLScriptElement).text = content; - } else if (url) { - library = document.createElement('link'); - library.type = 'text/css'; - (library as HTMLLinkElement).rel = 'stylesheet'; - - if (url) { - (library as HTMLLinkElement).href = url; - } - } else { - library = document.createElement('style'); - (library as HTMLStyleElement).textContent = content; - } - - library.onload = () => { - this.loadedLibraries[key].next(); - this.loadedLibraries[key].complete(); - - if (index === urlOrUrls.length - 1) { - subscriber.next(); - subscriber.complete(); - } - }; - - document.querySelector(targetQuery).insertAdjacentElement(position, library); - }); - }); + ), + tap(() => this.loaded.set(strategy.path, strategy.element)), + delay(100), + shareReplay({ bufferSize: 1, refCount: true }), + ); } remove(path: string): boolean { diff --git a/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts index 4868fb78f3..dc4d937f17 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/lazy-load.service.spec.ts @@ -1,6 +1,5 @@ -import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; import { of, throwError } from 'rxjs'; -import { catchError, switchMap } from 'rxjs/operators'; +import { switchMap } from 'rxjs/operators'; import { LazyLoadService } from '../services/lazy-load.service'; import { ScriptLoadingStrategy } from '../strategies'; @@ -84,94 +83,3 @@ describe('LazyLoadService', () => { }); }); }); - -describe('LazyLoadService (Deprecated)', () => { - let spectator: SpectatorService; - let service: LazyLoadService; - const scriptElement = document.createElement('script'); - const linkElement = document.createElement('link'); - const styleElement = document.createElement('style'); - const cloneDocument = { ...document }; - - const createService = createServiceFactory({ service: LazyLoadService }); - - beforeEach(() => { - spectator = createService(); - service = spectator.service; - }); - - afterEach(() => (document = { ...cloneDocument })); - - test('should load script with content just one time', done => { - const spy = jest.spyOn(document, 'createElement'); - spy.mockReturnValue(scriptElement); - - service.load('https://abp.io', 'script', 'test').subscribe(res => { - expect( - document.querySelector('script[src="https://abp.io"][type="text/javascript"]').textContent, - ).toMatch('test'); - }); - - scriptElement.onload(null); - - service.load('https://abp.io', 'script', 'test').subscribe(res => { - expect( - document.querySelectorAll('script[src="https://abp.io"][type="text/javascript"]'), - ).toHaveLength(1); - done(); - }); - }); - - test('should load style element', done => { - const spy = jest.spyOn(document, 'createElement'); - spy.mockReturnValue(styleElement); - - const content = '* { color: black; }'; - service.load(null, 'style', content).subscribe(res => { - expect(document.querySelector('style').textContent).toMatch(content); - done(); - }); - - styleElement.onload(null); - }); - - describe('style with url', () => { - beforeEach(() => { - const spy = jest.spyOn(document, 'createElement'); - spy.mockReturnValue(linkElement); - }); - - test('should load an link element', done => { - service.load('https://abp.io', 'style').subscribe(res => { - expect( - document.querySelector('link[type="text/css"][rel="stylesheet"][href="https://abp.io"]'), - ).toBeTruthy(); - done(); - }); - - linkElement.onload(null); - }); - - test('should load link elements', done => { - service.load(['https://abp.io', 'https://volosoft.com'], 'style').subscribe(res => { - expect(document.querySelector('link[href="https://volosoft.com"]')).toBeTruthy(); - done(); - }); - - linkElement.onload(null); - }); - }); - - test('should throw error when required parameters are null', done => { - service - .load(null, 'style') - .pipe( - catchError(err => { - expect(err).toBeTruthy(); - done(); - return of(null); - }), - ) - .subscribe(); - }); -}); From ed41a884bc38c5eb0a8f3531f0c899d7b6082ae3 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 16 Jun 2020 11:25:09 +0300 Subject: [PATCH 008/135] feat: add tree utils to core --- .../core/src/lib/tests/tree-utils.spec.ts | 68 +++++++++++++++++++ .../packages/core/src/lib/utils/index.ts | 1 + .../packages/core/src/lib/utils/tree-utils.ts | 51 ++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts create mode 100644 npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts new file mode 100644 index 0000000000..454c1c6891 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts @@ -0,0 +1,68 @@ +import { createTreeFromList, TreeNode } from '../utils/tree-utils'; + +const LIST_1 = [ + { id: 1, pid: null }, + { id: 2, pid: 1 }, +]; +const LIST_2 = [ + { id: 1, pid: null }, + { id: 2, pid: 1 }, + { id: 3, pid: 1 }, +]; +const LIST_3 = [ + { id: 1, pid: null }, + { id: 2, pid: 1 }, + { id: 3, pid: 2 }, +]; +const TREE_1 = [ + { id: 1, pid: null, isLeaf: false, children: [{ id: 2, pid: 1, isLeaf: true, children: [] }] }, +]; +const TREE_2 = [ + { + id: 1, + pid: null, + isLeaf: false, + children: [ + { id: 2, pid: 1, isLeaf: true, children: [] }, + { id: 3, pid: 1, isLeaf: true, children: [] }, + ], + }, +]; +const TREE_3 = [ + { + id: 1, + pid: null, + isLeaf: false, + children: [ + { id: 2, pid: 1, isLeaf: false, children: [{ id: 3, pid: 2, isLeaf: true, children: [] }] }, + ], + }, +]; +describe('Tree Utils', () => { + describe('createTreeFromList', () => { + test.each` + list | expected + ${LIST_1} | ${TREE_1} + ${LIST_2} | ${TREE_2} + ${LIST_3} | ${TREE_3} + `('should return $expected when given $list', ({ list, expected }: TestCreateTreeFromList) => { + expect( + createTreeFromList( + list, + x => x.id, + x => x.pid, + ), + ).toEqual(expected); + }); + }); +}); + +interface TestCreateTreeFromList { + list: ModelA[]; + expected: TreeNode[]; +} + +interface ModelA { + id: 1; + pid: null; +} diff --git a/npm/ng-packs/packages/core/src/lib/utils/index.ts b/npm/ng-packs/packages/core/src/lib/utils/index.ts index ce89b4dddb..6f6136b195 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/index.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/index.ts @@ -9,3 +9,4 @@ export * from './localization-utils'; export * from './number-utils'; export * from './route-utils'; export * from './rxjs-utils'; +export * from './tree-utils'; diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts new file mode 100644 index 0000000000..86844a4375 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -0,0 +1,51 @@ +export class TreeNode { + children: TreeNode[] = []; + isLeaf = true; + + constructor(props: T) { + Object.assign(this, props); + } +} + +export function createTreeFromList>( + list: T[], + keySelector: (item: T) => NodeKey, + parentKeySelector: (item: T) => NodeKey, + valueMapper = (item: T) => new TreeNode(item) as R, +) { + const map = createMapFromList(list, keySelector, valueMapper); + const tree: ReturnType[] = []; + + list.forEach(row => { + const id = keySelector(row); + const parentId = parentKeySelector(row); + const node = map.get(id); + + if (parentId) { + const parent = map.get(parentId); + parent.children.push(node); + parent.isLeaf = false; + } else { + tree.push(node); + } + }); + + return tree; +} + +export function createMapFromList>( + list: T[], + keySelector: (item: T) => NodeKey, + valueMapper = (item: T) => new TreeNode(item) as R, +) { + const map = new Map, ReturnType>(); + list.forEach(row => map.set(keySelector(row), valueMapper(row))); + return map; +} + +type NodeKey = number | string | Symbol; + +interface BranchOrLeaf { + children: BranchOrLeaf[]; + isLeaf: boolean; +} From e4d934c1b6d7468ddbdcfe254ba78538ca9bf6fd Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:25:32 +0300 Subject: [PATCH 009/135] refactor: import OAuthModule to RootcoreModule --- npm/ng-packs/packages/core/src/lib/core.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/core/src/lib/core.module.ts b/npm/ng-packs/packages/core/src/lib/core.module.ts index c0223d28a6..72aeacb29d 100644 --- a/npm/ng-packs/packages/core/src/lib/core.module.ts +++ b/npm/ng-packs/packages/core/src/lib/core.module.ts @@ -117,6 +117,7 @@ export class BaseCoreModule {} NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]), NgxsRouterPluginModule.forRoot(), NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }), + OAuthModule.forRoot(), ], }) export class RootCoreModule {} @@ -195,7 +196,6 @@ export class CoreModule { deps: [LocalizationService], useFactory: noop, }, - ...OAuthModule.forRoot().providers, { provide: OAuthStorage, useFactory: storageFactory }, ], }; From bb5a01ee620ab804c696997bd0e96068459c4511 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:26:25 +0300 Subject: [PATCH 010/135] chore: remove Toaster.Status enum --- .../packages/theme-shared/src/lib/models/toaster.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts index 16d06e1b60..e7f456fee4 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts @@ -20,13 +20,4 @@ export namespace Toaster { } export type Severity = 'neutral' | 'success' | 'info' | 'warning' | 'error'; - - /** - * @deprecated Status will be removed from toaster model in v3.0 - */ - export enum Status { - confirm = 'confirm', - reject = 'reject', - dismiss = 'dismiss', - } } From 576f2d76d6ee73a5ade11b992b219744a99bb6c7 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:26:47 +0300 Subject: [PATCH 011/135] chore: remove closable option from Confirmation.Options --- .../packages/theme-shared/src/lib/models/confirmation.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts b/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts index 18dceb320d..086942a1c1 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts @@ -10,12 +10,6 @@ export namespace Confirmation { hideYesBtn?: boolean; cancelText?: Config.LocalizationParam; yesText?: Config.LocalizationParam; - - /** - * - * @deprecated To be deleted in v2.9 - */ - closable?: boolean; } export interface DialogData { From 080d209a62bd88b2f28fcdbbc1985744e8ab5463 Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 11:28:09 +0300 Subject: [PATCH 012/135] feat: set the scrollbarH and virtualization inputs of ngx-datatable initial values --- .../src/lib/directives/ngx-datatable-default.directive.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts index 3672bde3b3..ec6710474f 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts @@ -19,5 +19,7 @@ export class NgxDatatableDefaultDirective { this.table.footerHeight = 50; this.table.headerHeight = 50; this.table.rowHeight = 'auto'; + this.table.scrollbarH = true; + this.table.virtualization = false; } } From 66111b9afa8546103d4cbf0b597c77c889d0a7cd Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Tue, 16 Jun 2020 14:21:28 +0300 Subject: [PATCH 013/135] feat: improve tree util types --- .../packages/core/src/lib/utils/tree-utils.ts | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts index 86844a4375..607a30ec52 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -1,20 +1,24 @@ -export class TreeNode { +export class TreeNodeFactory { children: TreeNode[] = []; isLeaf = true; constructor(props: T) { Object.assign(this, props); } + + static create(props: T) { + return new TreeNodeFactory(props) as TreeNode; + } } -export function createTreeFromList>( +export function createTreeFromList( list: T[], - keySelector: (item: T) => NodeKey, - parentKeySelector: (item: T) => NodeKey, - valueMapper = (item: T) => new TreeNode(item) as R, + keySelector: (item: T) => number | string | Symbol, + parentKeySelector: typeof keySelector, + valueMapper: (item: T) => R, ) { const map = createMapFromList(list, keySelector, valueMapper); - const tree: ReturnType[] = []; + const tree: NodeValue[] = []; list.forEach(row => { const id = keySelector(row); @@ -23,8 +27,8 @@ export function createTreeFromList>( if (parentId) { const parent = map.get(parentId); - parent.children.push(node); - parent.isLeaf = false; + (parent as any).children.push(node); + (parent as any).isLeaf = false; } else { tree.push(node); } @@ -33,19 +37,25 @@ export function createTreeFromList>( return tree; } -export function createMapFromList>( +export function createMapFromList( list: T[], - keySelector: (item: T) => NodeKey, - valueMapper = (item: T) => new TreeNode(item) as R, + keySelector: (item: T) => number | string | Symbol, + valueMapper: (item: T) => R, ) { - const map = new Map, ReturnType>(); + type Key = ReturnType; + type Value = NodeValue; + const map = new Map(); list.forEach(row => map.set(keySelector(row), valueMapper(row))); return map; } -type NodeKey = number | string | Symbol; - -interface BranchOrLeaf { - children: BranchOrLeaf[]; +export type TreeNode = { + [K in keyof T]: T[K]; +} & { + children: TreeNode[]; isLeaf: boolean; -} +}; + +type NodeValue any> = F extends undefined + ? TreeNode + : ReturnType; From c188c6773ee89000c67f8741112f4b730ae1e86d Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Tue, 16 Jun 2020 14:38:47 +0300 Subject: [PATCH 014/135] Update SignalR-Integration.md resolved https://github.com/abpframework/abp/issues/4184 --- docs/en/SignalR-Integration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/en/SignalR-Integration.md b/docs/en/SignalR-Integration.md index f318967888..5b176a84a5 100644 --- a/docs/en/SignalR-Integration.md +++ b/docs/en/SignalR-Integration.md @@ -234,4 +234,5 @@ Refer to the Microsoft's documentation to [host and scale](https://docs.microsof ## See Also -* [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) \ No newline at end of file +* [Microsoft SignalR documentation](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction) +* [Real-Time Messaging In A Distributed Architecture Using ABP, SingalR & RabbitMQ](https://volosoft.com/blog/RealTime-Messaging-Distributed-Architecture-Abp-SingalR-RabbitMQ) From 12f9d45c0826648e82e8ba800a3a555a79f75abf Mon Sep 17 00:00:00 2001 From: mehmet-erim Date: Tue, 16 Jun 2020 15:50:37 +0300 Subject: [PATCH 015/135] feat: add getUserAssingableRoles method to identity.service #4291 --- .../identity/src/lib/components/users/users.component.ts | 2 +- .../identity/src/lib/services/identity.service.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) 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 aeeb250126..73da8ad38d 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 @@ -82,7 +82,7 @@ export class UsersComponent implements OnInit { } buildForm() { - this.identityService.getAllRoles().subscribe(({ items }) => { + this.identityService.getUserAssingableRoles().subscribe(({ items }) => { this.roles = items; this.form = this.fb.group({ userName: [this.selected.userName || '', [Validators.required, Validators.maxLength(256)]], diff --git a/npm/ng-packs/packages/identity/src/lib/services/identity.service.ts b/npm/ng-packs/packages/identity/src/lib/services/identity.service.ts index 51e88557fe..7413079279 100644 --- a/npm/ng-packs/packages/identity/src/lib/services/identity.service.ts +++ b/npm/ng-packs/packages/identity/src/lib/services/identity.service.ts @@ -103,6 +103,15 @@ export class IdentityService { return this.rest.request(request, { apiName: this.apiName }); } + getUserAssingableRoles(): Observable { + const request: Rest.Request = { + method: 'GET', + url: '/api/identity/users/assignable-roles', + }; + + return this.rest.request(request, { apiName: this.apiName }); + } + deleteUser(id: string): Observable { const request: Rest.Request = { method: 'DELETE', From 7b1eafa5321de22b164966811be5a17d96ebfd1e Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 16 Jun 2020 21:15:19 +0800 Subject: [PATCH 016/135] Translate unit of work document --- docs/en/Unit-Of-Work.md | 4 +- docs/zh-Hans/Unit-Of-Work.md | 356 ++++++++++++++++++++++++++++++++++- docs/zh-Hans/docs-nav.json | 3 +- 3 files changed, 358 insertions(+), 5 deletions(-) diff --git a/docs/en/Unit-Of-Work.md b/docs/en/Unit-Of-Work.md index 0bbd81ebab..907e601d30 100644 --- a/docs/en/Unit-Of-Work.md +++ b/docs/en/Unit-Of-Work.md @@ -52,7 +52,7 @@ Configure(options => ### Option Properties * `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). A global point to configure the transaction behavior. Default value is `Auto` and work as explained in the "*Database Transaction Behavior*" section above. You can enable (even for HTTP GET requests) or disable transactions with this option. -* `TimeOut` (`TimeSpan?`): Used to set the timeout value for UOWs. **Default value is `null`** and uses to the default of the underlying database provider. +* `TimeOut` (`int?`): Used to set the timeout value for UOWs. **Default value is `null`** and uses to the default of the underlying database provider. * `IsolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. ## Controlling the Unit Of Work @@ -227,7 +227,7 @@ namespace AbpDemo * `requiresNew` (`bool`): Set `true` to ignore the surrounding unit of work and start a new UOW with the provided options. **Default value is `false`. If it is `false` and there is a surrounding UOW, `Begin` method doesn't actually begin a new UOW, but silently participates to the existing UOW.** * `isTransactional` (`bool`). Default value is `false`. * `isolationLevel` (`IsolationLevel?`): Used to set the [isolation level](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel) of the database transaction, if the UOW is transactional. If not set, uses the default configured value. -* `TimeOut` (`TimeSpan?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value. +* `TimeOut` (`int?`): Used to set the timeout value for this UOW. **Default value is `null`** and fallbacks to the default configured value. ### The Current Unit Of Work diff --git a/docs/zh-Hans/Unit-Of-Work.md b/docs/zh-Hans/Unit-Of-Work.md index dc281a0974..233467250b 100644 --- a/docs/zh-Hans/Unit-Of-Work.md +++ b/docs/zh-Hans/Unit-Of-Work.md @@ -1,3 +1,355 @@ -## Unit of Work +# 工作单元 -待添加 +ABP框架的工作单元(UOW)实现提供了对应用程序中的**数据库连接和事务范围**的抽象和控制. + +一旦一个新的UOW启动,它将创建一个**环境作用域**,当前作用域中执行的**所有数据库操作**都将参与该作用域并将其视为单个事务边界. 操作一起**提交**(成功时)或**回滚**(异常时). + +ABP的UOW系统是; + +* **按约定工作**, 所以大部分情况下你不需要处理UOW. +* **数据库提供者独立**. +* **Web独立**, 这意味着你可以在Web应用程序/服务之外的任何类型的应用程序中创建工作单元作用域. + +## 约定 + +以下方法类型被认为是一个工作单元: + +* ASP.NET Core MVC **Controller Actions**. +* ASP.NET Core Razor **Page Handlers**. +* **应用程序** 方法. +* **仓储方法**. + +UOW自动针对这些方法开始,除非**周围已经有一个(环境)**UOW在运行.示例; + +* 如果你调用一个[仓储]方法(Repositories.md),但还没有启动UOW,它将自动**启动一个新的事务UOW**,其中包括在仓储方法中完成的所有操作,如果仓储方法没有抛出任何异常,则**提交事务**. 仓储方法根本不知道UOW或事务. 它只在一个常规的数据库对象上工作(例如用于[EF Core](Entity-Framework-Core.md)的`DbContext`),而UOW由ABP框架处理. +* 如果调用[应用服务](Application-Services.md)方法,则相同的UOW系统将按上述说明工作. 如果应用服务方法使用某些仓储,这些仓储**不会开始新的UOW**,而是**参与由ABP框架为应用程序服务方法启动的当前工作单元中**. +* ASP.NET Core控制器操作也是如此. 如果操作以控制器action开始,**UOW范围是控制器action的方法主体**. + +所有这些都是由ABP框架自动处理的. + +### 数据库事务行为 + +虽然上一节解释了UOW是数据库事务,但实际上UOW不必是事务性的. 默认情况下; + +* **HTTP GET**请求不会启动事务性UOW. 它们仍然启动UOW,但**不创建数据库事务**. +* 如果底层数据库提供程序支持数据库事务,那么所有其他HTTP请求类型都使用数据库事务启动UOW. + +这是因为HTTP GET请求不会(也不应该)在数据库中进行任何更改. 你可以使用下面解释的选项来更改此行为. + +## 默认选项 + +`AbpUnitOfWorkDefaultOptions` 用于配置工作单元系统的默认选项.在你的[模块](Module-Development-Basics.md)的 `ConfigureServices` 方法中配置选项. + +**示例: 完全禁用数据库事务** + +````csharp +Configure(options => +{ + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; +}); +```` + +### 选项属性 + +* `TransactionBehavior` (`enum`: `UnitOfWorkTransactionBehavior`). 配置事务行为的全局点. 默认值为 `Auto` ,按照上面"*数据库事务行为"*一节的说明工作. 你可以使用此选项启用(甚至对于HTTP GET请求)或禁用事务. +* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值是 `null`** 并使用基础数据库提供程序的默认值. +* `IsolationLevel` (`IsolationLevel?`): 如果UOW是事务性的用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). + +## 控制工作单元 + +在某些情况下你可能希望更改常规事务作用域,创建内部作用域或精细控制事务行为. 下面几节将介绍这些可能性. + +### IUnitOfWorkEnabled 接口 + +这是为不是按照上面解释的约定作为工作单元的类(或类的层次结构)启用UOW的一种简单方法. + +**示例: 为任意服务实现 `IUnitOfWorkEnabled`** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace AbpDemo +{ + public class MyService : ITransientDependency, IUnitOfWorkEnabled + { + public virtual async Task FooAsync() + { + //this is a method with a UOW scope + } + } +} +```` + +然后 `MyService`(和它的派生类)方法都将是UOW. + +但是为了使它工作,**有些规则应该被遵守**; + +* 如果你不是通过接口(如`IMyService`)注入服务,则服务的方法必须是 `virtual` 的(否则[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统将无法工作). +* 仅异步方法(返回`Task`或`Task`的方法)被拦截. 因此同步方法无法启动UOW. + +> 注意,如果 `FooAsync` 在UOW作用域内被调用,那么它已经参与了UOW,不需要 `IUnitOfWorkEnabled` 或其他配置. + +### UnitOfWorkAttribute + +`UnitOfWork` attribute提供了更多的可能性,比如启用或禁用UOW和控制事务行为. + +`UnitOfWork` attribute可以用于**类**或**方法**级别. + +**示例: 为类的特定方法启用UOW** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace AbpDemo +{ + public class MyService : ITransientDependency + { + [UnitOfWork] + public virtual async Task FooAsync() + { + //this is a method with a UOW scope + } + + public virtual async Task BarAsync() + { + //this is a method without UOW + } + } +} +```` + +**示例: 为类的所有方法启用UOW** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace AbpDemo +{ + [UnitOfWork] + public class MyService : ITransientDependency + { + public virtual async Task FooAsync() + { + //this is a method with a UOW scope + } + + public virtual async Task BarAsync() + { + //this is a method with a UOW scope + } + } +} +```` + +**同样的规则**也适用于此: + +* 如果你不是通过接口(如`IMyService`)注入服务,则服务的方法必须是 `virtual` 的(否则[动态代理/拦截](Dynamic-Proxying-Interceptors.md)系统将无法工作). +* 仅异步方法(返回`Task`或`Task`的方法)被拦截. 因此同步方法无法启动UOW. + +#### UnitOfWorkAttribute 属性 + +* `IsTransactional` (`bool?`): 用于设置UOW是否是事务性的. **默认值为 `null`**. 如果你让它为 `null`,它会通过约定和配置自动确定. +* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值为 `null`**并回退到默认配置值. +* `IsolationLevel` (`IsolationLevel?`): 如果UOW是事务的,用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). 如果未设置,则使用默认值. +* `IsDisabled` (`bool`): 用于禁用当前方法/类的UOW. + +> 如果在环境UOW作用域内调用方法,将忽略 `UnitOfWork` 属性,并且该方法参与周围的事务. + +**示例: 为控制器action禁用UOW** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Uow; + +namespace AbpDemo.Web +{ + public class MyController : AbpController + { + [UnitOfWork(IsDisabled = true)] + public virtual async Task FooAsync() + { + //... + } + } +} +```` + +## IUnitOfWorkManager + +`IUnitOfWorkManager` 是用于控制工作单元系统的主要服务. 下面的部分解释了如何使用此服务(大多数时候你并不需要). + +### 开始新的工作单元 + +`IUnitOfWorkManager.Begin` 方法用于创建一个新的UOW作用域. + +**示例: 创建一个新的非事务性UOW作用域** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace AbpDemo +{ + public class MyService : ITransientDependency + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public MyService(IUnitOfWorkManager unitOfWorkManager) + { + _unitOfWorkManager = unitOfWorkManager; + } + + public virtual async Task FooAsync() + { + using (var uow = _unitOfWorkManager.Begin( + requiresNew: true, isTransactional: false + )) + { + //... + + await uow.CompleteAsync(); + } + } + } +} +```` + +`Begin` 方法有以下可选参数: + +* `requiresNew` (`bool`): 设置为 `true` 可忽略周围的工作单元,并使用提供的选项启动新的UOW. **默认值为`false`. 如果为`false`,并且周围有UOW,则 `Begin` 方法实际上不会开始新的UOW,而是以静默方式参与现有的UOW**. +* `isTransactional` (`bool`). 默认为 `false`. +* `isolationLevel` (`IsolationLevel?`): 如果UOW是事务的,用于设置数据库事务的[隔离级别](https://docs.microsoft.com/en-us/dotnet/api/system.data.isolationlevel). 如果未设置,则使用默认值. +* `TimeOut` (`int?`): 用于设置UOW的超时值. **默认值为 `null`**并回退到默认配置值. + +### 当前工作单元 + +如上所述UOW是环境的. 如果需要访问当前的工作单元,可以使用 `IUnitOfWorkManager.Current` 属性. + +**示例: 获取当前UOW** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Uow; + +namespace AbpDemo +{ + public class MyProductService : ITransientDependency + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public MyProductService(IUnitOfWorkManager unitOfWorkManager) + { + _unitOfWorkManager = unitOfWorkManager; + } + + public async Task FooAsync() + { + var uow = _unitOfWorkManager.Current; + //... + } + } +} +```` + +`Current` 属性返回一个 `IUnitOfWork` 对象. + +> 如果没有周围的工作单元,则**当前工作单元可以为`null`**. 如上所述,如果你的类是常规的UOW类,你将其手动设置为UOW或在UOW作用域内调用它,那么该值就不会为 `null`. + +#### SaveChangesAsync + +`IUnitOfWork.SaveChangesAsync()` 方法将到目前为止的所有更改保存到数据库中. 如果你正在使用EF Core,它的行为完全相同. 如果当前UOW是事务性的,即使已保存的更改也可以在错误时回滚(对于支持的数据库提供程序). + +**示例: 插入实体后保存更改以获取其自动增量ID** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace AbpDemo +{ + public class CategoryAppService : ApplicationService, ICategoryAppService + { + private readonly IRepository _categoryRepository; + + public CategoryAppService(IRepository categoryRepository) + { + _categoryRepository = categoryRepository; + } + + public async Task CreateAsync(string name) + { + var category = new Category {Name = name}; + await _categoryRepository.InsertAsync(category); + + //Saving changes to be able to get the auto increment id + await UnitOfWorkManager.Current.SaveChangesAsync(); + + return category.Id; + } + } +} +```` + +示例的 `Category` [实体](Entities.md)使用自动递增的 `int` 主键. 自动增量PK需要将实体保存到数据库中来获得新实体的ID. + +示例是从基类 `ApplicationService` 派生的[应用服务](Application-Services.md), `IUnitOfWorkManager` 服务已经作为 `UnitOfWorkManager` 属性注入,所以无需手动注入. + +获取当前UOW非常常见,所以还有一个 `UnitOfWorkManager.Current` 的快捷属性 `CurrentUnitOfWork`. 所以可以对上面的例子进行以下更改: + +````csharp +await CurrentUnitOfWork.SaveChangesAsync(); +```` + +##### SaveChanges() 的替代方法 + +由于经常需要在插入,更新或删除实体后保存更改,相应的[仓储](Repositories.md)方法有一个可选的 `autoSave` 参数. 可以将上面的 `CreateAsync` 方法按如下重写: + +````csharp +public async Task CreateAsync(string name) +{ + var category = new Category {Name = name}; + await _categoryRepository.InsertAsync(category, autoSave: true); + return category.Id; +} +```` + +如果你的目的只是在创建/更新/删除实体后保存更改,建议你使用 `autoSave` 选项,而不是手动使用 `CurrentUnitOfWork.SaveChangesAsync()`. + +> **Note-1**: 当工作单元结束而没有任何错误时,所有更改都会自动保存. 所以除非确实需要,否则不要调用 `SaveChangesAsync()`. +> +> **Note-2**: 如果你使用 `Guid` 作为主键,则无需插入时保存来获取生成的id,因为 `Guid` 主键是在应用程序中设置的,创建新实体后立即可用. + +#### IUnitOfWork 其他属性/方法 + +* `OnCompleted` 方法获得一个回调动作,当工作单元成功完成时调用(在这里你可以确保所有更改都保存了). +* `Failed` 和 `Disposed` 事件可以用于UOW失败和被销毁的通知. +* `Complete` 和 `Rollback` 方法用于完成(提交)或回滚当前 UOW, 通常ABP框架在内部使用,如果你使用 `IUnitOfWorkManager.Begin` 方法手动启动事务,那么你可以手动使用这些方法. +* `Options` 可用于获取启动UOW时使用的选项. +* `Items` 字典可用于在同一工作单元内存储和获取任意对象,可以实现自定义逻辑. + +## ASP.NET Core 集成 + +工作单元系统已完全集成到ASP.NET Core. 它为UOW系统定义了动作过滤器和页面过滤器. 当你使用ASP.NET Core MVC控制器或Razor页面时,它可以正常工作. + +> 使用ASP.NET Core时,通常你不需要做任何操作配置UOW. + +### 工作单元中间件 + +`AbpUnitOfWorkMiddleware` 是可以在ASP.NET Core请求管道中启用UOW的中间件. 如果你需要扩大UOW范围以涵盖其他一些中间件,可以这样做. + +**示例:** + +````csharp +app.UseUnitOfWork(); +app.UseConfiguredEndpoints(); +```` \ No newline at end of file diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 07c27016e1..c14355497b 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -287,7 +287,8 @@ "path": "Data-Transfer-Objects.md" }, { - "text": "工作单元" + "text": "工作单元", + "path": "Unit-Of-Work.md" } ] } From 54c8283a96e15d79ef5242ac4bb4a3ea5342fa50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ak=C4=B1n=20Sabri=20=C3=87am?= Date: Tue, 16 Jun 2020 21:23:20 +0300 Subject: [PATCH 017/135] added localization keys for abp io admin invoice page --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 4d81ba9058..9e5afc3b33 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -76,6 +76,7 @@ "Organizations": "Organizations", "LongName": "Long name", "LicenseType": "License type", + "MissingLicenseTypeField": "The license type field is required!", "LicenseStartTime": "License start time", "LicenseEndTime": "License end time", "AllowedDeveloperCount": "Allowed developer count", @@ -121,6 +122,7 @@ "TaxNumber": "Tax Number", "InvoiceNumber": "Invoice Number", "InvoiceDate": "Invoice Date", + "InvoiceNote": "Invoice Note", "Quantity": "Quantity", "AddProduct": "Add Product", "AddProductWarning": "You need to add product!", From 1fbb20d655ac42f1b8fd60a95c483891e4086609 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Wed, 17 Jun 2020 09:58:30 +0300 Subject: [PATCH 018/135] Fix: Feature name Localizations are wrong in feature management modal resolves https://github.com/abpframework/abp/issues/4370 --- .../Pages/FeatureManagement/FeatureManagementModal.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml index 221b46bea6..ec8b4b1706 100644 --- a/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml +++ b/modules/feature-management/src/Volo.Abp.FeatureManagement.Web/Pages/FeatureManagement/FeatureManagementModal.cshtml @@ -22,7 +22,7 @@ var feature = Model.FeatureListDto.Features[i];
- @feature.Name + @feature.DisplayName @if (feature.ValueType is FreeTextStringValueType) From f4cb8f41461cd37449aae662d9ff065a62e57c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 13:21:53 +0300 Subject: [PATCH 019/135] Added IQueryable & Async Operations section to the repository document --- docs/en/Repositories.md | 130 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 6a60c58278..96f160a5d1 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -38,6 +38,8 @@ public class PersonAppService : ApplicationService } ```` +> See the "*IQueryable & Async Operations*" section below to understand how you can use **async extension methods**, like `ToListAsync()` (which is strongly suggested) instead of `ToList()`. + In this example; * `PersonAppService` simply injects `IRepository` in it's constructor. @@ -116,5 +118,131 @@ public class PersonRepository : EfCoreRepository, IPe } ```` -You can directly access the data access provider (`DbContext` in this case) to perform operations. See [entity framework integration document](Entity-Framework-Core.md) for more about custom repositories based on EF Core. +You can directly access the data access provider (`DbContext` in this case) to perform operations. + +> See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories. + +## IQueryable & Async Operations + +`IRepository` inherits from `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Generic Repositories*" section above. + +**Example: Using the `Where(...)` &and the`ToList()` extension methods** + +````csharp +var people = _personRepository + .Where(p => p.Name.Contains(nameFilter)) + .ToList(); +```` + +`.ToList`, `Count()`... are standard extension methods defined in the `System.Linq` namespace ([see all](https://docs.microsoft.com/en-us/dotnet/api/system.linq.queryable)). + +You normally want to use `.ToListAsync()`, `.CountAsync()`... instead, to be able to write a **truly async code**. + +However, you see that can't use these async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because; + +* These async methods **are not standard LINQ methods** and they are defined in the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) NuGet package. +* The standard template **doesn't have a reference** to the EF Core package from the domain and application layers, to be independent from the database provider. + +Based on your requirements and development model, you have the following options to be able to use the async methods. + +> Using async methods is strongly suggested! Don't use sync LINQ methods while executing database queries to be able to develop a scalable application. + +### Option-1: Reference to the EF Core + +The easiest solution is to directly add the EF Core package from the project you want to use these async methods. + +> Add the [Volo.Abp.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore) NuGet package to your project, which indirectly reference to the EF Core package. This ensures that you use the correct version of the EF Core compatible to the rest of your application. + +When you add the NuGet package to your project, you can take full power of the EF Core extension methods. + +**Example: Directly using the `ToListAsync()` after adding the EF Core package** + +````csharp +var people = _personRepository + .Where(p => p.Name.Contains(nameFilter)) + .ToListAsync(); +```` + +This method is suggested; + +* If you are developing an application and you **don't plan to change** EF Core in the future, or you can **tolerate** it if you need to change later. We believe that's reasonable if you are developing a final application. + +#### MongoDB Case + +If you are using [MongoDB](MongoDB.md), you need to add the [Volo.Abp.MongoDB](https://www.nuget.org/packages/Volo.Abp.MongoDB) NuGet package to your project. Even in this case, you can't directly use async LINQ extensions (like `ToListAsync`) because MongoDB doesn't provide async extension methods for `IQueryable`, but provides for `IMongoQueryable`. You need to cast the query to `IMongoQueryable` first to be able to use the async extension methods. + +**Example: Cast `IQueryable` to `IMongoQueryable` and use `ToListAsync()`** + +````csharp +var people = ((IMongoQueryable)_personRepository + .Where(p => p.Name.Contains(nameFilter))) + .ToListAsync(); +```` + +### Option-2: Custom Repository Methods + +You can always create custom repository methods and use the database provider specific APIs, like async extension methods here. See [EF Core](Entity-Framework-Core.md) or [MongoDb](MongoDB.md) document for more info about the custom repositories. + +This method is suggested; + +* If you want to **completely isolate** your domain & application layers completely from the database provider. +* If you develop a **reusable [application module](Modules/Index.md)** and don't want to depend on a specific database provider. + +### Option-3: IAsyncQueryableExecuter + +> Notice that this feature is available in the ABP Framework 3.0 and later. While it is also usable with older versions, it was providing a very limited set of methods. + +`IAsyncQueryableExecuter` is a service that is used to execute an `IQueryable` object asynchronously **without depending on the actual database provider**. + +**Example: Inject & use the `IAsyncQueryableExecuter.ToListAsync()` method** + +````csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Linq; + +namespace AbpDemo +{ + public class ProductAppService : ApplicationService, IProductAppService + { + private readonly IRepository _productRepository; + private readonly IAsyncQueryableExecuter _asyncExecuter; + + public ProductAppService( + IRepository productRepository, + IAsyncQueryableExecuter asyncExecuter) + { + _productRepository = productRepository; + _asyncExecuter = asyncExecuter; + } + + public async Task> GetListAsync(string name) + { + //Create the query + var query = _productRepository + .Where(p => p.Name.Contains(name)) + .OrderBy(p => p.Name); + + //Run the query asynchronously + List products = await _asyncExecuter.ToListAsync(query); + + //... + } + } +} +```` + +> `ApplicationService` and `DomainService` base classes already have `AsyncExecuter` properties pre-injected and usable without needing an explicit constructor injection. + +ABP Framework executes the query asynchronously using the actual database provider's API. While that is not a usual way to execute a query, it is the best way to use the async API without depending on the database provider. + +This method is suggested; + +* If you are building a **reusable library** that doesn't have a database provider integration package, but needs to execute an `IQueryable` object in some case. +For example, ABP Framework uses the `IAsyncQueryableExecuter` in the `CrudAppService` base class (see the [application services](Application-Services.md) document). \ No newline at end of file From 6e501dfabc2e8c121674f49a44914e25fa0858cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 13:45:29 +0300 Subject: [PATCH 020/135] Update Repositories.md --- docs/en/Repositories.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 96f160a5d1..86a3e231c5 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -126,7 +126,7 @@ You can directly access the data access provider (`DbContext` in this case) to p `IRepository` inherits from `IQueryable`, that means you can **directly use LINQ extension methods** on it, as shown in the example of the "*Generic Repositories*" section above. -**Example: Using the `Where(...)` &and the`ToList()` extension methods** +**Example: Using the `Where(...)` &and the `ToList()` extension methods** ````csharp var people = _personRepository @@ -138,7 +138,7 @@ var people = _personRepository You normally want to use `.ToListAsync()`, `.CountAsync()`... instead, to be able to write a **truly async code**. -However, you see that can't use these async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because; +However, you see that you can't use these async extension methods in your application or domain layer when you create a new project using the standard [application startup template](Startup-Templates/Application.md), because; * These async methods **are not standard LINQ methods** and they are defined in the [Microsoft.EntityFrameworkCore](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore) NuGet package. * The standard template **doesn't have a reference** to the EF Core package from the domain and application layers, to be independent from the database provider. @@ -185,8 +185,8 @@ You can always create custom repository methods and use the database provider sp This method is suggested; -* If you want to **completely isolate** your domain & application layers completely from the database provider. -* If you develop a **reusable [application module](Modules/Index.md)** and don't want to depend on a specific database provider. +* If you want to **completely isolate** your domain & application layers from the database provider. +* If you develop a **reusable [application module](Modules/Index.md)** and don't want to force to a specific database provider, which should be done as a [best practice](Best-Practices/Index.md). ### Option-3: IAsyncQueryableExecuter From cdd42c9978af59d779a67e0f31f754cebc453b54 Mon Sep 17 00:00:00 2001 From: Alper Ebicoglu Date: Wed, 17 Jun 2020 14:01:36 +0300 Subject: [PATCH 021/135] recreated initial migrations due to OrganizationUnits change --- ...nit.Designer.cs => 20200617102723_Initial.Designer.cs} | 8 +++++--- .../{20200525053411_init.cs => 20200617102723_Initial.cs} | 2 +- .../Migrations/VoloDocsDbContextModelSnapshot.cs | 4 +++- .../VoloDocs.EntityFrameworkCore.csproj | 5 +++++ modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj | 4 ---- 5 files changed, 14 insertions(+), 9 deletions(-) rename modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/{20200525053411_init.Designer.cs => 20200617102723_Initial.Designer.cs} (99%) rename modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/{20200525053411_init.cs => 20200617102723_Initial.cs} (99%) diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200525053411_init.Designer.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200617102723_Initial.Designer.cs similarity index 99% rename from modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200525053411_init.Designer.cs rename to modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200617102723_Initial.Designer.cs index 2de428f388..5fbced9e4a 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200525053411_init.Designer.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200617102723_Initial.Designer.cs @@ -5,19 +5,21 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; using VoloDocs.EntityFrameworkCore; namespace VoloDocs.EntityFrameworkCore.Migrations { [DbContext(typeof(VoloDocsDbContext))] - [Migration("20200525053411_init")] - partial class init + [Migration("20200617102723_Initial")] + partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.2") + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) + .HasAnnotation("ProductVersion", "3.1.5") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200525053411_init.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200617102723_Initial.cs similarity index 99% rename from modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200525053411_init.cs rename to modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200617102723_Initial.cs index 00854e6745..179387743c 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200525053411_init.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/20200617102723_Initial.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace VoloDocs.EntityFrameworkCore.Migrations { - public partial class init : Migration + public partial class Initial : Migration { protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs index 216a9a06f4..43feaed625 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/Migrations/VoloDocsDbContextModelSnapshot.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Volo.Abp.EntityFrameworkCore; using VoloDocs.EntityFrameworkCore; namespace VoloDocs.EntityFrameworkCore.Migrations @@ -15,7 +16,8 @@ namespace VoloDocs.EntityFrameworkCore.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.2") + .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) + .HasAnnotation("ProductVersion", "3.1.5") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj b/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj index b2b54a234f..2f8204e44e 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj @@ -15,4 +15,9 @@ + + + + + diff --git a/modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj b/modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj index 3fb341be49..9da16c65cf 100644 --- a/modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj +++ b/modules/docs/app/VoloDocs.Web/VoloDocs.Web.csproj @@ -20,10 +20,6 @@ - - all - runtime; build; native; contentfiles; analyzers - From 11333312d2834b655098f47094c8a0f7e7a62ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ebubekir=20din=C3=A7?= Date: Wed, 17 Jun 2020 14:18:04 +0300 Subject: [PATCH 022/135] Resource Localization for base and account --- .../Account/Localization/Resources/tr.json | 8 +++++++ .../Base/Localization/Resources/tr.json | 21 +++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json index 5af6a13c50..9c36cd611b 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json @@ -1,5 +1,13 @@ { "culture": "tr", "texts": { + "Account": "ABP Hesabı - Giriş Yap & Kayıt Ol | ABP.IO", + "Welcome": "Hoş Geldiniz", + "UseOneOfTheFollowingLinksToContinue": "Devam etmek için linkleri kullanabilirsiniz", + "FrameworkHomePage": "Framework ana sayfa", + "FrameworkDocumentation": "Framework Dokümantasyon", + "OfficialBlog": "Resmi blog", + "CommercialHomePage": "Kurumsal ana sayfa", + "CommercialSupportWebSite": "Kurumsal destek web sitesi" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json index 3d324c16c0..c5f0021489 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json @@ -8,7 +8,24 @@ "Volo.AbpIo.Domain:010008": "Maksimum izin verilen kullanıcı sayısı mevcut kullanıcı sayısından az olamaz!", "Volo.AbpIo.Domain:010009": "Maksimum izin verilen kullanıcı sayısı sıfırdan az olamaz!", "Volo.AbpIo.Domain:010010": "Maksimum mac adresi sayısı geçildi!", - "Volo.AbpIo.Domain:010011": "Bireysel lisans birden fazla geliştiriciye sahip olamaz!", - "Volo.AbpIo.Domain:010012": "Lisans, lisans süresi bittikten bir ay sonra uzatılamaz!", + "Volo.AbpIo.Domain:010011": "Bireysel lisans birden fazla geliştiriciye sahip olamaz!", + "Volo.AbpIo.Domain:010012": "Lisans, lisans süresi bittikten bir ay sonra uzatılamaz!", + "Volo.AbpIo.Domain:020001": "Bu NPM Paketi silinemiyor çünkü \"{NugetPackages}\" Nuget Paketleri bu pakete bağımlı.", + "Volo.AbpIo.Domain:020002": "Bu NPM Paketi silinemiyor çünkü \"{Modules}\" Modülleri bu paketi kullanıyor.", + "Volo.AbpIo.Domain:020003": "Bu NPM Paketi silinemiyor çünkü \"{Modules}\" Modülleri bu paketi kullanıyor ve \"{NugetPackages}\" Nuget Paketleri bu pakete bağımlı.", + "Volo.AbpIo.Domain:020004": "Bu Nuget Paketi silinemiyor çünkü \"{Modules}\" Modülleri bu paketi kullanıyor.", + "WantToLearn?": "Öğrenmek ister misin?", + "ReadyToGetStarted?": "Başlamaya hazır mısın?", + "JoinOurCommunity": "Topluluğumuza katılın", + "GetStartedUpper": " BAŞLAYIN", + "ForkMeOnGitHub": "Fork me on GitHub", + "Features": "Özellikler", + "GetStarted": "Başlayın", + "Documents": "Dokümanlar", + "Community": "Topluluk", + "ContributionGuide": "Katkı Rehberi", + "Blog": "Blog", + "Commercial": "Ticari", + "SeeDocuments": "Dokümanlara Göz Atın" } } \ No newline at end of file From 533d99c51ee58866ab73087a0962685f15bd2a4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 15:31:08 +0300 Subject: [PATCH 023/135] Complete the event bus nav items --- docs/en/docs-nav.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index c49b053ce0..6c3b3a94de 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -153,16 +153,23 @@ ] }, { - "text": "Events", + "text": "Event Bus", "items": [ { - "text": "Event Bus (local)" + "text": "Overall", + "path": "Event-Bus.md" + }, + { + "text": "Local Event Bus", + "path": "Local-Event-Bus.md" }, { "text": "Distributed Event Bus", + "path": "Distributed-Event-Bus.md", "items": [ { - "text": "RabbitMQ Integration" + "text": "RabbitMQ Integration", + "path": "Distributed-Event-Bus-RabbitMQ-Integration.md" } ] } From b614033568165204a4580d633d219b26f0d98019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 15:52:58 +0300 Subject: [PATCH 024/135] Event Bus Overall document --- docs/en/Event-Bus.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/en/Event-Bus.md b/docs/en/Event-Bus.md index cf2b57c018..a0e6859465 100644 --- a/docs/en/Event-Bus.md +++ b/docs/en/Event-Bus.md @@ -1,3 +1,10 @@ # Event Bus -TODO \ No newline at end of file +An event bus is a mediator that transfers a message from a sender to a receiver. In this way, it provides a loosely coupled communication way between objects, services and applications. + +## Event Bus Types + +ABP Framework provides two type of event buses; + +* **[Local Event Bus](Local-Event-Bus.md)** is suitable for in-process messaging. +* **[Distributed Event Bus](Distributed-Event-Bus.md)** is suitable for inter-process messaging, like microservices publishing and subscribing to distributed events. \ No newline at end of file From c728d70f5ea09431194880468865022c99d60c16 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Wed, 17 Jun 2020 16:17:06 +0300 Subject: [PATCH 025/135] Update tr.json --- .../AbpIoLocalization/Base/Localization/Resources/tr.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json index c5f0021489..767346cd5e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/tr.json @@ -17,15 +17,13 @@ "WantToLearn?": "Öğrenmek ister misin?", "ReadyToGetStarted?": "Başlamaya hazır mısın?", "JoinOurCommunity": "Topluluğumuza katılın", - "GetStartedUpper": " BAŞLAYIN", - "ForkMeOnGitHub": "Fork me on GitHub", + "GetStartedUpper": "BAŞLAYIN", "Features": "Özellikler", "GetStarted": "Başlayın", "Documents": "Dokümanlar", "Community": "Topluluk", "ContributionGuide": "Katkı Rehberi", "Blog": "Blog", - "Commercial": "Ticari", "SeeDocuments": "Dokümanlara Göz Atın" } -} \ No newline at end of file +} From 9b9984c5d6376c0eb1e529a747418387d85dc3c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 16:42:37 +0300 Subject: [PATCH 026/135] Update Local-Event-Bus.md --- docs/en/Local-Event-Bus.md | 164 +++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/docs/en/Local-Event-Bus.md b/docs/en/Local-Event-Bus.md index 859ec8d28e..62b12c8457 100644 --- a/docs/en/Local-Event-Bus.md +++ b/docs/en/Local-Event-Bus.md @@ -1,3 +1,167 @@ # Local Event Bus +The Local Event Bus allows services to publish and subscribe to **in-process events**. That means it is suitable if two services (publisher and subscriber) are running in the same process. + +## Publishing Events + +There are two ways of publishing local events explained in the following sections. + +### ILocalEventBus + +`ILocalEventBus` can be [injected](Dependency-Injection.md) and used to publish a local event. + +**Example: Publish an event when the stock count of a product changes** + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Local; + +namespace AbpDemo +{ + public class MyService : ITransientDependency + { + private readonly ILocalEventBus _localEventBus; + + public MyService(ILocalEventBus localEventBus) + { + _localEventBus = localEventBus; + } + + public virtual async Task ChangeStockCountAsync(Guid productId, int newCount) + { + //TODO: IMPLEMENT YOUR LOGIC... + + //PUBLISH THE EVENT + await _localEventBus.PublishAsync( + new StockCountChangedEvent + { + ProductId = productId, + NewCount = newCount + } + ); + } + } +} +```` + +`PublishAsync` method gets a single parameter: the event object, which is responsible to hold the data related to the event. It is a simple plain class: + +````csharp +using System; + +namespace AbpDemo +{ + public class StockCountChangedEvent + { + public Guid ProductId { get; set; } + + public int NewCount { get; set; } + } +} +```` + +Even if you don't need to transfer any data, you need to create a class (which is an empty class in this case). + +### Inside Entity / Aggregate Root Classes + +[Entities](Entities.md) can not inject services via dependency injection, but it is very common to publish events inside entity / aggregate root classes. + +**Example: Publish an event inside an aggregate root method** + +````csharp +using System; +using Volo.Abp.Domain.Entities; + +namespace AbpDemo +{ + public class Product : AggregateRoot + { + public string Name { get; set; } + + public int StockCount { get; private set; } + + private Product() { } + + public Product(Guid id, string name) + : base(id) + { + Name = name; + } + + public void ChangeStockCount(int newCount) + { + StockCount = newCount; + + //ADD an EVENT TO BE PUBLISHED + AddLocalEvent( + new StockCountChangedEvent + { + ProductId = Id, + NewCount = newCount + } + ); + } + } +} +```` + +`AggregateRoot` class defines the `AddLocalEvent` to add a new local event, that is published when the aggregate root object is saved (created or updated) into the database. + +> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` which guarantees publishing the event. + +#### IGeneratesDomainEvents Interface + +Actually, adding local events are not unique to the `AggregateRoot` class. You can implement `IGeneratesDomainEvents` for any entity class. But, `AggregateRoot` implements it by default and makes it easy for you. + +> It is not suggested to implement this interface for entities those are not aggregate roots, since it may not work for some database providers for such entities. It works for EF Core, but not works for MongoDB for example. + +#### How It Was Implemented? + +Calling the `AddLocalEvent` doesn't immediately publish the event. The event is published when you save changes to the database; + +* For EF Core, it is published on `DbContext.SaveChanges`. +* For MongoDB, it is published when you call repository's `InsertAsync` or `UpdateAsync` methods (since MongoDB has no such a change tracking system). + +## Subscribing to Events + +A service can implement the `ILocalEventHandler` to handle the event. + +**Example: Handle the `StockCountChangedEvent` defined above** + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus; + +namespace AbpDemo +{ + public class MyHandler + : ILocalEventHandler, + ITransientDependency + { + public async Task HandleEventAsync(StockCountChangedEvent eventData) + { + //TODO: your code that does somthing on the event + } + } +} +```` + +That's all. `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEvent` occurs. You can inject any service and perform any required logic here. + +There can be zero or more handlers subscribed to the same event. + +> The handler class must be registered to the dependency injection (DI). The sample above uses the `ITransientDependency` to accomplish it. See the [DI document](Dependency-Injection.md) for more options. + +## Transaction & Exception Behavior + +When an event published, subscribed event handlers are immediately executed. So; + +* If a handler **throws an exception**, it effects the code that published the event. That means it gets the exception on the `PublishAsync` call. So, **use try-catch yourself** in the event handler if you want to hide the error. +* If the event publishing code is being executed inside a [Unit Of Work](Unit-Of-Work.md) scope, the event handlers also covered by the unit of work. That means if your UOW is transactional and a handler throws an exception, the transaction is rolled back. + +## Pre-Built Events + TODO \ No newline at end of file From 47eb121d00f1b6df0af753205fb2f93140f0efa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 17:28:27 +0300 Subject: [PATCH 027/135] Complete the local event bus document. --- docs/en/Local-Event-Bus.md | 66 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/docs/en/Local-Event-Bus.md b/docs/en/Local-Event-Bus.md index 62b12c8457..3aa9fd7d07 100644 --- a/docs/en/Local-Event-Bus.md +++ b/docs/en/Local-Event-Bus.md @@ -122,7 +122,7 @@ Actually, adding local events are not unique to the `AggregateRoot` class. You c Calling the `AddLocalEvent` doesn't immediately publish the event. The event is published when you save changes to the database; * For EF Core, it is published on `DbContext.SaveChanges`. -* For MongoDB, it is published when you call repository's `InsertAsync` or `UpdateAsync` methods (since MongoDB has no such a change tracking system). +* For MongoDB, it is published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system). ## Subscribing to Events @@ -151,7 +151,8 @@ namespace AbpDemo That's all. `MyHandler` is **automatically discovered** by the ABP Framework and `HandleEventAsync` is called whenever a `StockCountChangedEvent` occurs. You can inject any service and perform any required logic here. -There can be zero or more handlers subscribed to the same event. +* **Zero or more handlers** can subscribe to the same event. +* A single event handler class can **subscribe to multiple events** but implementing the `ILocalEventHandler` interface for each event type. > The handler class must be registered to the dependency injection (DI). The sample above uses the `ITransientDependency` to accomplish it. See the [DI document](Dependency-Injection.md) for more options. @@ -164,4 +165,63 @@ When an event published, subscribed event handlers are immediately executed. So; ## Pre-Built Events -TODO \ No newline at end of file +It is very common to **publish events on entity create, update and delete** operations. ABP Framework **automatically** publish these events for all entities. You can just subscribe to the related event. + +**Example: Subscribe to an event that published when a user was created** + +````csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events; +using Volo.Abp.EventBus; + +namespace AbpDemo +{ + public class MyHandler + : ILocalEventHandler>, + ITransientDependency + { + public async Task HandleEventAsync( + EntityCreatedEventData eventData) + { + var userName = eventData.Entity.UserName; + var email = eventData.Entity.Email; + //... + } + } +} +```` + +This class subscribes to the `EntityCreatedEventData`, which is published just after a user was created. You may want to send a "Welcome" email to the new user. + +There are two types of these events: events with past tense and events with continuous tense. + +### Events with Past Tense + +Events with past tense are published when the related unit of work completed and the entity change successfully saved to the database. If you throw an exception on these event handlers, it **can not rollback** the transaction since it was already committed. + +The event types are; + +* `EntityCreatedEventData` is published just after an entity was successfully created. +* `EntityUpdatedEventData` is published just after an entity was successfully updated. +* `EntityDeletedEventData` is published just after an entity was successfully deleted. +* `EntityChangedEventData` is published just after an entity was successfully created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events. + +### Events with Continuous Tense + +Events with continuous tense are published before completing the transaction (if database transaction is supported by the database provider being used). If you throw an exception on these event handlers, it **can rollback** the transaction since it is not completed yet and the change is not saved to the database. + +The event types are; + +* `EntityCreatingEventData` is published just before saving a new entity to the database. +* `EntityUpdatingEventData` is published just before an existing entity is being updated. +* `EntityDeletingEventData` is published just before an entity is being deleted. +* `EntityChangingEventData` is published just before an entity is being created, updated or deleted. It can be a shortcut if you need to listen any type of change - instead of subscribing to the individual events. + +#### How It Was Implemented? + +Pre-build events are published when you save changes to the database; + +* For EF Core, they are published on `DbContext.SaveChanges`. +* For MongoDB, they are published when you call repository's `InsertAsync`, `UpdateAsync` or `DeleteAsync` methods (since MongoDB has not a change tracking system). \ No newline at end of file From 7f1c5e4ed0e6bac581b44a8862e02643b1249f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 17:30:52 +0300 Subject: [PATCH 028/135] Update Local-Event-Bus.md --- docs/en/Local-Event-Bus.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/Local-Event-Bus.md b/docs/en/Local-Event-Bus.md index 3aa9fd7d07..4452149f56 100644 --- a/docs/en/Local-Event-Bus.md +++ b/docs/en/Local-Event-Bus.md @@ -109,7 +109,7 @@ namespace AbpDemo `AggregateRoot` class defines the `AddLocalEvent` to add a new local event, that is published when the aggregate root object is saved (created or updated) into the database. -> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` which guarantees publishing the event. +> If an entity publishes such an event, it is a good practice to change the related properties in a controlled manner, just like the example above - `StockCount` can only be changed by the `ChangeStockCount` method which guarantees publishing the event. #### IGeneratesDomainEvents Interface From fbd888a787520555d3192ea2cb8cf6b17e3666e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 17 Jun 2020 18:01:18 +0300 Subject: [PATCH 029/135] Update Distributed-Event-Bus.md --- docs/en/Distributed-Event-Bus.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/en/Distributed-Event-Bus.md b/docs/en/Distributed-Event-Bus.md index 03beb814d2..2120ac45a3 100644 --- a/docs/en/Distributed-Event-Bus.md +++ b/docs/en/Distributed-Event-Bus.md @@ -1,3 +1,14 @@ # Distributed Event Bus +Distributed Event bus system allows to publish and subscribe to events that can be transferred across application/service boundaries. You can use the distributed event bus to asynchronously send and receive message between microservices or applications. + +## Providers + +Distributed event bus system provides an abstraction that can be implemented by any vendor/provider. There are two providers implemented out of the box: + +* `LocalDistributedEventBus` is the default implementation that implements the distributed event bus to work as in-process. Yes! The **default implementation works just like the [local event bus](Local-Event-Bus.md)**, if you don't configure a real distributed provider. +* `RabbitMqDistributedEventBus` implements the distributed event bus with the [RabbitMQ](https://www.rabbitmq.com/). See the [RabbitMQ integration document](Distributed-Event-Bus-RabbitMQ-Integration.md) to learn how to configure it. + +## Publishing Events + TODO \ No newline at end of file From c72bcf48f44f2a1198b1284e0eb8623b62504f54 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:40:34 +0300 Subject: [PATCH 030/135] feat: add circular reference to parents of nodes --- .../core/src/lib/tests/tree-utils.spec.ts | 31 ++++++++++++------- .../packages/core/src/lib/utils/tree-utils.ts | 12 ++++--- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts index 454c1c6891..6115114872 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/tree-utils.spec.ts @@ -1,4 +1,4 @@ -import { createTreeFromList, TreeNode } from '../utils/tree-utils'; +import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils'; const LIST_1 = [ { id: 1, pid: null }, @@ -46,23 +46,32 @@ describe('Tree Utils', () => { ${LIST_2} | ${TREE_2} ${LIST_3} | ${TREE_3} `('should return $expected when given $list', ({ list, expected }: TestCreateTreeFromList) => { - expect( - createTreeFromList( - list, - x => x.id, - x => x.pid, - ), - ).toEqual(expected); + const tree = createTreeFromList( + list, + x => x.id, + x => x.pid, + x => BaseTreeNode.create(x), + ); + + expect(removeParents(tree)).toEqual(expected); }); }); }); +function removeParents(tree: TreeNode[]) { + return tree.map(v => { + const { parent, ...node } = v; + node.children = removeParents(node.children); + return node; + }); +} + interface TestCreateTreeFromList { - list: ModelA[]; - expected: TreeNode[]; + list: Model[]; + expected: TreeNode[]; } -interface ModelA { +interface Model { id: 1; pid: null; } diff --git a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts index 607a30ec52..d4c0ba0ab3 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/tree-utils.ts @@ -1,19 +1,20 @@ -export class TreeNodeFactory { +export class BaseTreeNode { children: TreeNode[] = []; isLeaf = true; + parent: TreeNode; constructor(props: T) { Object.assign(this, props); } static create(props: T) { - return new TreeNodeFactory(props) as TreeNode; + return new BaseTreeNode(props) as TreeNode; } } export function createTreeFromList( list: T[], - keySelector: (item: T) => number | string | Symbol, + keySelector: (item: T) => number | string | symbol, parentKeySelector: typeof keySelector, valueMapper: (item: T) => R, ) { @@ -27,8 +28,10 @@ export function createTreeFromList( if (parentId) { const parent = map.get(parentId); + if (!parent) return; (parent as any).children.push(node); (parent as any).isLeaf = false; + (node as any).parent = parent; } else { tree.push(node); } @@ -39,7 +42,7 @@ export function createTreeFromList( export function createMapFromList( list: T[], - keySelector: (item: T) => number | string | Symbol, + keySelector: (item: T) => number | string | symbol, valueMapper: (item: T) => R, ) { type Key = ReturnType; @@ -54,6 +57,7 @@ export type TreeNode = { } & { children: TreeNode[]; isLeaf: boolean; + parent?: TreeNode; }; type NodeValue any> = F extends undefined From 6478f57317473b381f8909834314e2f5c29aa2be Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:42:15 +0300 Subject: [PATCH 031/135] feat: add node, nav, and tab types --- .../packages/core/src/lib/models/common.ts | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts index 773233857f..ee160fef6d 100644 --- a/npm/ng-packs/packages/core/src/lib/models/common.ts +++ b/npm/ng-packs/packages/core/src/lib/models/common.ts @@ -1,4 +1,4 @@ -import { EventEmitter } from '@angular/core'; +import { EventEmitter, Type } from '@angular/core'; import { Router } from '@angular/router'; import { Subject } from 'rxjs'; import { eLayoutType } from '../enums/common'; @@ -35,21 +35,25 @@ export namespace ABP { maxResultCount?: number; } - export interface Route { - children?: Route[]; - invisible?: boolean; - layout?: eLayoutType; + export interface Node { name: string; - order?: number; parentName?: string; + order?: number; + invisible?: boolean; + } + + export interface Nav extends Node { path: string; requiredPolicy?: string; + } + + export interface Route extends Nav { + layout?: eLayoutType; iconClass?: string; } - export interface FullRoute extends Route { - url?: string; - wrapper?: boolean; + export interface Tab extends Nav { + component: Type; } export interface BasicItem { From 41f1f2740e074ff840679431d7f58653a97d4aee Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:43:30 +0300 Subject: [PATCH 032/135] feat: add utility function for getting a route path --- .../core/src/lib/tests/route-utils.spec.ts | 60 +++++++++++++ .../core/src/lib/utils/route-utils.ts | 87 ++----------------- 2 files changed, 65 insertions(+), 82 deletions(-) create mode 100644 npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts new file mode 100644 index 0000000000..9c5f81ac2a --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/route-utils.spec.ts @@ -0,0 +1,60 @@ +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { RouterOutletComponent } from '../components'; +import { getRoutePath } from '../utils/route-utils'; + +// tslint:disable-next-line +@Component({ template: '' }) +class DummyComponent {} + +describe('Route Utils', () => { + describe('#getRoutePath', () => { + let spectator: SpectatorRouting; + const createRouting = createRoutingFactory({ + component: RouterOutletComponent, + stubsEnabled: false, + declarations: [DummyComponent], + imports: [RouterModule], + routes: [ + { + path: '', + children: [ + { + path: 'foo', + children: [ + { + path: 'bar', + children: [ + { + path: 'baz', + component: DummyComponent, + }, + ], + }, + ], + }, + ], + }, + ], + }); + + beforeEach(async () => { + spectator = createRouting(); + }); + + test.each` + url | expected + ${''} | ${'/'} + ${'/'} | ${'/'} + ${'/foo'} | ${'/foo'} + ${'/foo/bar'} | ${'/foo/bar'} + ${'/foo/bar/baz'} | ${'/foo/bar/baz'} + ${'/foo?bar=baz'} | ${'/foo'} + ${'/foo#bar'} | ${'/foo'} + `('should return $expected when url is $url', async ({ url, expected }) => { + await spectator.router.navigateByUrl(url); + expect(getRoutePath(spectator.router)).toBe(expected); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts b/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts index 51abe4218b..7ef062986a 100644 --- a/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts +++ b/npm/ng-packs/packages/core/src/lib/utils/route-utils.ts @@ -1,85 +1,8 @@ -import { ABP } from '../models/common'; +import { PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router'; -export function organizeRoutes( - routes: ABP.FullRoute[], - wrappers: ABP.FullRoute[] = [], - parentNameArr = [] as ABP.FullRoute[], - parentName: string = null, -): ABP.FullRoute[] { - const filter = route => { - if (route.children && route.children.length) { - route.children = organizeRoutes(route.children, wrappers, parentNameArr, route.name); - } +export function getRoutePath(router: Router) { + const emptyGroup = { segments: [] } as UrlSegmentGroup; + const primaryGroup = router.parseUrl(router.url).root.children[PRIMARY_OUTLET]; - if (route.parentName && route.parentName !== parentName) { - parentNameArr.push(route); - return false; - } - - return true; - }; - - if (parentName) { - // recursive block - return routes.filter(filter); - } - - const filteredRoutes = routes.filter(filter); - - if (parentNameArr.length) { - return sortRoutes(setChildRoute([...filteredRoutes, ...wrappers], parentNameArr)); - } - - return filteredRoutes; -} - -export function setChildRoute( - routes: ABP.FullRoute[], - parentNameArr: ABP.FullRoute[], -): ABP.FullRoute[] { - return routes.map(route => { - if (route.children && route.children.length) { - route.children = setChildRoute(route.children, parentNameArr); - } - - const foundedChildren = parentNameArr.filter(parent => parent.parentName === route.name); - if (foundedChildren && foundedChildren.length) { - route.children = [...(route.children || []), ...foundedChildren]; - } - - return route; - }); -} - -export function sortRoutes(routes: ABP.FullRoute[] = []): ABP.FullRoute[] { - if (!routes.length) return []; - return routes - .map((route, index) => { - return { - ...route, - order: typeof route.order === 'undefined' ? index + 1 : route.order, - }; - }) - .sort((a, b) => a.order - b.order) - .map(route => { - if (route.children && route.children.length) { - route.children = sortRoutes(route.children); - } - - return route; - }); -} - -const ABP_ROUTES = [] as ABP.FullRoute[]; - -export function addAbpRoutes(routes: ABP.FullRoute | ABP.FullRoute[]): void { - if (!Array.isArray(routes)) { - routes = [routes]; - } - - ABP_ROUTES.push(...routes); -} - -export function getAbpRoutes(): ABP.FullRoute[] { - return ABP_ROUTES; + return '/' + (primaryGroup || emptyGroup).segments.map(({ path }) => path).join('/'); } From 19a405f3f7f721667750dc907039b6914405968c Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:44:43 +0300 Subject: [PATCH 033/135] feat: add a service for routes and setting tabs --- .../packages/core/src/lib/services/index.ts | 1 + .../core/src/lib/services/routes.service.ts | 144 ++++++++++++++++++ .../core/src/lib/tests/routes.service.spec.ts | 113 ++++++++++++++ 3 files changed, 258 insertions(+) create mode 100644 npm/ng-packs/packages/core/src/lib/services/routes.service.ts create mode 100644 npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts diff --git a/npm/ng-packs/packages/core/src/lib/services/index.ts b/npm/ng-packs/packages/core/src/lib/services/index.ts index f01dc876de..064dcbd35e 100644 --- a/npm/ng-packs/packages/core/src/lib/services/index.ts +++ b/npm/ng-packs/packages/core/src/lib/services/index.ts @@ -9,5 +9,6 @@ export * from './localization.service'; export * from './profile-state.service'; export * from './profile.service'; export * from './rest.service'; +export * from './routes.service'; export * from './session-state.service'; export * from './track-by.service'; diff --git a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts new file mode 100644 index 0000000000..4b9f3e7899 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts @@ -0,0 +1,144 @@ +import { Injectable } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { ABP } from '../models/common'; +import { ConfigState } from '../states/config.state'; +import { pushValueTo } from '../utils/array-utils'; +import { BaseTreeNode, createTreeFromList, TreeNode } from '../utils/tree-utils'; + +export abstract class AbstractTreeService { + abstract id: string; + abstract parentId: string; + abstract hide: (item: T) => boolean; + abstract sort: (a: T, b: T) => number; + + private _flat$ = new BehaviorSubject([]); + private _tree$ = new BehaviorSubject[]>([]); + private _visible$ = new BehaviorSubject[]>([]); + + get flat(): T[] { + return this._flat$.value; + } + + get flat$(): Observable { + return this._flat$.asObservable(); + } + + get tree(): TreeNode[] { + return this._tree$.value; + } + + get tree$(): Observable[]> { + return this._tree$.asObservable(); + } + + get visible(): TreeNode[] { + return this._visible$.value; + } + + get visible$(): Observable[]> { + return this._visible$.asObservable(); + } + + protected createTree(items: T[]): TreeNode[] { + return createTreeFromList>( + items, + item => item[this.id], + item => item[this.parentId], + item => BaseTreeNode.create(item), + ); + } + + private filterWith(setOrMap: Set | Map): T[] { + return this._flat$.value.filter( + item => !setOrMap.has(item[this.id]) && !setOrMap.has(item[this.parentId]), + ); + } + + private publish(flatItems: T[], visibleItems: T[]): T[] { + this._flat$.next(flatItems); + this._tree$.next(this.createTree(flatItems)); + this._visible$.next(this.createTree(visibleItems)); + return flatItems; + } + + add(items: T[]): T[] { + const map = new Map(); + items.forEach(item => map.set(item[this.id], item)); + + const flatItems = this.filterWith(map); + map.forEach(pushValueTo(flatItems)); + + flatItems.sort(this.sort); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + patch(identifier: string, props: Partial): T[] | false { + const flatItems = this._flat$.value; + const index = flatItems.findIndex(item => item[this.id] === identifier); + if (index < 0) return false; + + flatItems[index] = { ...flatItems[index], ...props }; + + flatItems.sort(this.sort); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + remove(identifiers: string[]): T[] { + const set = new Set(); + identifiers.forEach(id => set.add(id)); + + const flatItems = this.filterWith(set); + const visibleItems = flatItems.filter(item => !this.hide(item)); + + return this.publish(flatItems, visibleItems); + } + + search(params: Partial, tree = this.tree): TreeNode { + const searchKeys = Object.keys(params); + + return tree.reduce( + (acc, node) => + acc + ? acc + : searchKeys.every(key => node[key] === params[key]) + ? node + : node.children + ? this.search(params, node.children) + : acc, + null, + ); + } +} + +@Injectable({ + providedIn: 'root', +}) +export class RoutesService extends AbstractTreeService { + readonly id = 'name'; + readonly parentId = 'parentName'; + readonly hide = (item: ABP.Route) => item.invisible; + readonly sort = (a: ABP.Route, b: ABP.Route) => a.order - b.order; +} + +@Injectable({ + providedIn: 'root', +}) +export class SettingTabsService extends AbstractTreeService { + readonly id = 'name'; + readonly parentId = 'parentName'; + readonly hide = (setting: ABP.Tab) => setting.invisible || !this.isGranted(setting); + readonly sort = (a: ABP.Tab, b: ABP.Tab) => a.order - b.order; + + constructor(private store: Store) { + super(); + } + + private isGranted(setting: ABP.Tab): boolean { + return this.store.selectSnapshot(ConfigState.getGrantedPolicy(setting.requiredPolicy)); + } +} diff --git a/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts new file mode 100644 index 0000000000..2392a86183 --- /dev/null +++ b/npm/ng-packs/packages/core/src/lib/tests/routes.service.spec.ts @@ -0,0 +1,113 @@ +import { take } from 'rxjs/operators'; +import { RoutesService } from '../services'; + +const routes = [ + { path: '/foo', name: 'foo' }, + { path: '/foo/bar', name: 'bar', parentName: 'foo', invisible: true, order: 2 }, + { path: '/foo/bar/baz', name: 'baz', parentName: 'bar', order: 1 }, + { path: '/foo/x', name: 'x', parentName: 'foo', order: 1 }, +]; + +describe('Routes Service', () => { + describe('#add', () => { + it('should add given routes as flat$, tree$, and visible$', async () => { + const service = new RoutesService(); + service.add(routes); + + const flat = await service.flat$.pipe(take(1)).toPromise(); + const tree = await service.tree$.pipe(take(1)).toPromise(); + const visible = await service.visible$.pipe(take(1)).toPromise(); + + expect(flat.length).toBe(4); + expect(flat[0].name).toBe('foo'); + expect(flat[1].name).toBe('baz'); + expect(flat[2].name).toBe('x'); + expect(flat[3].name).toBe('bar'); + + expect(tree.length).toBe(1); + expect(tree[0].name).toBe('foo'); + expect(tree[0].children.length).toBe(2); + expect(tree[0].children[0].name).toBe('x'); + expect(tree[0].children[1].name).toBe('bar'); + expect(tree[0].children[1].children[0].name).toBe('baz'); + + expect(visible.length).toBe(1); + expect(visible[0].name).toBe('foo'); + expect(visible[0].children.length).toBe(1); + expect(visible[0].children[0].name).toBe('x'); + }); + }); + + describe('#remove', () => { + it('should remove routes based on given routeNames', () => { + const service = new RoutesService(); + service.add(routes); + service.remove(['bar']); + + const flat = service.flat; + const tree = service.tree; + const visible = service.visible; + + expect(flat.length).toBe(2); + expect(flat[0].name).toBe('foo'); + expect(flat[1].name).toBe('x'); + + expect(tree.length).toBe(1); + expect(tree[0].name).toBe('foo'); + expect(tree[0].children.length).toBe(1); + expect(tree[0].children[0].name).toBe('x'); + + expect(visible.length).toBe(1); + expect(visible[0].name).toBe('foo'); + expect(visible[0].children.length).toBe(1); + expect(visible[0].children[0].name).toBe('x'); + }); + }); + + describe('#patch', () => { + it('should patch propeties of routes based on given routeNames', () => { + const service = new RoutesService(); + service.add(routes); + service.patch('x', { invisible: true }); + + const flat = service.flat; + const tree = service.tree; + const visible = service.visible; + + expect(flat.length).toBe(4); + expect(flat[0].name).toBe('foo'); + expect(flat[1].name).toBe('baz'); + expect(flat[2].name).toBe('x'); + expect(flat[3].name).toBe('bar'); + + expect(tree.length).toBe(1); + expect(tree[0].name).toBe('foo'); + expect(tree[0].children.length).toBe(2); + expect(tree[0].children[0].name).toBe('x'); + expect(tree[0].children[1].name).toBe('bar'); + expect(tree[0].children[1].children[0].name).toBe('baz'); + + expect(visible.length).toBe(1); + expect(visible[0].name).toBe('foo'); + expect(visible[0].children.length).toBe(0); + }); + + it('should return false when route name is not found', () => { + const service = new RoutesService(); + service.add(routes); + const result = service.patch('A man has no name.', { invisible: true }); + expect(result).toBe(false); + }); + }); + + describe('#search', () => { + it('should return node found when route name is not found', () => { + const service = new RoutesService(); + service.add(routes); + const result = service.search({ invisible: true }); + expect(result.name).toBe('bar'); + expect(result.children.length).toBe(1); + expect(result.children[0].name).toBe('baz'); + }); + }); +}); From 82188a0d2916f86fba427bea5b2b70666c827b2b Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:53:37 +0300 Subject: [PATCH 034/135] feat: use RoutesService in DynamicLayoutComponent --- .../components/dynamic-layout.component.ts | 73 ++++++++----------- .../tests/dynamic-layout.component.spec.ts | 66 +++++++++-------- 2 files changed, 68 insertions(+), 71 deletions(-) diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index c07f11580f..5455187086 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -1,13 +1,14 @@ -import { Component, OnDestroy, Type } from '@angular/core'; -import { ActivatedRoute, NavigationEnd, Router, UrlSegment } from '@angular/router'; +import { Component, Injector, OnDestroy, Type } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngxs/store'; -import snq from 'snq'; import { eLayoutType } from '../enums/common'; -import { ABP } from '../models/common'; +import { ABP } from '../models'; import { ReplaceableComponents } from '../models/replaceable-components'; -import { ConfigState } from '../states/config.state'; +import { RoutesService } from '../services/routes.service'; import { ReplaceableComponentsState } from '../states/replaceable-components.state'; +import { getRoutePath } from '../utils/route-utils'; import { takeUntilDestroy } from '../utils/rxjs-utils'; +import { TreeNode } from '../utils/tree-utils'; @Component({ selector: 'abp-dynamic-layout', @@ -22,23 +23,37 @@ import { takeUntilDestroy } from '../utils/rxjs-utils'; export class DynamicLayoutComponent implements OnDestroy { layout: Type; - constructor(private router: Router, private route: ActivatedRoute, private store: Store) { - const { routes } = this.store.selectSnapshot(ConfigState.getAll); + constructor( + injector: Injector, + private route: ActivatedRoute, + private routes: RoutesService, + private store: Store, + ) { + const router = injector.get(Router); + const layouts = { + application: this.getComponent('Theme.ApplicationLayoutComponent'), + account: this.getComponent('Theme.AccountLayoutComponent'), + empty: this.getComponent('Theme.EmptyLayoutComponent'), + }; router.events.pipe(takeUntilDestroy(this)).subscribe(event => { if (event instanceof NavigationEnd) { - const segments = snq(() => router.parseUrl(event.url).root.children.primary.segments, [ - { path: router.url.replace('/', '') }, - ] as any); + let expectedLayout = (this.route.snapshot.data || {}).layout; + const path = getRoutePath(router); - const layouts = { - application: this.getComponent('Theme.ApplicationLayoutComponent'), - account: this.getComponent('Theme.AccountLayoutComponent'), - empty: this.getComponent('Theme.EmptyLayoutComponent'), - }; + if (!expectedLayout) { + let node = { parent: this.routes.search({ path }) } as TreeNode; + while (node.parent) { + node = node.parent; - const expectedLayout = - (this.route.snapshot.data || {}).layout || findLayout(segments, routes); + if (node.layout) { + expectedLayout = node.layout; + break; + } + } + } + + if (!expectedLayout) expectedLayout = eLayoutType.empty; this.layout = layouts[expectedLayout].component; } @@ -51,27 +66,3 @@ export class DynamicLayoutComponent implements OnDestroy { ngOnDestroy() {} } - -function findLayout(segments: UrlSegment[], routes: ABP.FullRoute[]): eLayoutType { - let layout = eLayoutType.empty; - - const route = routes - .reduce((acc, val) => (val.wrapper ? [...acc, ...val.children] : [...acc, val]), []) - .find(r => r.path === segments[0].path); - - if (route) { - if (route.layout) { - layout = route.layout; - } - - if (route.children && route.children.length && segments.length > 1) { - const child = route.children.find(c => c.path === segments[1].path); - - if (child && child.layout) { - layout = child.layout; - } - } - } - - return layout; -} diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts index e1a678a19f..89a6a683b9 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts @@ -1,13 +1,13 @@ +import { HttpClient } from '@angular/common/http'; import { Component, NgModule } from '@angular/core'; import { ActivatedRoute, RouterModule } from '@angular/router'; import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; import { NgxsModule, Store } from '@ngxs/store'; import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; -import { eLayoutType } from '../enums'; +import { eLayoutType } from '../enums/common'; import { ABP } from '../models'; -import { ConfigState, ReplaceableComponentsState } from '../states'; -import { ApplicationConfigurationService } from '../services'; -import { HttpClient } from '@angular/common/http'; +import { ApplicationConfigurationService, RoutesService } from '../services'; +import { ReplaceableComponentsState } from '../states'; @Component({ selector: 'abp-layout-application', @@ -48,28 +48,36 @@ class DummyComponent { constructor(public route: ActivatedRoute) {} } -const storeData = { - ConfigState: { - routes: [ - { - path: '', - wrapper: true, - children: [ - { - path: 'parentWithLayout', - layout: eLayoutType.application, - children: [ - { path: 'childWithoutLayout' }, - { path: 'childWithLayout', layout: eLayoutType.account }, - ], - }, - ], - }, - { path: 'withData', layout: eLayoutType.application }, - , - ] as ABP.FullRoute[], - environment: { application: {} }, +const routes: ABP.Route[] = [ + { + path: '', + name: 'Root', + }, + { + path: '/parentWithLayout', + name: 'ParentWithLayout', + parentName: 'Root', + layout: eLayoutType.application, + }, + { + path: '/parentWithLayout/childWithoutLayout', + name: 'ChildWithoutLayout', + parentName: 'ParentWithLayout', + }, + { + path: '/parentWithLayout/childWithLayout', + name: 'ChildWithLayout', + parentName: 'ParentWithLayout', + layout: eLayoutType.account, + }, + { + path: '/withData', + name: 'WithData', + layout: eLayoutType.application, }, +]; + +const storeData = { ReplaceableComponentsState: { replaceableComponents: [ { @@ -94,11 +102,7 @@ describe('DynamicLayoutComponent', () => { stubsEnabled: false, declarations: [DummyComponent, DynamicLayoutComponent], mocks: [ApplicationConfigurationService, HttpClient], - imports: [ - RouterModule, - DummyLayoutModule, - NgxsModule.forRoot([ConfigState, ReplaceableComponentsState]), - ], + imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot([ReplaceableComponentsState])], routes: [ { path: '', component: RouterOutletComponent }, { @@ -150,6 +154,8 @@ describe('DynamicLayoutComponent', () => { beforeEach(async () => { spectator = createComponent(); store = spectator.get(Store); + const routesService = spectator.get(RoutesService); + routesService.add(routes); store.reset(storeData); }); From 9c0fe4c8e145ef1519fa8dadf54adedc999325af Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:55:22 +0300 Subject: [PATCH 035/135] refactor: use ABP.Route type instead of ABP.FullRoute --- .../nav-items/nav-items.component.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts index 7bae923989..6819807cd7 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts @@ -1,31 +1,31 @@ -import { - Component, - AfterViewInit, - TrackByFunction, - TemplateRef, - ViewChild, - OnDestroy, - Input, -} from '@angular/core'; import { ABP, - takeUntilDestroy, - SetLanguage, + ApplicationConfiguration, AuthService, ConfigState, - ApplicationConfiguration, SessionState, + SetLanguage, + takeUntilDestroy, } from '@abp/ng.core'; -import { LayoutState } from '../../states/layout.state'; -import { Store, Select } from '@ngxs/store'; -import { eNavigationElementNames } from '../../enums/navigation-element-names'; -import { AddNavigationElement } from '../../actions/layout.actions'; -import { map, filter } from 'rxjs/operators'; -import { Observable } from 'rxjs'; -import { Layout } from '../../models/layout'; +import { + AfterViewInit, + Component, + Input, + OnDestroy, + TemplateRef, + TrackByFunction, + ViewChild, +} from '@angular/core'; import { Navigate, RouterState } from '@ngxs/router-plugin'; -import snq from 'snq'; +import { Select, Store } from '@ngxs/store'; import compare from 'just-compare'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import snq from 'snq'; +import { AddNavigationElement } from '../../actions/layout.actions'; +import { eNavigationElementNames } from '../../enums/navigation-element-names'; +import { Layout } from '../../models/layout'; +import { LayoutState } from '../../states/layout.state'; @Component({ selector: 'abp-nav-items', @@ -52,7 +52,7 @@ export class NavItemsComponent implements AfterViewInit, OnDestroy { rightPartElements: TemplateRef[] = []; - trackByFn: TrackByFunction = (_, element) => element; + trackByFn: TrackByFunction = (_, element) => element.name; get defaultLanguage$(): Observable { return this.languages$.pipe( From 962e636640f497348860e63429a218bc2076ea87 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:56:28 +0300 Subject: [PATCH 036/135] feat: use RoutesService in RoutesComponent --- .../components/routes/routes.component.html | 33 +++++++------------ .../lib/components/routes/routes.component.ts | 33 +++---------------- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html index 6437233cb1..4d22ddc8d2 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html @@ -1,6 +1,6 @@
- - + + + + diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts index 2ed204e0b0..8b73c8ee29 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts @@ -1,37 +1,14 @@ -import { ABP, ConfigState } from '@abp/ng.core'; -import { Component, Input, Renderer2, TrackByFunction } from '@angular/core'; -import { Select } from '@ngxs/store'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { ABP, RoutesService, TreeNode } from '@abp/ng.core'; +import { Component, Input, TrackByFunction } from '@angular/core'; @Component({ selector: 'abp-routes', templateUrl: 'routes.component.html', }) export class RoutesComponent { - @Select(ConfigState.getOne('routes')) - routes$: Observable; + @Input() smallScreen: boolean; - @Input() - smallScreen: boolean; + trackByFn: TrackByFunction> = (_, item) => item.name; - get visibleRoutes$(): Observable { - return this.routes$.pipe(map(routes => getVisibleRoutes(routes))); - } - - trackByFn: TrackByFunction = (_, item) => item.name; - - constructor(private renderer: Renderer2) {} -} - -function getVisibleRoutes(routes: ABP.FullRoute[]) { - return routes.reduce((acc, val) => { - if (val.invisible) return acc; - - if (val.children && val.children.length) { - val.children = getVisibleRoutes(val.children); - } - - return [...acc, val]; - }, []); + constructor(public readonly routes: RoutesService) {} } From 8cd85a4d443defd1694c72776f08726c819fd597 Mon Sep 17 00:00:00 2001 From: Arman Ozak Date: Wed, 17 Jun 2020 18:57:30 +0300 Subject: [PATCH 037/135] feat: use RoutesService in BreadcrumbComponent --- .../breadcrumb/breadcrumb.component.html | 4 +- .../breadcrumb/breadcrumb.component.ts | 84 +++++++++---------- 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html index fa8f7df3da..da17512ea1 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.html @@ -1,4 +1,4 @@ -