mirror of https://github.com/abpframework/abp.git
210 changed files with 5018 additions and 1164 deletions
@ -1,13 +1,17 @@ |
|||
@using Localization.Resources.AbpUi |
|||
@using Microsoft.Extensions.Localization |
|||
|
|||
@inject IStringLocalizer<AbpUiResource> L |
|||
|
|||
<form method="post" class="page-search-form"> |
|||
<div class="input-group"> |
|||
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value"> |
|||
<input class="form-control page-search-filter-text" placeholder="@L["Search"].Value" /> |
|||
<div class="input-group-append"> |
|||
<abp-button button-type="Primary" type="submit"> |
|||
<i class="fa fa-search" aria-hidden="true" style="margin-left: 10%; margin-top:50%;" ></i> |
|||
<i class="fa fa-search" aria-hidden="true"></i> |
|||
</abp-button> |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</form> |
|||
|
|||
|
|||
|
|||
@ -0,0 +1,4 @@ |
|||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bundling |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,25 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<AssemblyName>Volo.Abp.Caching.StackExchangeRedis</AssemblyName> |
|||
<PackageId>Volo.Abp.Caching.StackExchangeRedis</PackageId> |
|||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\Volo.Abp.Caching\Volo.Abp.Caching.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="3.1.5" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,30 @@ |
|||
using System; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Caching.StackExchangeRedis |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpCachingModule) |
|||
)] |
|||
public class AbpCachingStackExchangeRedisModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
var configuration = context.Services.GetConfiguration(); |
|||
|
|||
context.Services.AddStackExchangeRedisCache(options => |
|||
{ |
|||
var redisConfiguration = configuration["Redis:Configuration"]; |
|||
if (!redisConfiguration.IsNullOrEmpty()) |
|||
{ |
|||
options.Configuration = configuration["Redis:Configuration"]; |
|||
} |
|||
}); |
|||
|
|||
context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, AbpRedisCache>()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,49 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Extensions.Caching.StackExchangeRedis; |
|||
using Microsoft.Extensions.Options; |
|||
using StackExchange.Redis; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Caching.StackExchangeRedis |
|||
{ |
|||
[DisableConventionalRegistration] |
|||
public class AbpRedisCache : RedisCache |
|||
{ |
|||
private static readonly FieldInfo RedisDatabaseField; |
|||
private static readonly MethodInfo ConnectMethod; |
|||
private static readonly MethodInfo ConnectAsyncMethod; |
|||
|
|||
protected IDatabase RedisDatabase => RedisDatabaseField.GetValue(this) as IDatabase; |
|||
|
|||
static AbpRedisCache() |
|||
{ |
|||
RedisDatabaseField = typeof(RedisCache) |
|||
.GetField("_cache", BindingFlags.Instance | BindingFlags.NonPublic); |
|||
|
|||
ConnectMethod = typeof(RedisCache) |
|||
.GetMethod("Connect", BindingFlags.Instance | BindingFlags.NonPublic); |
|||
|
|||
ConnectAsyncMethod = typeof(RedisCache) |
|||
.GetMethod("ConnectAsync", BindingFlags.Instance | BindingFlags.NonPublic); |
|||
} |
|||
|
|||
public AbpRedisCache(IOptions<RedisCacheOptions> optionsAccessor) |
|||
: base(optionsAccessor) |
|||
{ |
|||
|
|||
} |
|||
|
|||
protected virtual void Connect() |
|||
{ |
|||
ConnectMethod.Invoke(this, Array.Empty<object>()); |
|||
} |
|||
|
|||
protected virtual Task ConnectAsync(CancellationToken token = default) |
|||
{ |
|||
return (Task) ConnectAsyncMethod.Invoke(this, new object[] {token}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\common.test.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netcoreapp3.1</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" /> |
|||
<ProjectReference Include="..\..\src\Volo.Abp.Caching.StackExchangeRedis\Volo.Abp.Caching.StackExchangeRedis.csproj" /> |
|||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,12 @@ |
|||
using Volo.Abp.Testing; |
|||
|
|||
namespace Volo.Abp.Caching.StackExchangeRedis |
|||
{ |
|||
public abstract class AbpCachingStackExchangeRedisTestBase : AbpIntegratedTest<AbpCachingStackExchangeRedisTestModule> |
|||
{ |
|||
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) |
|||
{ |
|||
options.UseAutofac(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using Volo.Abp.Autofac; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace Volo.Abp.Caching.StackExchangeRedis |
|||
{ |
|||
[DependsOn( |
|||
typeof(AbpCachingStackExchangeRedisModule), |
|||
typeof(AbpTestBaseModule), |
|||
typeof(AbpAutofacModule) |
|||
)] |
|||
public class AbpCachingStackExchangeRedisTestModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Shouldly; |
|||
using Xunit; |
|||
|
|||
namespace Volo.Abp.Caching.StackExchangeRedis |
|||
{ |
|||
public class AbpRedisCache_Tests : AbpCachingStackExchangeRedisTestBase |
|||
{ |
|||
private readonly IDistributedCache _distributedCache; |
|||
|
|||
public AbpRedisCache_Tests() |
|||
{ |
|||
_distributedCache = GetRequiredService<IDistributedCache>(); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Replace_RedisCache() |
|||
{ |
|||
(_distributedCache is AbpRedisCache).ShouldBeTrue(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
import { RoutesService, eLayoutType } from '@abp/ng.core'; |
|||
import { APP_INITIALIZER } from '@angular/core'; |
|||
|
|||
export const APP_ROUTE_PROVIDER = [ |
|||
{ provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, |
|||
]; |
|||
|
|||
function configureRoutes(routes: RoutesService) { |
|||
return () => { |
|||
routes.add([ |
|||
{ |
|||
path: '/', |
|||
name: '::Menu:Home', |
|||
iconClass: 'fas fa-home', |
|||
order: 1, |
|||
layout: eLayoutType.application, |
|||
}, |
|||
]); |
|||
}; |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
entryPoints: { |
|||
'.': {}, |
|||
'./config': {}, |
|||
'./dist': { ignore: true }, |
|||
}, |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
entryPoints: { |
|||
'.': {}, |
|||
'./config': {}, |
|||
'./dist': { ignore: true }, |
|||
}, |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
entryPoints: { |
|||
'.': {}, |
|||
'./config': {}, |
|||
'./dist': { ignore: true }, |
|||
}, |
|||
}; |
|||
@ -0,0 +1,7 @@ |
|||
module.exports = { |
|||
entryPoints: { |
|||
'.': {}, |
|||
'./config': {}, |
|||
'./dist': { ignore: true }, |
|||
}, |
|||
}; |
|||
@ -1 +0,0 @@ |
|||
export * from './layout.actions'; |
|||
@ -1,11 +0,0 @@ |
|||
import { Layout } from '../models/layout'; |
|||
|
|||
export class AddNavigationElement { |
|||
static readonly type = '[Layout] Add Navigation Element'; |
|||
constructor(public payload: Layout.NavigationElement | Layout.NavigationElement[]) {} |
|||
} |
|||
|
|||
export class RemoveNavigationElementByName { |
|||
static readonly type = '[Layout] Remove Navigation ElementByName'; |
|||
constructor(public name: string) {} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
import { ApplicationConfiguration, AuthService, ConfigState } from '@abp/ng.core'; |
|||
import { Component, OnInit } from '@angular/core'; |
|||
import { Router } from '@angular/router'; |
|||
import { Select } from '@ngxs/store'; |
|||
import { Observable } from 'rxjs'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-current-user', |
|||
// tslint:disable-next-line: component-max-inline-declarations
|
|||
template: ` |
|||
<ng-template #loginBtn> |
|||
<a role="button" class="nav-link" routerLink="/account/login">{{ |
|||
'AbpAccount::Login' | abpLocalization |
|||
}}</a> |
|||
</ng-template> |
|||
<div |
|||
*ngIf="(currentUser$ | async)?.isAuthenticated; else loginBtn" |
|||
ngbDropdown |
|||
class="dropdown" |
|||
#currentUserDropdown="ngbDropdown" |
|||
display="static" |
|||
> |
|||
<a |
|||
ngbDropdownToggle |
|||
class="nav-link" |
|||
href="javascript:void(0)" |
|||
role="button" |
|||
id="dropdownMenuLink" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
aria-expanded="false" |
|||
> |
|||
{{ (currentUser$ | async)?.userName }} |
|||
</a> |
|||
<div |
|||
class="dropdown-menu dropdown-menu-right border-0 shadow-sm" |
|||
aria-labelledby="dropdownMenuLink" |
|||
[class.d-block]="smallScreen && currentUserDropdown.isOpen()" |
|||
> |
|||
<a class="dropdown-item" routerLink="/account/manage-profile" |
|||
><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::ManageYourProfile' | abpLocalization }}</a |
|||
> |
|||
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()" |
|||
><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a |
|||
> |
|||
</div> |
|||
</div> |
|||
`,
|
|||
}) |
|||
export class CurrentUserComponent implements OnInit { |
|||
@Select(ConfigState.getOne('currentUser')) |
|||
currentUser$: Observable<ApplicationConfiguration.CurrentUser>; |
|||
|
|||
get smallScreen(): boolean { |
|||
return window.innerWidth < 992; |
|||
} |
|||
|
|||
constructor(private authService: AuthService, private router: Router) {} |
|||
|
|||
ngOnInit() {} |
|||
|
|||
logout() { |
|||
this.authService.logout().subscribe(() => { |
|||
this.router.navigate(['/'], { state: { redirectUrl: this.router.url } }); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
import { Component, OnInit, Input } from '@angular/core'; |
|||
import { Store, Select } from '@ngxs/store'; |
|||
import { SetLanguage, ConfigState, ApplicationConfiguration, SessionState } from '@abp/ng.core'; |
|||
import { Observable } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
import snq from 'snq'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-languages', |
|||
// tslint:disable-next-line: component-max-inline-declarations
|
|||
template: ` |
|||
<div |
|||
*ngIf="(dropdownLanguages$ | async)?.length > 0" |
|||
class="dropdown" |
|||
ngbDropdown |
|||
#languageDropdown="ngbDropdown" |
|||
display="static" |
|||
> |
|||
<a |
|||
ngbDropdownToggle |
|||
class="nav-link" |
|||
href="javascript:void(0)" |
|||
role="button" |
|||
id="dropdownMenuLink" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
aria-expanded="false" |
|||
> |
|||
{{ defaultLanguage$ | async }} |
|||
</a> |
|||
<div |
|||
class="dropdown-menu dropdown-menu-right border-0 shadow-sm" |
|||
aria-labelledby="dropdownMenuLink" |
|||
[class.d-block]="smallScreen && languageDropdown.isOpen()" |
|||
> |
|||
<a |
|||
*ngFor="let lang of dropdownLanguages$ | async" |
|||
href="javascript:void(0)" |
|||
class="dropdown-item" |
|||
(click)="onChangeLang(lang.cultureName)" |
|||
>{{ lang?.displayName }}</a |
|||
> |
|||
</div> |
|||
</div> |
|||
`,
|
|||
}) |
|||
export class LanguagesComponent implements OnInit { |
|||
get smallScreen(): boolean { |
|||
return window.innerWidth < 992; |
|||
} |
|||
|
|||
@Select(ConfigState.getDeep('localization.languages')) |
|||
languages$: Observable<ApplicationConfiguration.Language[]>; |
|||
|
|||
get defaultLanguage$(): Observable<string> { |
|||
return this.languages$.pipe( |
|||
map( |
|||
languages => |
|||
snq( |
|||
() => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName, |
|||
), |
|||
'', |
|||
), |
|||
); |
|||
} |
|||
|
|||
get dropdownLanguages$(): Observable<ApplicationConfiguration.Language[]> { |
|||
return this.languages$.pipe( |
|||
map( |
|||
languages => |
|||
snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)), |
|||
[], |
|||
), |
|||
); |
|||
} |
|||
|
|||
get selectedLangCulture(): string { |
|||
return this.store.selectSnapshot(SessionState.getLanguage); |
|||
} |
|||
|
|||
constructor(private store: Store) {} |
|||
|
|||
ngOnInit() {} |
|||
|
|||
onChangeLang(cultureName: string) { |
|||
this.store.dispatch(new SetLanguage(cultureName)); |
|||
} |
|||
} |
|||
@ -1,81 +1,16 @@ |
|||
<ul class="navbar-nav"> |
|||
<ng-container |
|||
*ngFor="let element of rightPartElements; trackBy: trackByFn" |
|||
[ngTemplateOutlet]="element" |
|||
[ngTemplateOutletContext]="{ smallScreen: smallScreen }" |
|||
></ng-container> |
|||
</ul> |
|||
|
|||
<ng-template #language let-smallScreen="smallScreen"> |
|||
<li *ngIf="(dropdownLanguages$ | async)?.length > 0" class="nav-item"> |
|||
<div class="dropdown" ngbDropdown #languageDropdown="ngbDropdown" display="static"> |
|||
<a |
|||
ngbDropdownToggle |
|||
class="nav-link" |
|||
href="javascript:void(0)" |
|||
role="button" |
|||
id="dropdownMenuLink" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
aria-expanded="false" |
|||
> |
|||
{{ defaultLanguage$ | async }} |
|||
</a> |
|||
<div |
|||
class="dropdown-menu dropdown-menu-right border-0 shadow-sm" |
|||
aria-labelledby="dropdownMenuLink" |
|||
[class.d-block]="smallScreen && languageDropdown.isOpen()" |
|||
> |
|||
<a |
|||
*ngFor="let lang of dropdownLanguages$ | async" |
|||
href="javascript:void(0)" |
|||
class="dropdown-item" |
|||
(click)="onChangeLang(lang.cultureName)" |
|||
>{{ lang?.displayName }}</a |
|||
> |
|||
</div> |
|||
</div> |
|||
</li> |
|||
</ng-template> |
|||
<li |
|||
class="nav-item d-flex align-items-center" |
|||
*ngFor="let item of navItems.items$ | async; trackBy: trackByFn" |
|||
[abpPermission]="item.requiredPolicy" |
|||
> |
|||
<ng-container |
|||
*ngIf="item.component; else htmlTemplate" |
|||
[ngComponentOutlet]="item.component" |
|||
></ng-container> |
|||
|
|||
<ng-template #currentUser let-smallScreen="smallScreen"> |
|||
<li class="nav-item"> |
|||
<ng-template #loginBtn> |
|||
<a role="button" class="nav-link" routerLink="/account/login">{{ |
|||
'AbpAccount::Login' | abpLocalization |
|||
}}</a> |
|||
<ng-template #htmlTemplate> |
|||
<div [innerHTML]="item.html" (click)="item.action ? item.action() : null"></div> |
|||
</ng-template> |
|||
<div |
|||
*ngIf="(currentUser$ | async)?.isAuthenticated; else loginBtn" |
|||
ngbDropdown |
|||
class="dropdown" |
|||
#currentUserDropdown="ngbDropdown" |
|||
display="static" |
|||
> |
|||
<a |
|||
ngbDropdownToggle |
|||
class="nav-link" |
|||
href="javascript:void(0)" |
|||
role="button" |
|||
id="dropdownMenuLink" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
aria-expanded="false" |
|||
> |
|||
{{ (currentUser$ | async)?.userName }} |
|||
</a> |
|||
<div |
|||
class="dropdown-menu dropdown-menu-right border-0 shadow-sm" |
|||
aria-labelledby="dropdownMenuLink" |
|||
[class.d-block]="smallScreen && currentUserDropdown.isOpen()" |
|||
> |
|||
<a class="dropdown-item" routerLink="/account/manage-profile" |
|||
><i class="fa fa-cog mr-1"></i>{{ 'AbpAccount::ManageYourProfile' | abpLocalization }}</a |
|||
> |
|||
<a class="dropdown-item" href="javascript:void(0)" (click)="logout()" |
|||
><i class="fa fa-power-off mr-1"></i>{{ 'AbpUi::Logout' | abpLocalization }}</a |
|||
> |
|||
</div> |
|||
</div> |
|||
</li> |
|||
</ng-template> |
|||
</ul> |
|||
|
|||
@ -1,125 +1,12 @@ |
|||
import { |
|||
ABP, |
|||
ApplicationConfiguration, |
|||
AuthService, |
|||
ConfigState, |
|||
SessionState, |
|||
SetLanguage, |
|||
takeUntilDestroy, |
|||
} from '@abp/ng.core'; |
|||
import { |
|||
AfterViewInit, |
|||
Component, |
|||
Input, |
|||
OnDestroy, |
|||
TemplateRef, |
|||
TrackByFunction, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { Navigate, RouterState } from '@ngxs/router-plugin'; |
|||
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'; |
|||
import { NavItem, NavItemsService } from '@abp/ng.theme.shared'; |
|||
import { Component, Input, TrackByFunction } from '@angular/core'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-nav-items', |
|||
templateUrl: 'nav-items.component.html', |
|||
}) |
|||
export class NavItemsComponent implements AfterViewInit, OnDestroy { |
|||
@Select(LayoutState.getNavigationElements) |
|||
navElements$: Observable<Layout.NavigationElement[]>; |
|||
export class NavItemsComponent { |
|||
trackByFn: TrackByFunction<NavItem> = (_, element) => element.id; |
|||
|
|||
@Select(ConfigState.getOne('currentUser')) |
|||
currentUser$: Observable<ApplicationConfiguration.CurrentUser>; |
|||
|
|||
@Select(ConfigState.getDeep('localization.languages')) |
|||
languages$: Observable<ApplicationConfiguration.Language[]>; |
|||
|
|||
@ViewChild('currentUser', { static: false, read: TemplateRef }) |
|||
currentUserRef: TemplateRef<any>; |
|||
|
|||
@ViewChild('language', { static: false, read: TemplateRef }) |
|||
languageRef: TemplateRef<any>; |
|||
|
|||
@Input() |
|||
smallScreen: boolean; |
|||
|
|||
rightPartElements: TemplateRef<any>[] = []; |
|||
|
|||
trackByFn: TrackByFunction<ABP.Route> = (_, element) => element.name; |
|||
|
|||
get defaultLanguage$(): Observable<string> { |
|||
return this.languages$.pipe( |
|||
map( |
|||
languages => |
|||
snq( |
|||
() => languages.find(lang => lang.cultureName === this.selectedLangCulture).displayName, |
|||
), |
|||
'', |
|||
), |
|||
); |
|||
} |
|||
|
|||
get dropdownLanguages$(): Observable<ApplicationConfiguration.Language[]> { |
|||
return this.languages$.pipe( |
|||
map( |
|||
languages => |
|||
snq(() => languages.filter(lang => lang.cultureName !== this.selectedLangCulture)), |
|||
[], |
|||
), |
|||
); |
|||
} |
|||
|
|||
get selectedLangCulture(): string { |
|||
return this.store.selectSnapshot(SessionState.getLanguage); |
|||
} |
|||
|
|||
constructor(private store: Store, private authService: AuthService) {} |
|||
|
|||
ngAfterViewInit() { |
|||
const navigations = this.store |
|||
.selectSnapshot(LayoutState.getNavigationElements) |
|||
.map(({ name }) => name); |
|||
|
|||
if (navigations.indexOf(eNavigationElementNames.Language) < 0) { |
|||
this.store.dispatch( |
|||
new AddNavigationElement([ |
|||
{ element: this.languageRef, order: 4, name: eNavigationElementNames.Language }, |
|||
{ element: this.currentUserRef, order: 5, name: eNavigationElementNames.User }, |
|||
]), |
|||
); |
|||
} |
|||
|
|||
this.navElements$ |
|||
.pipe( |
|||
map(elements => elements.map(({ element }) => element)), |
|||
filter(elements => !compare(elements, this.rightPartElements)), |
|||
takeUntilDestroy(this), |
|||
) |
|||
.subscribe(elements => { |
|||
setTimeout(() => (this.rightPartElements = elements), 0); |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() {} |
|||
|
|||
onChangeLang(cultureName: string) { |
|||
this.store.dispatch(new SetLanguage(cultureName)); |
|||
} |
|||
|
|||
logout() { |
|||
this.authService.logout().subscribe(() => { |
|||
this.store.dispatch( |
|||
new Navigate(['/'], null, { |
|||
state: { redirectUrl: this.store.selectSnapshot(RouterState).state.url }, |
|||
}), |
|||
); |
|||
}); |
|||
} |
|||
constructor(public readonly navItems: NavItemsService) {} |
|||
} |
|||
|
|||
@ -1,4 +0,0 @@ |
|||
export const enum eNavigationElementNames { |
|||
Language = 'LanguageRef', |
|||
User = 'CurrentUserRef', |
|||
} |
|||
@ -1 +1,2 @@ |
|||
export * from './nav-item.provider'; |
|||
export * from './styles.provider'; |
|||
|
|||
@ -0,0 +1,31 @@ |
|||
import { NavItemsService } from '@abp/ng.theme.shared'; |
|||
import { APP_INITIALIZER } from '@angular/core'; |
|||
import { CurrentUserComponent } from '../components/nav-items/current-user.component'; |
|||
import { LanguagesComponent } from '../components/nav-items/languages.component'; |
|||
import { eThemeBasicComponents } from '../enums/components'; |
|||
|
|||
export const BASIC_THEME_NAV_ITEM_PROVIDERS = [ |
|||
{ |
|||
provide: APP_INITIALIZER, |
|||
useFactory: configureNavItems, |
|||
deps: [NavItemsService], |
|||
multi: true, |
|||
}, |
|||
]; |
|||
|
|||
export function configureNavItems(navItems: NavItemsService) { |
|||
return () => { |
|||
navItems.addItems([ |
|||
{ |
|||
id: eThemeBasicComponents.Languages, |
|||
order: 100, |
|||
component: LanguagesComponent, |
|||
}, |
|||
{ |
|||
id: eThemeBasicComponents.CurrentUser, |
|||
order: 101, |
|||
component: CurrentUserComponent, |
|||
}, |
|||
]); |
|||
}; |
|||
} |
|||
@ -1 +0,0 @@ |
|||
export * from './layout-state.service'; |
|||
@ -1,24 +0,0 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { LayoutState } from '../states/layout.state'; |
|||
import { AddNavigationElement, RemoveNavigationElementByName } from '../actions'; |
|||
import { Layout } from '../models/layout'; |
|||
|
|||
@Injectable({ providedIn: 'root' }) |
|||
export class LayoutStateService { |
|||
constructor(private store: Store) {} |
|||
|
|||
getNavigationElements() { |
|||
return this.store.selectSnapshot(LayoutState.getNavigationElements); |
|||
} |
|||
|
|||
dispatchAddNavigationElement(...args: ConstructorParameters<typeof AddNavigationElement>) { |
|||
return this.store.dispatch(new AddNavigationElement(...args)); |
|||
} |
|||
|
|||
dispatchRemoveNavigationElementByName( |
|||
...args: ConstructorParameters<typeof RemoveNavigationElementByName> |
|||
) { |
|||
return this.store.dispatch(new RemoveNavigationElementByName(...args)); |
|||
} |
|||
} |
|||
@ -1 +0,0 @@ |
|||
export * from './layout.state'; |
|||
@ -1,67 +0,0 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { Action, Selector, State, StateContext } from '@ngxs/store'; |
|||
import snq from 'snq'; |
|||
import { AddNavigationElement, RemoveNavigationElementByName } from '../actions/layout.actions'; |
|||
import { Layout } from '../models/layout'; |
|||
|
|||
@State<Layout.State>({ |
|||
name: 'LayoutState', |
|||
defaults: { navigationElements: [] } as Layout.State, |
|||
}) |
|||
@Injectable() |
|||
export class LayoutState { |
|||
@Selector() |
|||
static getNavigationElements({ navigationElements }: Layout.State): Layout.NavigationElement[] { |
|||
return navigationElements; |
|||
} |
|||
|
|||
@Action(AddNavigationElement) |
|||
layoutAddAction( |
|||
{ getState, patchState }: StateContext<Layout.State>, |
|||
{ payload = [] }: AddNavigationElement, |
|||
) { |
|||
let { navigationElements } = getState(); |
|||
|
|||
if (!Array.isArray(payload)) { |
|||
payload = [payload]; |
|||
} |
|||
|
|||
if (navigationElements.length) { |
|||
payload = snq( |
|||
() => |
|||
(payload as Layout.NavigationElement[]).filter( |
|||
({ name }) => navigationElements.findIndex(nav => nav.name === name) < 0, |
|||
), |
|||
[], |
|||
); |
|||
} |
|||
|
|||
if (!payload.length) return; |
|||
|
|||
navigationElements = [...navigationElements, ...payload] |
|||
.map(element => ({ ...element, order: element.order || 99 })) |
|||
.sort((a, b) => a.order - b.order); |
|||
|
|||
return patchState({ |
|||
navigationElements, |
|||
}); |
|||
} |
|||
|
|||
@Action(RemoveNavigationElementByName) |
|||
layoutRemoveAction( |
|||
{ getState, patchState }: StateContext<Layout.State>, |
|||
{ name }: RemoveNavigationElementByName, |
|||
) { |
|||
let { navigationElements } = getState(); |
|||
|
|||
const index = navigationElements.findIndex(element => element.name === name); |
|||
|
|||
if (index > -1) { |
|||
navigationElements = navigationElements.splice(index, 1); |
|||
} |
|||
|
|||
return patchState({ |
|||
navigationElements, |
|||
}); |
|||
} |
|||
} |
|||
@ -1,57 +0,0 @@ |
|||
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; |
|||
import { Store } from '@ngxs/store'; |
|||
import * as LayoutActions from '../actions'; |
|||
import { LayoutStateService } from '../services/layout-state.service'; |
|||
import { LayoutState } from '../states/layout.state'; |
|||
describe('LayoutStateService', () => { |
|||
let service: LayoutStateService; |
|||
let spectator: SpectatorService<LayoutStateService>; |
|||
let store: SpyObject<Store>; |
|||
|
|||
const createService = createServiceFactory({ service: LayoutStateService, mocks: [Store] }); |
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
service = spectator.service; |
|||
store = spectator.get(Store); |
|||
}); |
|||
|
|||
test('should have the all LayoutState static methods', () => { |
|||
const reg = /(?<=static )(.*)(?=\()/gm; |
|||
LayoutState.toString() |
|||
.match(reg) |
|||
.forEach(fnName => { |
|||
expect(service[fnName]).toBeTruthy(); |
|||
|
|||
const spy = jest.spyOn(store, 'selectSnapshot'); |
|||
spy.mockClear(); |
|||
|
|||
const isDynamicSelector = LayoutState[fnName].name !== 'memoized'; |
|||
|
|||
if (isDynamicSelector) { |
|||
LayoutState[fnName] = jest.fn((...args) => args); |
|||
service[fnName]('test', 0, {}); |
|||
expect(LayoutState[fnName]).toHaveBeenCalledWith('test', 0, {}); |
|||
} else { |
|||
service[fnName](); |
|||
expect(spy).toHaveBeenCalledWith(LayoutState[fnName]); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
test('should have a dispatch method for every LayoutState action', () => { |
|||
const reg = /(?<=dispatch)(\w+)(?=\()/gm; |
|||
LayoutStateService.toString() |
|||
.match(reg) |
|||
.forEach(fnName => { |
|||
expect(LayoutActions[fnName]).toBeTruthy(); |
|||
|
|||
const spy = jest.spyOn(store, 'dispatch'); |
|||
spy.mockClear(); |
|||
|
|||
const params = Array.from(new Array(LayoutActions[fnName].length)); |
|||
|
|||
service[`dispatch${fnName}`](...params); |
|||
expect(spy).toHaveBeenCalledWith(new LayoutActions[fnName](...params)); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/theme-shared/extensions", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { NgbDateStruct, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Injectable() |
|||
export class DateTimeAdapter { |
|||
value: NgbDateTimeStruct; |
|||
|
|||
fromModel(value: string | Date): NgbDateTimeStruct | null { |
|||
if (!value) return null; |
|||
|
|||
const date = new Date(value); |
|||
|
|||
if (isNaN((date as unknown) as number)) return null; |
|||
|
|||
this.value = { |
|||
year: date.getFullYear(), |
|||
month: date.getMonth() + 1, |
|||
day: date.getDate(), |
|||
hour: date.getHours(), |
|||
minute: date.getMinutes(), |
|||
second: date.getSeconds(), |
|||
}; |
|||
|
|||
return this.value; |
|||
} |
|||
|
|||
toModel(value: NgbDateTimeStruct | null): string { |
|||
if (!value) return ''; |
|||
|
|||
const now = new Date(); |
|||
|
|||
value = { |
|||
year: now.getUTCFullYear(), |
|||
month: now.getMonth() + 1, |
|||
day: now.getDate(), |
|||
hour: 0, |
|||
minute: 0, |
|||
second: 0, |
|||
...this.value, |
|||
...value, |
|||
}; |
|||
|
|||
const date = new Date( |
|||
value.year, |
|||
value.month - 1, |
|||
value.day, |
|||
value.hour, |
|||
value.minute, |
|||
value.second, |
|||
); |
|||
|
|||
return new Date(date).toISOString(); |
|||
} |
|||
} |
|||
|
|||
type NgbDateTimeStruct = NgbDateStruct & NgbTimeStruct; |
|||
@ -0,0 +1,29 @@ |
|||
import { formatDate } from '@angular/common'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { NgbDateAdapter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Injectable() |
|||
export class DateAdapter extends NgbDateAdapter<string> { |
|||
fromModel(value: string | Date): NgbDateStruct | null { |
|||
if (!value) return null; |
|||
|
|||
const date = new Date(value); |
|||
|
|||
if (isNaN((date as unknown) as number)) return null; |
|||
|
|||
return { |
|||
day: date.getDate(), |
|||
month: date.getMonth() + 1, |
|||
year: date.getFullYear(), |
|||
}; |
|||
} |
|||
|
|||
toModel(value: NgbDateStruct | null): string { |
|||
if (!value) return ''; |
|||
|
|||
const date = new Date(value.year, value.month - 1, value.day); |
|||
const formattedDate = formatDate(date, 'yyyy-MM-dd', 'en'); |
|||
|
|||
return formattedDate; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
import { formatDate } from '@angular/common'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { NgbTimeAdapter, NgbTimeStruct } from '@ng-bootstrap/ng-bootstrap'; |
|||
|
|||
@Injectable() |
|||
export class TimeAdapter extends NgbTimeAdapter<string> { |
|||
fromModel(value: string | Date): NgbTimeStruct | null { |
|||
if (!value) return null; |
|||
|
|||
const date = isTimeStr(value) |
|||
? new Date(0, 0, 1, ...value.split(':').map(Number)) |
|||
: new Date(value); |
|||
|
|||
if (isNaN((date as unknown) as number)) return null; |
|||
|
|||
return { |
|||
hour: date.getHours(), |
|||
minute: date.getMinutes(), |
|||
second: date.getSeconds(), |
|||
}; |
|||
} |
|||
|
|||
toModel(value: NgbTimeStruct | null): string { |
|||
if (!value) return ''; |
|||
|
|||
const date = new Date(0, 0, 1, value.hour, value.minute, value.second); |
|||
const formattedDate = formatDate(date, 'HH:mm', 'en'); |
|||
|
|||
return formattedDate; |
|||
} |
|||
} |
|||
|
|||
function isTimeStr(value: string | Date): value is string { |
|||
return /^((2[123])|[01][0-9])(\:[0-5][0-9]){1,2}$/.test(String(value)); |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
import { Directive, Injector, Input } from '@angular/core'; |
|||
import { ActionData, ActionList } from '../../models/actions'; |
|||
import { ExtensionsService } from '../../services/extensions.service'; |
|||
import { EXTENSIONS_ACTION_TYPE, EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; |
|||
|
|||
// tslint:disable: directive-class-suffix
|
|||
// Fix for https://github.com/angular/angular/issues/23904
|
|||
// @dynamic
|
|||
@Directive() |
|||
export abstract class AbstractActionsComponent<L extends ActionList<any>> extends ActionData< |
|||
InferredRecord<L> |
|||
> { |
|||
readonly actionList: L; |
|||
|
|||
readonly getInjected: InferredData<L>['getInjected']; |
|||
|
|||
@Input() readonly record: InferredData<L>['record']; |
|||
|
|||
constructor(injector: Injector) { |
|||
super(); |
|||
|
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
const extensions = injector.get(ExtensionsService); |
|||
const name = injector.get(EXTENSIONS_IDENTIFIER); |
|||
const type = injector.get(EXTENSIONS_ACTION_TYPE); |
|||
this.actionList = (extensions[type].get(name).actions as unknown) as L; |
|||
} |
|||
} |
|||
|
|||
type InferredData<L> = ActionData<InferredRecord<L>>; |
|||
type InferredRecord<L> = L extends ActionList<infer R> ? R : never; |
|||
@ -0,0 +1,74 @@ |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Input, |
|||
Optional, |
|||
SkipSelf, |
|||
ViewChild, |
|||
} from '@angular/core'; |
|||
import { ControlContainer } from '@angular/forms'; |
|||
import { |
|||
NgbDateAdapter, |
|||
NgbInputDatepicker, |
|||
NgbTimeAdapter, |
|||
NgbTimepicker, |
|||
} from '@ng-bootstrap/ng-bootstrap'; |
|||
import { DateTimeAdapter } from '../../adapters/date-time.adapter'; |
|||
import { FormProp } from '../../models/form-props'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpDateTimePicker', |
|||
selector: 'abp-date-time-picker', |
|||
template: ` |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
(ngModelChange)="setTime($event)" |
|||
(click)="datepicker.open()" |
|||
(keyup.space)="datepicker.open()" |
|||
ngbDatepicker |
|||
#datepicker="ngbDatepicker" |
|||
type="text" |
|||
class="form-control" |
|||
/> |
|||
<ngb-timepicker |
|||
#timepicker |
|||
[formControlName]="prop.name" |
|||
(ngModelChange)="setDate($event)" |
|||
></ngb-timepicker> |
|||
`,
|
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
viewProviders: [ |
|||
{ |
|||
provide: ControlContainer, |
|||
useFactory: selfFactory, |
|||
deps: [[new Optional(), new SkipSelf(), ControlContainer]], |
|||
}, |
|||
{ |
|||
provide: NgbDateAdapter, |
|||
useClass: DateTimeAdapter, |
|||
}, |
|||
{ |
|||
provide: NgbTimeAdapter, |
|||
useClass: DateTimeAdapter, |
|||
}, |
|||
], |
|||
}) |
|||
export class DateTimePickerComponent { |
|||
@Input() prop: FormProp; |
|||
|
|||
@ViewChild(NgbInputDatepicker) date: NgbInputDatepicker; |
|||
@ViewChild(NgbTimepicker) time: NgbTimepicker; |
|||
|
|||
constructor(public readonly cdRef: ChangeDetectorRef) {} |
|||
|
|||
setDate(datestr: string) { |
|||
this.date.writeValue(datestr); |
|||
} |
|||
|
|||
setTime(datestr: string) { |
|||
this.time.writeValue(datestr); |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
<div class="form-group" [abpPermission]="prop.permission" [ngSwitch]="getComponent(prop)"> |
|||
<ng-template ngSwitchCase="input"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[autocomplete]="prop.autocomplete" |
|||
[type]="getType(prop)" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
[readonly]="prop.readonly(data)" |
|||
class="form-control" |
|||
/> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="checkbox"> |
|||
<div class="custom-checkbox custom-control" validationTarget> |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
type="checkbox" |
|||
class="custom-control-input" |
|||
/> |
|||
<label [htmlFor]="prop.id" class="custom-control-label" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
</div> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="select"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<select |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
class="custom-select form-control" |
|||
> |
|||
<option |
|||
*ngFor="let option of prop.options(data) | async; trackBy: track.by('value')" |
|||
[ngValue]="option.value" |
|||
>{{ option.key }}</option |
|||
> |
|||
</select> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="date"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<input |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
(click)="datepicker.open()" |
|||
(keyup.space)="datepicker.open()" |
|||
ngbDatepicker |
|||
#datepicker="ngbDatepicker" |
|||
type="text" |
|||
class="form-control" |
|||
/> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="time"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<ngb-timepicker [formControlName]="prop.name"></ngb-timepicker> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="dateTime"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<abp-date-time-picker [prop]="prop"></abp-date-time-picker> |
|||
</ng-template> |
|||
|
|||
<ng-template ngSwitchCase="textarea"> |
|||
<label [htmlFor]="prop.id" |
|||
>{{ prop.displayName | abpLocalization }} {{ getAsterisk(prop, data) }}</label |
|||
> |
|||
<textarea |
|||
[id]="prop.id" |
|||
[formControlName]="prop.name" |
|||
[abpDisabled]="prop.disabled(data)" |
|||
[readonly]="prop.readonly(data)" |
|||
class="form-control" |
|||
></textarea> |
|||
</ng-template> |
|||
</div> |
|||
@ -0,0 +1,80 @@ |
|||
import { TrackByService } from '@abp/ng.core'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Input, |
|||
Optional, |
|||
SkipSelf, |
|||
} from '@angular/core'; |
|||
import { ControlContainer, Validators } from '@angular/forms'; |
|||
import { NgbDateAdapter, NgbTimeAdapter } from '@ng-bootstrap/ng-bootstrap'; |
|||
import { DateAdapter } from '../../adapters/date.adapter'; |
|||
import { TimeAdapter } from '../../adapters/time.adapter'; |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { FormProp } from '../../models/form-props'; |
|||
import { PropData } from '../../models/props'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
|
|||
@Component({ |
|||
selector: 'abp-extensible-form-prop', |
|||
templateUrl: './extensible-form-prop.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
viewProviders: [ |
|||
{ |
|||
provide: ControlContainer, |
|||
useFactory: selfFactory, |
|||
deps: [[new Optional(), new SkipSelf(), ControlContainer]], |
|||
}, |
|||
{ provide: NgbDateAdapter, useClass: DateAdapter }, |
|||
{ provide: NgbTimeAdapter, useClass: TimeAdapter }, |
|||
], |
|||
}) |
|||
export class ExtensibleFormPropComponent { |
|||
@Input() data: PropData; |
|||
|
|||
@Input() prop: FormProp; |
|||
|
|||
constructor(public readonly cdRef: ChangeDetectorRef, public readonly track: TrackByService) {} |
|||
|
|||
getAsterisk(prop: FormProp, data: PropData): string { |
|||
return prop.validators(data).some(validator => validator === Validators.required) ? '*' : ''; |
|||
} |
|||
|
|||
getComponent(prop: FormProp): string { |
|||
if (prop.options) return 'select'; |
|||
|
|||
switch (prop.type) { |
|||
case ePropType.Boolean: |
|||
return 'checkbox'; |
|||
case ePropType.Date: |
|||
return 'date'; |
|||
case ePropType.DateTime: |
|||
return 'dateTime'; |
|||
case ePropType.Text: |
|||
return 'textarea'; |
|||
case ePropType.Time: |
|||
return 'time'; |
|||
default: |
|||
return 'input'; |
|||
} |
|||
} |
|||
|
|||
getType(prop: FormProp): string { |
|||
switch (prop.type) { |
|||
case ePropType.Date: |
|||
case ePropType.String: |
|||
return 'text'; |
|||
case ePropType.Boolean: |
|||
return 'checkbox'; |
|||
case ePropType.Number: |
|||
return 'number'; |
|||
case ePropType.Email: |
|||
return 'email'; |
|||
case ePropType.Password: |
|||
return 'password'; |
|||
default: |
|||
return 'hidden'; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<ng-container *ngIf="form"> |
|||
<ng-container *abpPropData="let data; fromList: propList; withRecord: record"> |
|||
<ng-container *ngFor="let prop of propList; trackBy: track.by('name')"> |
|||
<ng-container *ngIf="prop.visible(data)"> |
|||
<ng-container |
|||
[formGroupName]="extraPropertiesKey" |
|||
*ngIf="extraProperties.controls[prop.name]; else tempDefault" |
|||
> |
|||
<abp-extensible-form-prop [prop]="prop" [data]="data"></abp-extensible-form-prop> |
|||
</ng-container> |
|||
|
|||
<ng-template #tempDefault> |
|||
<abp-extensible-form-prop |
|||
*ngIf="form.get(prop.name)" |
|||
[prop]="prop" |
|||
[data]="data" |
|||
></abp-extensible-form-prop> |
|||
</ng-template> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
</ng-container> |
|||
@ -0,0 +1,82 @@ |
|||
import { TrackByService } from '@abp/ng.core'; |
|||
import { |
|||
AfterViewInit, |
|||
ChangeDetectionStrategy, |
|||
ChangeDetectorRef, |
|||
Component, |
|||
Inject, |
|||
Input, |
|||
OnDestroy, |
|||
Optional, |
|||
QueryList, |
|||
SkipSelf, |
|||
ViewChildren, |
|||
} from '@angular/core'; |
|||
import { ControlContainer, FormGroup } from '@angular/forms'; |
|||
import { Subscription } from 'rxjs'; |
|||
import { debounceTime } from 'rxjs/operators'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../../constants/extra-properties'; |
|||
import { FormPropList } from '../../models/form-props'; |
|||
import { ExtensionsService } from '../../services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; |
|||
import { selfFactory } from '../../utils/factory.util'; |
|||
import { ExtensibleFormPropComponent } from './extensible-form-prop.component'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpExtensibleForm', |
|||
selector: 'abp-extensible-form', |
|||
templateUrl: './extensible-form.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
viewProviders: [ |
|||
{ |
|||
provide: ControlContainer, |
|||
useFactory: selfFactory, |
|||
deps: [[new Optional(), new SkipSelf(), ControlContainer]], |
|||
}, |
|||
], |
|||
}) |
|||
export class ExtensibleFormComponent<R = any> implements AfterViewInit, OnDestroy { |
|||
@ViewChildren(ExtensibleFormPropComponent) |
|||
formProps: QueryList<ExtensibleFormPropComponent>; |
|||
|
|||
@Input() |
|||
set selectedRecord(record: R) { |
|||
const type = !record || JSON.stringify(record) === '{}' ? 'create' : 'edit'; |
|||
this.propList = this.extensions[`${type}FormProps`].get(this.identifier).props; |
|||
this.record = record; |
|||
} |
|||
|
|||
private subscription = new Subscription(); |
|||
extraPropertiesKey = EXTRA_PROPERTIES_KEY; |
|||
propList: FormPropList<R>; |
|||
record: R; |
|||
|
|||
get form(): FormGroup { |
|||
return (this.container ? this.container.control : { controls: {} }) as FormGroup; |
|||
} |
|||
|
|||
get extraProperties(): FormGroup { |
|||
return (this.form.controls.extraProperties || { controls: {} }) as FormGroup; |
|||
} |
|||
|
|||
constructor( |
|||
public readonly cdRef: ChangeDetectorRef, |
|||
public readonly track: TrackByService, |
|||
private container: ControlContainer, |
|||
private extensions: ExtensionsService, |
|||
@Inject(EXTENSIONS_IDENTIFIER) private identifier: string, |
|||
) {} |
|||
|
|||
ngAfterViewInit() { |
|||
this.subscription.add( |
|||
this.form.statusChanges.pipe(debounceTime(0)).subscribe(() => { |
|||
this.formProps.forEach(prop => prop.cdRef.markForCheck()); |
|||
this.cdRef.detectChanges(); |
|||
}), |
|||
); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.subscription.unsubscribe(); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
<ngx-datatable default [rows]="data" [count]="recordsTotal" [list]="list"> |
|||
<ngx-datatable-column |
|||
[name]="actionsText | abpLocalization" |
|||
[maxWidth]="columnWidths[0]" |
|||
[width]="columnWidths[0]" |
|||
[sortable]="false" |
|||
> |
|||
<ng-template let-row="row" let-i="rowIndex" ngx-datatable-cell-template> |
|||
<ng-container |
|||
*ngTemplateOutlet="actionsTemplate || gridActions; context: { $implicit: row, index: i }" |
|||
></ng-container> |
|||
<ng-template #gridActions> |
|||
<abp-grid-actions |
|||
[index]="i" |
|||
[record]="row" |
|||
text="LanguageManagement::Actions" |
|||
></abp-grid-actions> |
|||
</ng-template> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
|
|||
<ng-container *ngFor="let prop of propList; let i = index; trackBy: trackByFn"> |
|||
<ngx-datatable-column |
|||
[width]="columnWidths[i + 1] || 200" |
|||
[name]="prop.displayName | abpLocalization" |
|||
[prop]="prop.name" |
|||
[sortable]="prop.sortable" |
|||
> |
|||
<ng-template let-row="row" let-i="index" ngx-datatable-cell-template> |
|||
<ng-container |
|||
*abpPropData="let data; fromList: propList; withRecord: row; atIndex: i" |
|||
[abpPermission]="prop.permission" |
|||
> |
|||
<div *ngIf="prop.visible(data)" [innerHTML]="getContent(prop, data) | async"></div> |
|||
</ng-container> |
|||
</ng-template> |
|||
</ngx-datatable-column> |
|||
</ng-container> |
|||
</ngx-datatable> |
|||
@ -0,0 +1,88 @@ |
|||
import { ListService } from '@abp/ng.core'; |
|||
import { formatDate } from '@angular/common'; |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
Inject, |
|||
Injector, |
|||
Input, |
|||
LOCALE_ID, |
|||
TemplateRef, |
|||
TrackByFunction, |
|||
} from '@angular/core'; |
|||
import { Observable } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { EntityProp, EntityPropList } from '../../models/entity-props'; |
|||
import { PropData } from '../../models/props'; |
|||
import { ExtensionsService } from '../../services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; |
|||
const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpExtensibleTable', |
|||
selector: 'abp-extensible-table', |
|||
templateUrl: './extensible-table.component.html', |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class ExtensibleTableComponent<R = any> { |
|||
@Input() actionsText: string; |
|||
@Input() data: R[]; |
|||
@Input() list: ListService; |
|||
@Input() recordsTotal: number; |
|||
@Input() set actionsColumnWidth(width: number) { |
|||
this.setColumnWidths(width ? Number(width) : undefined); |
|||
} |
|||
@Input() actionsTemplate: TemplateRef<any>; |
|||
|
|||
readonly columnWidths: number[]; |
|||
|
|||
readonly propList: EntityPropList<R>; |
|||
|
|||
readonly trackByFn: TrackByFunction<EntityProp<R>> = (_, item) => item.name; |
|||
|
|||
constructor(@Inject(LOCALE_ID) private locale: string, injector: Injector) { |
|||
const extensions = injector.get(ExtensionsService); |
|||
const name = injector.get(EXTENSIONS_IDENTIFIER); |
|||
this.propList = extensions.entityProps.get(name).props; |
|||
this.setColumnWidths(DEFAULT_ACTIONS_COLUMN_WIDTH); |
|||
} |
|||
|
|||
private setColumnWidths(actionsColumn: number) { |
|||
const widths = [actionsColumn]; |
|||
this.propList.forEach(({ value: prop }) => { |
|||
widths.push(prop.columnWidth); |
|||
}); |
|||
(this.columnWidths as any) = widths; |
|||
} |
|||
|
|||
private getDate(value: Date, format: string) { |
|||
return value ? formatDate(value, format, this.locale) : ''; |
|||
} |
|||
|
|||
private getIcon(value: boolean) { |
|||
return value |
|||
? '<div class="text-center text-success"><i class="fa fa-check"></i></div>' |
|||
: '<div class="text-center text-danger"><i class="fa fa-times"></i></div>'; |
|||
} |
|||
|
|||
getContent(prop: EntityProp<R>, data: PropData): Observable<string> { |
|||
return prop.valueResolver(data).pipe( |
|||
map(value => { |
|||
switch (prop.type) { |
|||
case ePropType.Boolean: |
|||
return this.getIcon(value); |
|||
case ePropType.Date: |
|||
return this.getDate(value, 'yyyy-MM-dd'); |
|||
case ePropType.Time: |
|||
return this.getDate(value, 'HH:mm'); |
|||
case ePropType.DateTime: |
|||
return this.getDate(value, 'yyyy-MM-dd HH:mm:ss Z'); |
|||
default: |
|||
return value; |
|||
// More types can be handled in the future
|
|||
} |
|||
}), |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
<div ngbDropdown container="body" class="d-inline-block"> |
|||
<button |
|||
class="btn btn-primary btn-sm dropdown-toggle" |
|||
data-toggle="dropdown" |
|||
aria-haspopup="true" |
|||
ngbDropdownToggle |
|||
> |
|||
<i [ngClass]="icon" [class.mr-1]="icon"></i>{{ text | abpLocalization }} |
|||
</button> |
|||
<div ngbDropdownMenu> |
|||
<ng-container *ngFor="let action of actionList; trackBy: trackByFn"> |
|||
<button |
|||
*ngIf="action.visible(data)" |
|||
ngbDropdownItem |
|||
[abpPermission]="action.permission" |
|||
(click)="action.action(data)" |
|||
type="button" |
|||
> |
|||
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i> |
|||
{{ action.text | abpLocalization }} |
|||
</button> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,36 @@ |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
Injector, |
|||
Input, |
|||
TrackByFunction, |
|||
} from '@angular/core'; |
|||
import { EntityAction, EntityActionList } from '../../models/entity-actions'; |
|||
import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; |
|||
import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpGridActions', |
|||
selector: 'abp-grid-actions', |
|||
templateUrl: './grid-actions.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_ACTION_TYPE, |
|||
useValue: 'entityActions', |
|||
}, |
|||
], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class GridActionsComponent<R = any> extends AbstractActionsComponent<EntityActionList<R>> { |
|||
@Input() icon = 'fa fa-cog'; |
|||
|
|||
@Input() readonly index: number; |
|||
|
|||
@Input() text = ''; |
|||
|
|||
readonly trackByFn: TrackByFunction<EntityAction<R>> = (_, item) => item.text; |
|||
|
|||
constructor(injector: Injector) { |
|||
super(injector); |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
<div class="row justify-content-end mx-n1" id="AbpContentToolbar"> |
|||
<div class="col-auto px-1 pt-2" *ngFor="let action of actionList; trackBy: trackByFn"> |
|||
<ng-container *ngIf="action.visible(data)" [abpPermission]="action.permission"> |
|||
<ng-container *ngIf="action.component as component; else button"> |
|||
<ng-container |
|||
*ngComponentOutlet="component; injector: createInjector(action)" |
|||
></ng-container> |
|||
</ng-container> |
|||
|
|||
<ng-template #button> |
|||
<button (click)="action.action(data)" type="button" class="btn btn-primary btn-sm"> |
|||
<i [ngClass]="action.icon" [class.mr-1]="action.icon"></i> |
|||
{{ action.text | abpLocalization }} |
|||
</button> |
|||
</ng-template> |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,49 @@ |
|||
import { |
|||
ChangeDetectionStrategy, |
|||
Component, |
|||
InjectFlags, |
|||
InjectionToken, |
|||
Injector, |
|||
TrackByFunction, |
|||
Type, |
|||
} from '@angular/core'; |
|||
import { ToolbarActionList, ToolbarComponent } from '../../models/toolbar-actions'; |
|||
import { |
|||
EXTENSIONS_ACTION_CALLBACK, |
|||
EXTENSIONS_ACTION_DATA, |
|||
EXTENSIONS_ACTION_TYPE, |
|||
} from '../../tokens/extensions.token'; |
|||
import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; |
|||
|
|||
@Component({ |
|||
exportAs: 'abpPageToolbar', |
|||
selector: 'abp-page-toolbar', |
|||
templateUrl: './page-toolbar.component.html', |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_ACTION_TYPE, |
|||
useValue: 'toolbarActions', |
|||
}, |
|||
], |
|||
changeDetection: ChangeDetectionStrategy.OnPush, |
|||
}) |
|||
export class PageToolbarComponent<R = any> extends AbstractActionsComponent<ToolbarActionList<R>> { |
|||
readonly trackByFn: TrackByFunction<ToolbarComponent<R>> = (_, item) => |
|||
item.action || item.component; |
|||
|
|||
constructor(private readonly injector: Injector) { |
|||
super(injector); |
|||
} |
|||
|
|||
createInjector(action: ToolbarComponent<R>): Injector { |
|||
const get = <T>(token: Type<T> | InjectionToken<T>, notFoundValue?: T, flags?: InjectFlags) => { |
|||
return token === EXTENSIONS_ACTION_DATA |
|||
? this.data |
|||
: token === EXTENSIONS_ACTION_CALLBACK |
|||
? (data = this.data) => action.action(data) |
|||
: this.getInjected.call(this.injector, token, notFoundValue, flags); |
|||
}; |
|||
|
|||
return { get }; |
|||
} |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export const EXTRA_PROPERTIES_KEY = 'extraProperties'; |
|||
@ -0,0 +1,19 @@ |
|||
import { Directive, Host, Input, OnChanges, SimpleChanges } from '@angular/core'; |
|||
import { NgControl } from '@angular/forms'; |
|||
|
|||
@Directive({ |
|||
selector: '[abpDisabled]', |
|||
}) |
|||
export class DisabledDirective implements OnChanges { |
|||
@Input() |
|||
abpDisabled: boolean; |
|||
|
|||
constructor(@Host() private ngControl: NgControl) {} |
|||
|
|||
// Related issue: https://github.com/angular/angular/issues/35330
|
|||
ngOnChanges({ abpDisabled }: SimpleChanges) { |
|||
if (this.ngControl.control && abpDisabled) { |
|||
this.ngControl.control[abpDisabled.currentValue ? 'disable' : 'enable'](); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
import { |
|||
Directive, |
|||
Injector, |
|||
Input, |
|||
OnChanges, |
|||
OnDestroy, |
|||
TemplateRef, |
|||
ViewContainerRef, |
|||
} from '@angular/core'; |
|||
import { PropData, PropList } from '../models/props'; |
|||
|
|||
@Directive({ |
|||
exportAs: 'abpPropData', |
|||
selector: '[abpPropData]', |
|||
}) |
|||
export class PropDataDirective<L extends PropList<any>> extends PropData<InferredData<L>> |
|||
implements OnChanges, OnDestroy { |
|||
/* tslint:disable:no-input-rename */ |
|||
@Input('abpPropDataFromList') readonly propList: L; |
|||
|
|||
@Input('abpPropDataWithRecord') readonly record: InferredData<L>['record']; |
|||
|
|||
@Input('abpPropDataAtIndex') readonly index: number; |
|||
/* tslint:enable:no-input-rename */ |
|||
|
|||
readonly getInjected: InferredData<L>['getInjected']; |
|||
|
|||
constructor( |
|||
private tempRef: TemplateRef<any>, |
|||
private vcRef: ViewContainerRef, |
|||
injector: Injector, |
|||
) { |
|||
super(); |
|||
|
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
} |
|||
|
|||
ngOnChanges() { |
|||
this.vcRef.clear(); |
|||
|
|||
this.vcRef.createEmbeddedView(this.tempRef, { |
|||
$implicit: this.data, |
|||
index: 0, |
|||
}); |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
this.vcRef.clear(); |
|||
} |
|||
} |
|||
|
|||
type InferredData<L> = PropData<InferredRecord<L>>; |
|||
type InferredRecord<L> = L extends PropList<infer R> ? R : never; |
|||
@ -0,0 +1,12 @@ |
|||
export const enum ePropType { |
|||
Boolean = 'boolean', |
|||
Date = 'date', |
|||
DateTime = 'dateTime', |
|||
Email = 'email', |
|||
Enum = 'enum', |
|||
Number = 'number', |
|||
Password = 'password', |
|||
String = 'string', |
|||
Text = 'text', |
|||
Time = 'time', |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { InjectFlags, InjectionToken, Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
|
|||
export abstract class ActionList<R = any, A = Action<R>> extends LinkedList<A> {} |
|||
|
|||
export abstract class ActionData<R = any> { |
|||
abstract getInjected: <T>( |
|||
token: Type<T> | InjectionToken<T>, |
|||
notFoundValue?: T, |
|||
flags?: InjectFlags, |
|||
) => T; |
|||
index?: number; |
|||
abstract record: R; |
|||
|
|||
get data(): ReadonlyActionData<R> { |
|||
return { |
|||
getInjected: this.getInjected, |
|||
index: this.index, |
|||
record: this.record, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export type ReadonlyActionData<R = any> = O.Readonly<Omit<ActionData<R>, 'data'>>; |
|||
|
|||
export abstract class Action<R = any> { |
|||
constructor( |
|||
public readonly permission: string, |
|||
public readonly visible: ActionPredicate<R> = _ => true, |
|||
public readonly action: ActionCallback<R> = _ => {}, |
|||
) {} |
|||
} |
|||
|
|||
export type ActionCallback<T, R = any> = (data?: Omit<ActionData<T>, 'data'>) => R; |
|||
export type ActionPredicate<T> = (data?: Omit<ActionData<T>, 'data'>) => boolean; |
|||
|
|||
export abstract class ActionsFactory<C extends Actions<any>> { |
|||
protected abstract _ctor: Type<C>; |
|||
private contributorCallbacks: ActionContributorCallbacks<InferredActionList<C>> = {}; |
|||
|
|||
get(name: string): C { |
|||
this.contributorCallbacks[name] = this.contributorCallbacks[name] || []; |
|||
|
|||
return new this._ctor(this.contributorCallbacks[name]); |
|||
} |
|||
} |
|||
|
|||
export abstract class Actions<L extends ActionList> { |
|||
protected abstract _ctor: Type<L>; |
|||
|
|||
get actions(): L { |
|||
const actionList = new this._ctor(); |
|||
|
|||
this.callbackList.forEach(callback => callback(actionList)); |
|||
|
|||
return actionList; |
|||
} |
|||
|
|||
constructor(private readonly callbackList: ActionContributorCallback<L>[]) {} |
|||
|
|||
addContributor(contributeCallback: ActionContributorCallback<L>) { |
|||
this.callbackList.push(contributeCallback); |
|||
} |
|||
|
|||
clearContributors() { |
|||
while (this.callbackList.length) this.callbackList.pop(); |
|||
} |
|||
} |
|||
|
|||
export type ActionContributorCallbacks<L extends ActionList<any>> = Record< |
|||
string, |
|||
ActionContributorCallback<L>[] |
|||
>; |
|||
|
|||
export type ActionContributorCallback<L extends ActionList<any>> = (actionList: L) => any; |
|||
|
|||
type InferredActionList<C> = C extends Actions<infer L> ? L : never; |
|||
@ -0,0 +1,53 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Action, |
|||
ActionContributorCallback, |
|||
ActionContributorCallbacks, |
|||
ActionList, |
|||
Actions, |
|||
ActionsFactory, |
|||
} from './actions'; |
|||
|
|||
export class EntityActionList<R = any> extends ActionList<R, EntityAction<R>> {} |
|||
|
|||
export class EntityActions<R = any> extends Actions<EntityActionList<R>> { |
|||
protected _ctor: Type<EntityActionList<R>> = EntityActionList; |
|||
} |
|||
|
|||
export class EntityActionsFactory<R = any> extends ActionsFactory<EntityActions<R>> { |
|||
protected _ctor: Type<EntityActions<R>> = EntityActions; |
|||
} |
|||
|
|||
export class EntityAction<R = any> extends Action<R> { |
|||
readonly text: string; |
|||
readonly icon: string; |
|||
|
|||
constructor(options: EntityActionOptions<R>) { |
|||
super(options.permission, options.visible, options.action); |
|||
this.text = options.text; |
|||
this.icon = options.icon || ''; |
|||
} |
|||
|
|||
static create<R = any>(options: EntityActionOptions<R>) { |
|||
return new EntityAction<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: EntityActionOptions<R>[]) { |
|||
return arrayOfOptions.map(EntityAction.create); |
|||
} |
|||
} |
|||
|
|||
export type EntityActionOptions<R = any> = O.Optional< |
|||
O.Writable<EntityAction<R>>, |
|||
'permission' | 'visible' | 'icon' |
|||
>; |
|||
|
|||
export type EntityActionDefaults<R = any> = Record<string, EntityAction<R>[]>; |
|||
export type EntityActionContributorCallback<R = any> = ActionContributorCallback< |
|||
EntityActionList<R> |
|||
>; |
|||
export type EntityActionContributorCallbacks<R = any> = ActionContributorCallbacks< |
|||
EntityActionList<R> |
|||
>; |
|||
@ -0,0 +1,67 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { Type } from '@angular/core'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Prop, |
|||
PropCallback, |
|||
PropContributorCallback, |
|||
PropContributorCallbacks, |
|||
PropList, |
|||
Props, |
|||
PropsFactory, |
|||
} from './props'; |
|||
|
|||
export class EntityPropList<R = any> extends PropList<R, EntityProp<R>> {} |
|||
|
|||
export class EntityProps<R = any> extends Props<EntityPropList<R>> { |
|||
protected _ctor: Type<EntityPropList<R>> = EntityPropList; |
|||
} |
|||
|
|||
export class EntityPropsFactory<R = any> extends PropsFactory<EntityProps<R>> { |
|||
protected _ctor: Type<EntityProps<R>> = EntityProps; |
|||
} |
|||
|
|||
export class EntityProp<R = any> extends Prop<R> { |
|||
readonly columnWidth: number | undefined; |
|||
readonly sortable: boolean; |
|||
readonly valueResolver: PropCallback<R, Observable<any>>; |
|||
|
|||
constructor(options: EntityPropOptions<R>) { |
|||
super( |
|||
options.type, |
|||
options.name, |
|||
options.displayName, |
|||
options.permission, |
|||
options.visible, |
|||
options.isExtra, |
|||
); |
|||
|
|||
this.columnWidth = options.columnWidth; |
|||
this.sortable = options.sortable || false; |
|||
this.valueResolver = options.valueResolver || (data => of(data.record[this.name])); |
|||
} |
|||
|
|||
static create<R = any>(options: EntityPropOptions<R>) { |
|||
return new EntityProp<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: EntityPropOptions<R>[]) { |
|||
return arrayOfOptions.map(EntityProp.create); |
|||
} |
|||
} |
|||
|
|||
export type EntityPropOptions<R = any> = O.Optional< |
|||
O.Writable<EntityProp<R>>, |
|||
| 'permission' |
|||
| 'visible' |
|||
| 'displayName' |
|||
| 'isExtra' |
|||
| 'columnWidth' |
|||
| 'sortable' |
|||
| 'valueResolver' |
|||
>; |
|||
|
|||
export type EntityPropDefaults<R = any> = Record<string, EntityProp<R>[]>; |
|||
export type EntityPropContributorCallback<R = any> = PropContributorCallback<EntityPropList<R>>; |
|||
export type EntityPropContributorCallbacks<R = any> = PropContributorCallbacks<EntityPropList<R>>; |
|||
@ -0,0 +1,109 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { ABP } from '@abp/ng.core'; |
|||
import { Injector, Type } from '@angular/core'; |
|||
import { AsyncValidatorFn, ValidatorFn } from '@angular/forms'; |
|||
import { Observable } from 'rxjs'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Prop, |
|||
PropCallback, |
|||
PropContributorCallback, |
|||
PropContributorCallbacks, |
|||
PropData, |
|||
PropList, |
|||
PropPredicate, |
|||
Props, |
|||
PropsFactory, |
|||
} from './props'; |
|||
|
|||
export class FormPropList<R = any> extends PropList<R, FormProp<R>> {} |
|||
|
|||
export class FormProps<R = any> extends Props<FormPropList<R>> { |
|||
protected _ctor: Type<FormPropList<R>> = FormPropList; |
|||
} |
|||
|
|||
export class CreateFormPropsFactory<R = any> extends PropsFactory<FormProps<R>> { |
|||
protected _ctor: Type<FormProps<R>> = FormProps; |
|||
} |
|||
|
|||
export class EditFormPropsFactory<R = any> extends PropsFactory<FormProps<R>> { |
|||
protected _ctor: Type<FormProps<R>> = FormProps; |
|||
} |
|||
|
|||
export class FormProp<R = any> extends Prop<R> { |
|||
readonly validators: PropCallback<R, ValidatorFn[]>; |
|||
readonly asyncValidators: PropCallback<R, AsyncValidatorFn[]>; |
|||
readonly disabled: PropPredicate<R>; |
|||
readonly readonly: PropPredicate<R>; |
|||
readonly autocomplete: string; |
|||
readonly defaultValue: boolean | number | string | Date; |
|||
readonly options: PropCallback<R, Observable<ABP.Option<any>[]>> | undefined; |
|||
readonly id: string | undefined; |
|||
|
|||
constructor(options: FormPropOptions<R>) { |
|||
super( |
|||
options.type, |
|||
options.name, |
|||
options.displayName, |
|||
options.permission, |
|||
options.visible, |
|||
options.isExtra, |
|||
); |
|||
|
|||
this.asyncValidators = options.asyncValidators || (_ => []); |
|||
this.validators = options.validators || (_ => []); |
|||
this.disabled = options.disabled || (_ => false); |
|||
this.readonly = options.readonly || (_ => false); |
|||
this.autocomplete = options.autocomplete || 'off'; |
|||
this.options = options.options; |
|||
this.id = options.id || options.name; |
|||
const defaultValue = options.defaultValue; |
|||
this.defaultValue = isFalsyValue(defaultValue) ? defaultValue : defaultValue || null; |
|||
} |
|||
|
|||
static create<R = any>(options: FormPropOptions<R>) { |
|||
return new FormProp<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: FormPropOptions<R>[]) { |
|||
return arrayOfOptions.map(FormProp.create); |
|||
} |
|||
} |
|||
|
|||
export class FormPropData<R = any> extends PropData<R> { |
|||
getInjected: PropData<R>['getInjected']; |
|||
|
|||
constructor(injector: Injector, public readonly record: R) { |
|||
super(); |
|||
|
|||
// tslint:disable-next-line
|
|||
this.getInjected = injector.get.bind(injector); |
|||
} |
|||
} |
|||
|
|||
export type FormPropOptions<R = any> = O.Optional< |
|||
O.Writable<FormProp<R>>, |
|||
| 'permission' |
|||
| 'visible' |
|||
| 'displayName' |
|||
| 'isExtra' |
|||
| 'validators' |
|||
| 'asyncValidators' |
|||
| 'disabled' |
|||
| 'readonly' |
|||
| 'autocomplete' |
|||
| 'defaultValue' |
|||
| 'options' |
|||
| 'id' |
|||
>; |
|||
|
|||
export type CreateFormPropDefaults<R = any> = Record<string, FormProp<R>[]>; |
|||
export type CreateFormPropContributorCallback<R = any> = PropContributorCallback<FormPropList<R>>; |
|||
export type CreateFormPropContributorCallbacks<R = any> = PropContributorCallbacks<FormPropList<R>>; |
|||
export type EditFormPropDefaults<R = any> = Record<string, FormProp<R>[]>; |
|||
export type EditFormPropContributorCallback<R = any> = PropContributorCallback<FormPropList<R>>; |
|||
export type EditFormPropContributorCallbacks<R = any> = PropContributorCallbacks<FormPropList<R>>; |
|||
|
|||
function isFalsyValue(defaultValue: FormProp['defaultValue']): boolean { |
|||
return [0, '', false].indexOf(defaultValue as any) > -1; |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
import { ePropType } from '../../enums/props.enum'; |
|||
import { EntityPropList } from '../entity-props'; |
|||
import { FormPropList } from '../form-props'; |
|||
import { PropContributorCallbacks } from '../props'; |
|||
|
|||
export interface PropContributors<T = any> { |
|||
prop: PropContributorCallbacks<EntityPropList<T>>; |
|||
createForm: PropContributorCallbacks<FormPropList<T>>; |
|||
editForm: PropContributorCallbacks<FormPropList<T>>; |
|||
} |
|||
|
|||
export interface Config { |
|||
objectExtensions: Item; |
|||
} |
|||
|
|||
export interface Item { |
|||
modules: Modules; |
|||
enums: Enums; |
|||
} |
|||
|
|||
export type Modules = Configuration<Module>; |
|||
|
|||
export interface Module { |
|||
configuration: Configuration; |
|||
entities: Entities; |
|||
} |
|||
|
|||
export type Entities = Configuration<Entity>; |
|||
|
|||
export interface Entity { |
|||
configuration: Configuration; |
|||
properties: Properties; |
|||
} |
|||
|
|||
export type Properties = Configuration<Property>; |
|||
|
|||
export interface Property { |
|||
type: string; |
|||
typeSimple: ePropType; |
|||
displayName: DisplayName | null; |
|||
api: Api; |
|||
ui: Ui; |
|||
attributes: Attribute[]; |
|||
configuration: Configuration; |
|||
defaultValue?: any; |
|||
} |
|||
|
|||
export interface DisplayName { |
|||
name: string; |
|||
resource: string; |
|||
} |
|||
|
|||
export interface Api { |
|||
onGet: ApiConfig; |
|||
onCreate: ApiConfig; |
|||
onUpdate: ApiConfig; |
|||
} |
|||
|
|||
export interface ApiConfig { |
|||
isAvailable: boolean; |
|||
} |
|||
|
|||
export interface Ui { |
|||
onTable: UiPropConfig; |
|||
onCreateForm: UiFormConfig; |
|||
onEditForm: UiFormConfig; |
|||
} |
|||
|
|||
export interface UiPropConfig { |
|||
isSortable: boolean; |
|||
isVisible: boolean; |
|||
} |
|||
|
|||
export interface UiFormConfig { |
|||
isVisible: boolean; |
|||
} |
|||
|
|||
export interface Attribute { |
|||
typeSimple: string; |
|||
config: Configuration; |
|||
} |
|||
|
|||
export type Configuration<T = any> = Record<string, T>; |
|||
|
|||
export type Enums = Record<string, Enum>; |
|||
|
|||
export interface Enum { |
|||
fields: EnumMember[]; |
|||
localizationResource?: string; |
|||
transformed?: any; |
|||
} |
|||
|
|||
export interface EnumMember { |
|||
name: string; |
|||
value: any; |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
import * as ObjectExtensions from './internal/object-extensions'; |
|||
|
|||
export { ObjectExtensions }; |
|||
@ -0,0 +1,85 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { InjectFlags, InjectionToken, Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
|
|||
export abstract class PropList<R = any, A = Prop<R>> extends LinkedList<A> {} |
|||
|
|||
export abstract class PropData<R = any> { |
|||
abstract getInjected: <T>( |
|||
token: Type<T> | InjectionToken<T>, |
|||
notFoundValue?: T, |
|||
flags?: InjectFlags, |
|||
) => T; |
|||
index?: number; |
|||
abstract record: R; |
|||
|
|||
get data(): ReadonlyPropData<R> { |
|||
return { |
|||
getInjected: this.getInjected, |
|||
index: this.index, |
|||
record: this.record, |
|||
}; |
|||
} |
|||
} |
|||
|
|||
export type ReadonlyPropData<R = any> = O.Readonly<Omit<PropData<R>, 'data'>>; |
|||
|
|||
export abstract class Prop<R = any> { |
|||
constructor( |
|||
public readonly type: ePropType, |
|||
public readonly name: string, |
|||
public readonly displayName: string, |
|||
public readonly permission: string, |
|||
public readonly visible: PropPredicate<R> = _ => true, |
|||
public readonly isExtra = false, |
|||
) { |
|||
this.displayName = this.displayName || this.name; |
|||
} |
|||
} |
|||
|
|||
export type PropCallback<T, R = any> = (data?: Omit<PropData<T>, 'data'>) => R; |
|||
export type PropPredicate<T> = (data?: Omit<PropData<T>, 'data'>) => boolean; |
|||
|
|||
export abstract class PropsFactory<C extends Props<any>> { |
|||
protected abstract _ctor: Type<C>; |
|||
private contributorCallbacks: PropContributorCallbacks<InferredPropList<C>> = {}; |
|||
|
|||
get(name: string): C { |
|||
this.contributorCallbacks[name] = this.contributorCallbacks[name] || []; |
|||
|
|||
return new this._ctor(this.contributorCallbacks[name]); |
|||
} |
|||
} |
|||
|
|||
export abstract class Props<L extends PropList> { |
|||
protected abstract _ctor: Type<L>; |
|||
|
|||
get props(): L { |
|||
const propList = new this._ctor(); |
|||
|
|||
this.callbackList.forEach(callback => callback(propList)); |
|||
|
|||
return propList; |
|||
} |
|||
|
|||
constructor(private readonly callbackList: PropContributorCallback<L>[]) {} |
|||
|
|||
addContributor(contributeCallback: PropContributorCallback<L>) { |
|||
this.callbackList.push(contributeCallback); |
|||
} |
|||
|
|||
clearContributors() { |
|||
while (this.callbackList.length) this.callbackList.pop(); |
|||
} |
|||
} |
|||
|
|||
export type PropContributorCallbacks<L extends PropList<any>> = Record< |
|||
string, |
|||
PropContributorCallback<L>[] |
|||
>; |
|||
|
|||
export type PropContributorCallback<L extends PropList<any>> = (propList: L) => any; |
|||
|
|||
type InferredPropList<C> = C extends Props<infer L> ? L : never; |
|||
@ -0,0 +1,78 @@ |
|||
/* tslint:disable:variable-name */ |
|||
import { Type } from '@angular/core'; |
|||
import { O } from 'ts-toolbelt'; |
|||
import { |
|||
Action, |
|||
ActionContributorCallback, |
|||
ActionContributorCallbacks, |
|||
ActionList, |
|||
Actions, |
|||
ActionsFactory, |
|||
} from './actions'; |
|||
|
|||
export class ToolbarActionList<R = any> extends ActionList< |
|||
R, |
|||
ToolbarAction<R> | ToolbarComponent<R> |
|||
> {} |
|||
|
|||
export class ToolbarActions<R = any> extends Actions<ToolbarActionList<R>> { |
|||
protected _ctor: Type<ToolbarActionList<R>> = ToolbarActionList; |
|||
} |
|||
|
|||
export class ToolbarActionsFactory<R = any> extends ActionsFactory<ToolbarActions<R>> { |
|||
protected _ctor: Type<ToolbarActions<R>> = ToolbarActions; |
|||
} |
|||
|
|||
export class ToolbarAction<R = any> extends Action<R> { |
|||
readonly text: string; |
|||
readonly icon: string; |
|||
|
|||
constructor(options: ToolbarActionOptions<R>) { |
|||
super(options.permission, options.visible, options.action); |
|||
this.text = options.text; |
|||
this.icon = options.icon || ''; |
|||
} |
|||
|
|||
static create<R = any>(options: ToolbarActionOptions<R>) { |
|||
return new ToolbarAction<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: ToolbarActionOptions<R>[]) { |
|||
return arrayOfOptions.map(ToolbarAction.create); |
|||
} |
|||
} |
|||
|
|||
export class ToolbarComponent<R = any> extends Action<R> { |
|||
readonly component: Type<any>; |
|||
|
|||
constructor(options: ToolbarComponentOptions<R>) { |
|||
super(options.permission, options.visible, options.action); |
|||
this.component = options.component; |
|||
} |
|||
|
|||
static create<R = any>(options: ToolbarComponentOptions<R>) { |
|||
return new ToolbarComponent<R>(options); |
|||
} |
|||
|
|||
static createMany<R = any>(arrayOfOptions: ToolbarComponentOptions<R>[]) { |
|||
return arrayOfOptions.map(ToolbarComponent.create); |
|||
} |
|||
} |
|||
|
|||
export type ToolbarActionOptions<R = any> = O.Optional< |
|||
O.Writable<ToolbarAction<R>>, |
|||
'permission' | 'visible' | 'icon' |
|||
>; |
|||
|
|||
export type ToolbarComponentOptions<R = any> = O.Optional< |
|||
O.Writable<ToolbarComponent<R>>, |
|||
'permission' | 'visible' | 'action' |
|||
>; |
|||
|
|||
export type ToolbarActionDefaults<R = any> = Record<string, ToolbarAction<R>[]>; |
|||
export type ToolbarActionContributorCallback<R = any> = ActionContributorCallback< |
|||
ToolbarActionList<R> |
|||
>; |
|||
export type ToolbarActionContributorCallbacks<R = any> = ActionContributorCallbacks< |
|||
ToolbarActionList<R> |
|||
>; |
|||
@ -0,0 +1,16 @@ |
|||
import { Injectable } from '@angular/core'; |
|||
import { EntityActionsFactory } from '../models/entity-actions'; |
|||
import { EntityPropsFactory } from '../models/entity-props'; |
|||
import { CreateFormPropsFactory, EditFormPropsFactory } from '../models/form-props'; |
|||
import { ToolbarActionsFactory } from '../models/toolbar-actions'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class ExtensionsService<R = any> { |
|||
readonly entityActions = new EntityActionsFactory<R>(); |
|||
readonly toolbarActions = new ToolbarActionsFactory<R[]>(); |
|||
readonly entityProps = new EntityPropsFactory<R>(); |
|||
readonly createFormProps = new CreateFormPropsFactory<R>(); |
|||
readonly editFormProps = new EditFormPropsFactory<R>(); |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
import { InjectionToken } from '@angular/core'; |
|||
import { ActionCallback, ReadonlyActionData as ActionData } from '../models/actions'; |
|||
import { ExtensionsService } from '../services/extensions.service'; |
|||
|
|||
export const EXTENSIONS_IDENTIFIER = new InjectionToken<string>('EXTENSIONS_IDENTIFIER'); |
|||
export type ActionKeys = Extract<'entityActions' | 'toolbarActions', keyof ExtensionsService>; |
|||
|
|||
export const EXTENSIONS_ACTION_TYPE = new InjectionToken<ActionKeys>('EXTENSIONS_ACTION_TYPE'); |
|||
|
|||
export const EXTENSIONS_ACTION_DATA = new InjectionToken<ActionData>('EXTENSIONS_ACTION_DATA'); |
|||
export const EXTENSIONS_ACTION_CALLBACK = new InjectionToken<ActionCallback<unknown>>( |
|||
'EXTENSIONS_ACTION_DATA', |
|||
); |
|||
@ -0,0 +1,48 @@ |
|||
import { CoreModule } from '@abp/ng.core'; |
|||
import { ThemeSharedModule } from '@abp/ng.theme.shared'; |
|||
import { NgModule } from '@angular/core'; |
|||
import { |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
NgbTimepickerModule, |
|||
} from '@ng-bootstrap/ng-bootstrap'; |
|||
import { NgxValidateCoreModule } from '@ngx-validate/core'; |
|||
import { DateTimePickerComponent } from './components/date-time-picker/date-time-picker.component'; |
|||
import { ExtensibleFormPropComponent } from './components/extensible-form/extensible-form-prop.component'; |
|||
import { ExtensibleFormComponent } from './components/extensible-form/extensible-form.component'; |
|||
import { ExtensibleTableComponent } from './components/extensible-table/extensible-table.component'; |
|||
import { GridActionsComponent } from './components/grid-actions/grid-actions.component'; |
|||
import { PageToolbarComponent } from './components/page-toolbar/page-toolbar.component'; |
|||
import { DisabledDirective } from './directives/disabled.directive'; |
|||
import { PropDataDirective } from './directives/prop-data.directive'; |
|||
|
|||
@NgModule({ |
|||
exports: [ |
|||
DateTimePickerComponent, |
|||
PageToolbarComponent, |
|||
GridActionsComponent, |
|||
ExtensibleFormComponent, |
|||
ExtensibleTableComponent, |
|||
PropDataDirective, |
|||
DisabledDirective, |
|||
], |
|||
declarations: [ |
|||
DateTimePickerComponent, |
|||
PageToolbarComponent, |
|||
GridActionsComponent, |
|||
ExtensibleFormPropComponent, |
|||
ExtensibleFormComponent, |
|||
ExtensibleTableComponent, |
|||
PropDataDirective, |
|||
DisabledDirective, |
|||
], |
|||
imports: [ |
|||
CoreModule, |
|||
ThemeSharedModule, |
|||
NgxValidateCoreModule, |
|||
NgbDatepickerModule, |
|||
NgbDropdownModule, |
|||
NgbTimepickerModule, |
|||
], |
|||
}) |
|||
export class UiExtensionsModule {} |
|||
@ -0,0 +1,49 @@ |
|||
import { ActionContributorCallback, ActionList, ActionsFactory } from '../models/actions'; |
|||
import { |
|||
EntityActionContributorCallbacks, |
|||
EntityActionDefaults, |
|||
EntityActions, |
|||
EntityActionsFactory, |
|||
} from '../models/entity-actions'; |
|||
import { |
|||
ToolbarActionContributorCallbacks, |
|||
ToolbarActionDefaults, |
|||
ToolbarActions, |
|||
ToolbarActionsFactory, |
|||
} from '../models/toolbar-actions'; |
|||
|
|||
export function mergeWithDefaultActions<F extends ActionsFactory<any>>( |
|||
extension: F, |
|||
defaultActions: InferredActionDefaults<F>, |
|||
...contributors: InferredActionContributorCallbacks<F>[] |
|||
) { |
|||
Object.keys(defaultActions).forEach((name: string) => { |
|||
const actions: InferredActions<F> = extension.get(name); |
|||
actions.clearContributors(); |
|||
actions.addContributor((actionList: ActionList) => |
|||
actionList.addManyTail(defaultActions[name]), |
|||
); |
|||
contributors.forEach(contributor => |
|||
(contributor[name] || []).forEach((callback: ActionContributorCallback<any>) => |
|||
actions.addContributor(callback), |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
type InferredActionDefaults<F> = F extends EntityActionsFactory<infer RE> |
|||
? EntityActionDefaults<RE> |
|||
: F extends ToolbarActionsFactory<infer RT> |
|||
? ToolbarActionDefaults<RT> |
|||
: never; |
|||
|
|||
type InferredActionContributorCallbacks<F> = F extends EntityActionsFactory<infer RE> |
|||
? EntityActionContributorCallbacks<RE> |
|||
: F extends ToolbarActionsFactory<infer RT> |
|||
? ToolbarActionContributorCallbacks<RT> |
|||
: never; |
|||
|
|||
type InferredActions<F> = F extends EntityActionsFactory<infer RE> |
|||
? EntityActions<RE> |
|||
: F extends ToolbarActionsFactory<infer RT> |
|||
? ToolbarActions<RT> |
|||
: never; |
|||
@ -0,0 +1,73 @@ |
|||
import { ABP, LocalizationService } from '@abp/ng.core'; |
|||
import { merge, Observable, of } from 'rxjs'; |
|||
import { map } from 'rxjs/operators'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
import { PropCallback } from '../models/props'; |
|||
|
|||
export function createEnum(members: ObjectExtensions.EnumMember[]) { |
|||
const enumObject: any = {}; |
|||
|
|||
members.forEach(({ name, value }) => { |
|||
enumObject[(enumObject[name] = value)] = name; |
|||
}); |
|||
|
|||
return enumObject; |
|||
} |
|||
|
|||
export function createEnumValueResolver<T = any>( |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.Enum, |
|||
propName: string, |
|||
): PropCallback<T, Observable<string>> { |
|||
return data => { |
|||
const value = data.record[EXTRA_PROPERTIES_KEY][propName]; |
|||
const key = lookupEnum.transformed[value]; |
|||
const l10n = data.getInjected(LocalizationService); |
|||
const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum); |
|||
|
|||
return createLocalizationStream(l10n, localizeEnum(key)); |
|||
}; |
|||
} |
|||
|
|||
export function createEnumOptions<T = any>( |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.Enum, |
|||
): PropCallback<T, Observable<ABP.Option<any>[]>> { |
|||
return data => { |
|||
const l10n = data.getInjected(LocalizationService); |
|||
const localizeEnum = createEnumLocalizer(l10n, enumType, lookupEnum); |
|||
|
|||
return createLocalizationStream( |
|||
l10n, |
|||
lookupEnum.fields.map(({ name, value }) => ({ |
|||
key: localizeEnum(name), |
|||
value, |
|||
})), |
|||
); |
|||
}; |
|||
} |
|||
|
|||
function createLocalizationStream(l10n: LocalizationService, mapTarget: any) { |
|||
return merge(of(null), l10n.languageChange).pipe(map(() => mapTarget)); |
|||
} |
|||
|
|||
function createEnumLocalizer( |
|||
l10n: LocalizationService, |
|||
enumType: string, |
|||
lookupEnum: ObjectExtensions.Enum, |
|||
): (key: string) => string { |
|||
const resource = lookupEnum.localizationResource; |
|||
const shortType = getShortEnumType(enumType); |
|||
|
|||
return key => |
|||
l10n.localizeWithFallbackSync( |
|||
[resource], |
|||
['Enum:' + shortType + '.' + key, shortType + '.' + key, key], |
|||
key, |
|||
); |
|||
} |
|||
|
|||
function getShortEnumType(enumType: string): string { |
|||
return enumType.split('.').pop(); |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export function selfFactory(dependency?: any) { |
|||
return dependency; |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
import { FormControl, FormGroup } from '@angular/forms'; |
|||
import { DateTimeAdapter } from '../adapters/date-time.adapter'; |
|||
import { DateAdapter } from '../adapters/date.adapter'; |
|||
import { TimeAdapter } from '../adapters/time.adapter'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
import { FormPropList } from '../models/form-props'; |
|||
import { PropData } from '../models/props'; |
|||
import { ExtensionsService } from '../services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../tokens/extensions.token'; |
|||
|
|||
export function generateFormFromProps<R extends any>(data: PropData<R>) { |
|||
const extensions = data.getInjected(ExtensionsService); |
|||
const identifier = data.getInjected(EXTENSIONS_IDENTIFIER); |
|||
|
|||
const form = new FormGroup({}); |
|||
const extraForm = new FormGroup({}); |
|||
form.addControl(EXTRA_PROPERTIES_KEY, extraForm); |
|||
|
|||
const record = data.record || {}; |
|||
const type = JSON.stringify(record) === '{}' ? 'create' : 'edit'; |
|||
const props: FormPropList<R> = extensions[`${type}FormProps`].get(identifier).props; |
|||
const extraProperties = record[EXTRA_PROPERTIES_KEY] || {}; |
|||
|
|||
props.forEach(({ value: prop }) => { |
|||
const name = prop.name; |
|||
const isExtraProperty = prop.isExtra || name in extraProperties; |
|||
let value = isExtraProperty ? extraProperties[name] : name in record ? record[name] : undefined; |
|||
|
|||
if (typeof value === 'undefined') value = prop.defaultValue; |
|||
|
|||
if (value) { |
|||
let adapter: DateAdapter | TimeAdapter | DateTimeAdapter; |
|||
switch (prop.type) { |
|||
case ePropType.Date: |
|||
adapter = new DateAdapter(); |
|||
value = adapter.toModel(adapter.fromModel(value)); |
|||
break; |
|||
case ePropType.Time: |
|||
adapter = new TimeAdapter(); |
|||
value = adapter.toModel(adapter.fromModel(value)); |
|||
break; |
|||
case ePropType.DateTime: |
|||
adapter = new DateTimeAdapter(); |
|||
value = adapter.toModel(adapter.fromModel(value) as any); |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
} |
|||
|
|||
const formControl = new FormControl(value, { |
|||
asyncValidators: prop.asyncValidators(data), |
|||
validators: prop.validators(data), |
|||
}); |
|||
|
|||
(isExtraProperty ? extraForm : form).addControl(name, formControl); |
|||
}); |
|||
|
|||
return form; |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
import { ApplicationConfiguration, createLocalizationPipeKeyGenerator } from '@abp/ng.core'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
|
|||
export function createDisplayNameLocalizationPipeKeyGenerator( |
|||
localization: ApplicationConfiguration.Localization, |
|||
) { |
|||
const generateLocalizationPipeKey = createLocalizationPipeKeyGenerator(localization); |
|||
|
|||
return (displayName: ObjectExtensions.DisplayName, fallback: ObjectExtensions.DisplayName) => { |
|||
if (displayName && displayName.name) |
|||
return generateLocalizationPipeKey( |
|||
[displayName.resource], |
|||
[displayName.name], |
|||
displayName.name, |
|||
); |
|||
|
|||
const key = generateLocalizationPipeKey( |
|||
[fallback.resource], |
|||
['DisplayName:' + fallback.name], |
|||
undefined, |
|||
); |
|||
|
|||
if (key) return key; |
|||
|
|||
return generateLocalizationPipeKey([fallback.resource], [fallback.name], fallback.name); |
|||
}; |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
import { of } from 'rxjs'; |
|||
import { EXTRA_PROPERTIES_KEY } from '../constants/extra-properties'; |
|||
import { |
|||
EntityPropContributorCallbacks, |
|||
EntityPropDefaults, |
|||
EntityProps, |
|||
EntityPropsFactory, |
|||
} from '../models/entity-props'; |
|||
import { |
|||
CreateFormPropContributorCallbacks, |
|||
CreateFormPropDefaults, |
|||
CreateFormPropsFactory, |
|||
EditFormPropContributorCallbacks, |
|||
EditFormPropDefaults, |
|||
EditFormPropsFactory, |
|||
FormProps, |
|||
} from '../models/form-props'; |
|||
import { PropContributorCallback, PropData, PropList, PropsFactory } from '../models/props'; |
|||
|
|||
export function createExtraPropertyValueResolver<T>(name: string) { |
|||
return (data?: PropData<T>) => of(data.record[EXTRA_PROPERTIES_KEY][name]); |
|||
} |
|||
|
|||
export function mergeWithDefaultProps<F extends PropsFactory<any>>( |
|||
extension: F, |
|||
defaultProps: InferredPropDefaults<F>, |
|||
...contributors: InferredPropContributorCallbacks<F>[] |
|||
) { |
|||
Object.keys(defaultProps).forEach((name: string) => { |
|||
const props: InferredProps<F> = extension.get(name); |
|||
props.clearContributors(); |
|||
props.addContributor((propList: PropList) => propList.addManyTail(defaultProps[name])); |
|||
contributors.forEach(contributor => |
|||
(contributor[name] || []).forEach((callback: PropContributorCallback<any>) => |
|||
props.addContributor(callback), |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
type InferredPropDefaults<F> = F extends EntityPropsFactory<infer RE> |
|||
? EntityPropDefaults<RE> |
|||
: F extends CreateFormPropsFactory<infer RCF> |
|||
? CreateFormPropDefaults<RCF> |
|||
: F extends EditFormPropsFactory<infer REF> |
|||
? EditFormPropDefaults<REF> |
|||
: never; |
|||
|
|||
type InferredPropContributorCallbacks<F> = F extends EntityPropsFactory<infer RE> |
|||
? EntityPropContributorCallbacks<RE> |
|||
: F extends CreateFormPropsFactory<infer RCF> |
|||
? CreateFormPropContributorCallbacks<RCF> |
|||
: F extends EditFormPropsFactory<infer REF> |
|||
? EditFormPropContributorCallbacks<REF> |
|||
: never; |
|||
|
|||
type InferredProps<F> = F extends EntityPropsFactory<infer RE> |
|||
? EntityProps<RE> |
|||
: F extends CreateFormPropsFactory<infer RCF> |
|||
? FormProps<RCF> |
|||
: F extends EditFormPropsFactory<infer REF> |
|||
? FormProps<REF> |
|||
: never; |
|||
@ -0,0 +1,174 @@ |
|||
import { ABP, ApplicationConfiguration } from '@abp/ng.core'; |
|||
import { createSelector, Store } from '@ngxs/store'; |
|||
import { Observable, pipe, zip } from 'rxjs'; |
|||
import { filter, map, switchMap, take } from 'rxjs/operators'; |
|||
import { ePropType } from '../enums/props.enum'; |
|||
import { EntityProp, EntityPropList } from '../models/entity-props'; |
|||
import { FormProp, FormPropList } from '../models/form-props'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
import { PropCallback } from '../models/props'; |
|||
import { createEnum, createEnumOptions, createEnumValueResolver } from './enum.util'; |
|||
import { createDisplayNameLocalizationPipeKeyGenerator } from './localization.util'; |
|||
import { createExtraPropertyValueResolver } from './props.util'; |
|||
import { getValidatorsFromProperty } from './validation.util'; |
|||
|
|||
const selectConfig = (state: any) => state.ConfigState; |
|||
|
|||
const selectObjectExtensions = createSelector( |
|||
[selectConfig], |
|||
(config: ObjectExtensions.Config) => config.objectExtensions, |
|||
); |
|||
|
|||
const selectLocalization = createSelector( |
|||
[selectConfig], |
|||
(config: ApplicationConfiguration.Response) => config.localization, |
|||
); |
|||
|
|||
const selectEnums = createSelector( |
|||
[selectObjectExtensions, selectLocalization], |
|||
(extensions: ObjectExtensions.Item) => |
|||
Object.keys(extensions.enums).reduce((acc, key) => { |
|||
const { fields, localizationResource } = extensions.enums[key]; |
|||
acc[key] = { |
|||
fields, |
|||
localizationResource, |
|||
transformed: createEnum(fields), |
|||
}; |
|||
return acc; |
|||
}, {} as ObjectExtensions.Enums), |
|||
); |
|||
|
|||
const createObjectExtensionEntitiesSelector = (moduleKey: ModuleKey) => |
|||
createSelector([selectObjectExtensions], (extensions: ObjectExtensions.Item) => { |
|||
if (!extensions) return null; |
|||
|
|||
return (extensions.modules[moduleKey] || ({} as ObjectExtensions.Module)).entities; |
|||
}); |
|||
|
|||
export function getObjectExtensionEntitiesFromStore(store: Store, moduleKey: ModuleKey) { |
|||
return store.select(createObjectExtensionEntitiesSelector(moduleKey)).pipe( |
|||
map(entities => (isUndefined(entities) ? {} : entities)), |
|||
filter<ObjectExtensions.Entities>(Boolean), |
|||
take(1), |
|||
); |
|||
} |
|||
|
|||
export function mapEntitiesToContributors<T = any>(store: Store, resource: string) { |
|||
return pipe( |
|||
switchMap(entities => |
|||
zip(store.select(selectLocalization), store.select(selectEnums)).pipe( |
|||
map(([localization, enums]) => { |
|||
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator(localization); |
|||
|
|||
return Object.keys(entities).reduce( |
|||
(acc, key: keyof ObjectExtensions.Entities) => { |
|||
acc.prop[key] = []; |
|||
acc.createForm[key] = []; |
|||
acc.editForm[key] = []; |
|||
|
|||
const entity: ObjectExtensions.Entity = entities[key]; |
|||
if (!entity) return acc; |
|||
|
|||
const properties = entity.properties; |
|||
if (!properties) return acc; |
|||
|
|||
const mapPropertiesToContributors = createPropertiesToContributorsMapper<T>( |
|||
generateDisplayName, |
|||
resource, |
|||
enums, |
|||
); |
|||
|
|||
return mapPropertiesToContributors(properties, acc, key); |
|||
}, |
|||
{ |
|||
prop: {}, |
|||
createForm: {}, |
|||
editForm: {}, |
|||
} as ObjectExtensions.PropContributors, |
|||
); |
|||
}), |
|||
), |
|||
), |
|||
take(1), |
|||
); |
|||
} |
|||
|
|||
function createPropertiesToContributorsMapper<T = any>( |
|||
generateDisplayName: DisplayNameGeneratorFn, |
|||
resource: string, |
|||
enums: Record<string, ObjectExtensions.Enum>, |
|||
) { |
|||
return ( |
|||
properties: Record<string, ObjectExtensions.Property>, |
|||
contributors: ObjectExtensions.PropContributors<T>, |
|||
key: string, |
|||
) => { |
|||
const isExtra = true; |
|||
|
|||
Object.keys(properties).forEach((name: string) => { |
|||
const property = properties[name]; |
|||
const type = getTypeFromProperty(property); |
|||
const displayName = generateDisplayName(property.displayName, { name, resource }); |
|||
|
|||
if (property.ui.onTable.isVisible) { |
|||
const sortable = Boolean(property.ui.onTable.isSortable); |
|||
const columnWidth = type === ePropType.Boolean ? 150 : 250; |
|||
const valueResolver = |
|||
type === ePropType.Enum |
|||
? createEnumValueResolver(property.type, enums[property.type], name) |
|||
: createExtraPropertyValueResolver<T>(name); |
|||
|
|||
const entityProp = new EntityProp<T>({ |
|||
type, |
|||
name, |
|||
displayName, |
|||
sortable, |
|||
columnWidth, |
|||
valueResolver, |
|||
isExtra, |
|||
}); |
|||
|
|||
const contributor = (propList: EntityPropList<T>) => propList.addTail(entityProp); |
|||
contributors.prop[key].push(contributor); |
|||
} |
|||
|
|||
const isOnCreateForm = property.ui.onCreateForm.isVisible; |
|||
const isOnEditForm = property.ui.onEditForm.isVisible; |
|||
|
|||
if (isOnCreateForm || isOnEditForm) { |
|||
const defaultValue = property.defaultValue; |
|||
const validators = () => getValidatorsFromProperty(property); |
|||
let options: PropCallback<any, Observable<ABP.Option<any>[]>>; |
|||
if (type === ePropType.Enum) options = createEnumOptions(name, enums[property.type]); |
|||
|
|||
const formProp = new FormProp({ |
|||
type, |
|||
name, |
|||
displayName, |
|||
options, |
|||
defaultValue, |
|||
validators, |
|||
isExtra, |
|||
}); |
|||
|
|||
const formContributor = (propList: FormPropList<T>) => propList.addTail(formProp); |
|||
|
|||
if (isOnCreateForm) contributors.createForm[key].push(formContributor); |
|||
if (isOnEditForm) contributors.editForm[key].push(formContributor); |
|||
} |
|||
}); |
|||
|
|||
return contributors; |
|||
}; |
|||
} |
|||
|
|||
function getTypeFromProperty(property: ObjectExtensions.Property): ePropType { |
|||
return (property.typeSimple.replace(/\?$/, '') as string) as ePropType; |
|||
} |
|||
|
|||
function isUndefined(obj: any): obj is undefined { |
|||
return typeof obj === 'undefined'; |
|||
} |
|||
|
|||
type DisplayNameGeneratorFn = ReturnType<typeof createDisplayNameLocalizationPipeKeyGenerator>; |
|||
type ModuleKey = keyof ObjectExtensions.Modules; |
|||
@ -0,0 +1,14 @@ |
|||
import { AbpValidators } from '@abp/ng.core'; |
|||
import { ValidatorFn } from '@angular/forms'; |
|||
import { ObjectExtensions } from '../models/object-extensions'; |
|||
|
|||
export function getValidatorsFromProperty(property: ObjectExtensions.Property): ValidatorFn[] { |
|||
const validators: ValidatorFn[] = []; |
|||
|
|||
property.attributes.forEach(attr => { |
|||
if (attr.typeSimple in AbpValidators) |
|||
validators.push(AbpValidators[attr.typeSimple](attr.config)); |
|||
}); |
|||
|
|||
return validators; |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
export * from './lib/adapters/date-time.adapter'; |
|||
export * from './lib/adapters/date.adapter'; |
|||
export * from './lib/adapters/time.adapter'; |
|||
export * from './lib/components/date-time-picker/date-time-picker.component'; |
|||
export * from './lib/components/extensible-form/extensible-form-prop.component'; |
|||
export * from './lib/components/extensible-form/extensible-form.component'; |
|||
export * from './lib/components/extensible-table/extensible-table.component'; |
|||
export * from './lib/components/grid-actions/grid-actions.component'; |
|||
export * from './lib/components/page-toolbar/page-toolbar.component'; |
|||
export * from './lib/constants/extra-properties'; |
|||
export * from './lib/directives/disabled.directive'; |
|||
export * from './lib/directives/prop-data.directive'; |
|||
export * from './lib/enums/props.enum'; |
|||
export { |
|||
ActionCallback, |
|||
ActionList, |
|||
ActionPredicate, |
|||
ReadonlyActionData as ActionData, |
|||
} from './lib/models/actions'; |
|||
export { |
|||
EntityAction, |
|||
EntityActionContributorCallback, |
|||
EntityActionList, |
|||
EntityActionOptions, |
|||
EntityActions, |
|||
EntityActionsFactory, |
|||
} from './lib/models/entity-actions'; |
|||
export { |
|||
EntityProp, |
|||
EntityPropContributorCallback, |
|||
EntityPropList, |
|||
EntityPropOptions, |
|||
EntityProps, |
|||
EntityPropsFactory, |
|||
} from './lib/models/entity-props'; |
|||
export { |
|||
CreateFormPropContributorCallback, |
|||
CreateFormPropsFactory, |
|||
EditFormPropContributorCallback, |
|||
EditFormPropsFactory, |
|||
FormProp, |
|||
FormPropData, |
|||
FormPropList, |
|||
FormPropOptions, |
|||
FormProps, |
|||
} from './lib/models/form-props'; |
|||
export * from './lib/models/object-extensions'; |
|||
export { |
|||
PropCallback, |
|||
PropList, |
|||
PropPredicate, |
|||
ReadonlyPropData as PropData, |
|||
} from './lib/models/props'; |
|||
export { |
|||
ToolbarAction, |
|||
ToolbarActionContributorCallback, |
|||
ToolbarActionList, |
|||
ToolbarActionOptions, |
|||
ToolbarActions, |
|||
ToolbarActionsFactory, |
|||
ToolbarComponent, |
|||
ToolbarComponentOptions, |
|||
} from './lib/models/toolbar-actions'; |
|||
export * from './lib/services/extensions.service'; |
|||
export * from './lib/tokens/extensions.token'; |
|||
export * from './lib/ui-extensions.module'; |
|||
export * from './lib/utils/actions.util'; |
|||
export * from './lib/utils/form-props.util'; |
|||
export * from './lib/utils/props.util'; |
|||
export * from './lib/utils/state.util'; |
|||
@ -0,0 +1,22 @@ |
|||
import { ActionData } from '../lib/models/actions'; |
|||
|
|||
describe('ActionData', () => { |
|||
describe('#data', () => { |
|||
it('should return record and getInjected', () => { |
|||
const spy = jest.fn(); |
|||
class Data extends ActionData<string> { |
|||
index = 0; |
|||
record = 'X'; |
|||
getInjected = spy; |
|||
} |
|||
|
|||
const data = new Data(); |
|||
data.data.getInjected(null); |
|||
|
|||
expect(spy).toHaveBeenCalledTimes(1); |
|||
expect(spy).toHaveBeenCalledWith(null); |
|||
expect(data.data.index).toBe(0); |
|||
expect(data.data.record).toBe('X'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,53 @@ |
|||
import { |
|||
EntityAction, |
|||
EntityActionContributorCallbacks, |
|||
EntityActionDefaults, |
|||
EntityActionsFactory, |
|||
} from '../lib/models/entity-actions'; |
|||
import { mergeWithDefaultActions } from '../lib/utils/actions.util'; |
|||
|
|||
describe('Entity Action Utils', () => { |
|||
describe('#mergeEntityActions', () => { |
|||
let entityActions: EntityActionsFactory; |
|||
|
|||
beforeEach(() => { |
|||
entityActions = new EntityActionsFactory(); |
|||
}); |
|||
|
|||
it('should merge default actions with action contributors', () => { |
|||
const defaults: EntityActionDefaults = { |
|||
x: [(1 as any) as EntityAction, (2 as any) as EntityAction, (3 as any) as EntityAction], |
|||
y: [(1 as any) as EntityAction, (2 as any) as EntityAction, (3 as any) as EntityAction], |
|||
}; |
|||
|
|||
const contributors1: EntityActionContributorCallbacks = { |
|||
x: [ |
|||
actionList => { |
|||
const x2 = actionList.dropByIndex(1); // 1 <-> 3
|
|||
actionList.addHead(x2.value); // 2 <-> 1 <-> 3
|
|||
}, |
|||
actionList => { |
|||
actionList.dropTail(); // 2 <-> 1
|
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const contributors2: EntityActionContributorCallbacks = { |
|||
y: [ |
|||
actionList => { |
|||
const y2 = actionList.dropByIndex(1); // 1 <-> 3
|
|||
actionList.addTail(y2.value); // 1 <-> 3 <-> 2
|
|||
}, |
|||
actionList => { |
|||
actionList.dropHead(); // 3 <-> 2
|
|||
}, |
|||
], |
|||
}; |
|||
|
|||
mergeWithDefaultActions(entityActions, defaults, contributors1, contributors2); |
|||
|
|||
expect(entityActions.get('x').actions.toString()).toBe('2 <-> 1'); |
|||
expect(entityActions.get('y').actions.toString()).toBe('3 <-> 2'); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,35 @@ |
|||
import { DateTimeAdapter } from '../lib/adapters/date-time.adapter'; |
|||
|
|||
describe('DateTime Adapter', () => { |
|||
const adapter = new DateTimeAdapter(); |
|||
const date = new Date(2002, 2, 30, 13, 30, 45, 0); |
|||
const year = date.getFullYear(); |
|||
const month = date.getMonth() + 1; |
|||
const day = date.getDate(); |
|||
const hour = date.getHours(); |
|||
const minute = date.getMinutes(); |
|||
const second = date.getSeconds(); |
|||
|
|||
describe('#fromModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${null} |
|||
${null} | ${null} |
|||
${'x'} | ${null} |
|||
${date} | ${{ year, month, day, hour, minute, second }} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.fromModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${''} |
|||
${null} | ${''} |
|||
${{ year, month, day, hour, minute, second }} | ${date.toISOString()} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.toModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,31 @@ |
|||
import { DateAdapter } from '../lib/adapters/date.adapter'; |
|||
|
|||
describe('Date Adapter', () => { |
|||
const adapter = new DateAdapter(); |
|||
|
|||
describe('#fromModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${null} |
|||
${null} | ${null} |
|||
${'x'} | ${null} |
|||
${'2002-03-30'} | ${{ day: 30, month: 3, year: 2002 }} |
|||
${'03/30/2002'} | ${{ day: 30, month: 3, year: 2002 }} |
|||
${new Date(0)} | ${{ day: 1, month: 1, year: 1970 }} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.fromModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toModel', () => { |
|||
test.each` |
|||
param | expected |
|||
${undefined} | ${''} |
|||
${null} | ${''} |
|||
${{ day: 30, month: 3, year: 2002 }} | ${'2002-03-30'} |
|||
${{ day: 1, month: 1, year: 1970 }} | ${'1970-01-01'} |
|||
`('should return $expected when $param is given', ({ param, expected }) => {
|
|||
expect(adapter.toModel(param)).toEqual(expected); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,151 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { |
|||
EntityAction, |
|||
EntityActionContributorCallback, |
|||
EntityActionList, |
|||
EntityActions, |
|||
EntityActionsFactory, |
|||
} from '../lib/models/entity-actions'; |
|||
|
|||
describe('EntityActionList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new EntityActionList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityActions', () => { |
|||
const add1toTail: EntityActionContributorCallback = actionList => actionList.addTail(1 as any); |
|||
const add2toTail: EntityActionContributorCallback = actionList => actionList.addTail(2 as any); |
|||
const add3toTail: EntityActionContributorCallback = actionList => actionList.addTail(3 as any); |
|||
const dropIndex1: EntityActionContributorCallback = actionList => actionList.dropByIndex(1); |
|||
|
|||
describe('#actions', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new EntityActions(callbackList); |
|||
expect(creator.actions.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new EntityActions([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set actions to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.actions.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityActionsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an EntityActions instance', () => { |
|||
const entityActions = new EntityActionsFactory(); |
|||
const creator = entityActions.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(EntityActions); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to EntityActionsCreator instance it returns', () => { |
|||
const entityActions = new EntityActionsFactory(); |
|||
const creatorX1 = entityActions.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(EntityActions); |
|||
expect(creatorX1.actions.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(actionList => actionList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = entityActions.get('X'); |
|||
expect(creatorX2.actions.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityAction', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
icon: 'ICON', |
|||
}; |
|||
|
|||
const action = new EntityAction(options); |
|||
|
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action(null)).toBe(options.action()); |
|||
expect(action.permission).toBe(options.permission); |
|||
expect(action.visible(null)).toBe(options.visible()); |
|||
expect(action.icon).toBe(options.icon); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
}; |
|||
|
|||
const action = new EntityAction(options); |
|||
|
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action).toBe(options.action); |
|||
expect(action.permission).toBeUndefined(); |
|||
expect(action.visible(null)).toBe(true); |
|||
expect(action.icon).toBe(''); |
|||
}); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
text: 'TEXT', |
|||
action: () => 'ACTION', |
|||
}; |
|||
|
|||
const action = EntityAction.create(options); |
|||
|
|||
expect(action).toBeInstanceOf(EntityAction); |
|||
expect(action.text).toBe(options.text); |
|||
expect(action.action).toBe(options.action); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
text: 'TEXT 1', |
|||
action: () => 'ACTION 1', |
|||
}; |
|||
const options2 = { |
|||
text: 'TEXT 2', |
|||
action: () => 'ACTION 2', |
|||
}; |
|||
|
|||
const [action1, action2] = EntityAction.createMany([options1, options2]); |
|||
|
|||
expect(action1).toBeInstanceOf(EntityAction); |
|||
expect(action1.text).toBe(options1.text); |
|||
expect(action1.action).toBe(options1.action); |
|||
expect(action2).toBeInstanceOf(EntityAction); |
|||
expect(action2.text).toBe(options2.text); |
|||
expect(action2.action).toBe(options2.action); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,166 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { NEVER } from 'rxjs'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { |
|||
EntityProp, |
|||
EntityPropContributorCallback, |
|||
EntityPropList, |
|||
EntityProps, |
|||
EntityPropsFactory, |
|||
} from '../lib/models/entity-props'; |
|||
import { PropData } from '../lib/models/props'; |
|||
|
|||
describe('EntityPropList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new EntityPropList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityProps', () => { |
|||
const add1toTail: EntityPropContributorCallback = propList => propList.addTail(1 as any); |
|||
const add2toTail: EntityPropContributorCallback = propList => propList.addTail(2 as any); |
|||
const add3toTail: EntityPropContributorCallback = propList => propList.addTail(3 as any); |
|||
const dropIndex1: EntityPropContributorCallback = propList => propList.dropByIndex(1); |
|||
|
|||
describe('#props', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new EntityProps(callbackList); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new EntityProps([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set props to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityPropsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an EntityProps instance', () => { |
|||
const entityProps = new EntityPropsFactory(); |
|||
const creator = entityProps.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(EntityProps); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to EntityPropsCreator instance it returns', () => { |
|||
const entityProps = new EntityPropsFactory(); |
|||
const creatorX1 = entityProps.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(EntityProps); |
|||
expect(creatorX1.props.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(propList => propList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = entityProps.get('X'); |
|||
expect(creatorX2.props.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('EntityProp', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
displayName: 'DISPLAY NAME', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
valueResolver: () => NEVER, |
|||
sortable: true, |
|||
columnWidth: 999, |
|||
}; |
|||
|
|||
const prop = new EntityProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.displayName); |
|||
expect(prop.permission).toBe(options.permission); |
|||
expect(prop.visible()).toBe(options.visible()); |
|||
expect(prop.valueResolver()).toBe(options.valueResolver()); |
|||
expect(prop.sortable).toBe(options.sortable); |
|||
expect(prop.columnWidth).toBe(options.columnWidth); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', done => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = new EntityProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.name); |
|||
expect(prop.permission).toBeUndefined(); |
|||
expect(prop.visible()).toBe(true); |
|||
expect(prop.sortable).toBe(false); |
|||
expect(prop.columnWidth).toBeUndefined(); |
|||
prop.valueResolver({ record: { NAME: 'X' } } as PropData).subscribe(value => { |
|||
expect(value).toBe('X'); |
|||
done(); |
|||
}); |
|||
}); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = EntityProp.create(options); |
|||
|
|||
expect(prop).toBeInstanceOf(EntityProp); |
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
type: ePropType.String, |
|||
name: 'NAME 1', |
|||
}; |
|||
const options2 = { |
|||
type: ePropType.Boolean, |
|||
name: 'NAME 2', |
|||
}; |
|||
|
|||
const [prop1, prop2] = EntityProp.createMany([options1, options2]); |
|||
|
|||
expect(prop1).toBeInstanceOf(EntityProp); |
|||
expect(prop1.type).toBe(options1.type); |
|||
expect(prop1.name).toBe(options1.name); |
|||
expect(prop2).toBeInstanceOf(EntityProp); |
|||
expect(prop2.type).toBe(options2.type); |
|||
expect(prop2.name).toBe(options2.name); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,133 @@ |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
import { Store } from '@ngxs/store'; |
|||
import { Subject } from 'rxjs'; |
|||
import { take } from 'rxjs/operators'; |
|||
import { PropData } from '../lib/models/props'; |
|||
import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util'; |
|||
|
|||
const fields = [ |
|||
{ name: 'foo', value: 1 }, |
|||
{ name: 'bar', value: 2 }, |
|||
{ name: 'baz', value: 3 }, |
|||
]; |
|||
|
|||
class MockPropData<R = any> extends PropData<R> { |
|||
getInjected: PropData<R>['getInjected']; |
|||
|
|||
constructor(public readonly record: R) { |
|||
super(); |
|||
} |
|||
} |
|||
|
|||
describe('Enum Utils', () => { |
|||
describe('#createEnum', () => { |
|||
const enumFromFields = createEnum(fields); |
|||
|
|||
test.each` |
|||
key | expected |
|||
${'foo'} | ${1} |
|||
${'bar'} | ${2} |
|||
${'baz'} | ${3} |
|||
${1} | ${'foo'} |
|||
${2} | ${'bar'} |
|||
${3} | ${'baz'} |
|||
`('should create an enum that returns $expected when $key is accessed', ({ key, expected }) => {
|
|||
expect(enumFromFields[key]).toBe(expected); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createEnumValueResolver', () => { |
|||
const service = new LocalizationService( |
|||
new Subject().asObservable(), |
|||
({ |
|||
selectSnapshot: () => ({ |
|||
values: { |
|||
Default: { |
|||
'Enum:MyEnum.foo': 'Foo', |
|||
'MyEnum.bar': 'Bar', |
|||
baz: 'Baz', |
|||
}, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}), |
|||
} as unknown) as Store, |
|||
null, |
|||
null, |
|||
null, |
|||
); |
|||
|
|||
test.each` |
|||
value | expected |
|||
${1} | ${'Foo'} |
|||
${2} | ${'Bar'} |
|||
${3} | ${'Baz'} |
|||
`(
|
|||
'should create a resolver that returns observable $expected when enum value is $value', |
|||
async ({ value, expected }) => { |
|||
const valueResolver = createEnumValueResolver( |
|||
'MyCompanyName.MyProjectName.MyEnum', |
|||
{ |
|||
fields, |
|||
localizationResource: null, |
|||
transformed: createEnum(fields), |
|||
}, |
|||
'EnumProp', |
|||
); |
|||
const propData = new MockPropData({ extraProperties: { EnumProp: value } }); |
|||
propData.getInjected = () => service as any; |
|||
|
|||
const resolved = await valueResolver(propData) |
|||
.pipe(take(1)) |
|||
.toPromise(); |
|||
|
|||
expect(resolved).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#createEnumOptions', () => { |
|||
const service = new LocalizationService( |
|||
new Subject().asObservable(), |
|||
({ |
|||
selectSnapshot: () => ({ |
|||
values: { |
|||
Default: { |
|||
'Enum:MyEnum.foo': 'Foo', |
|||
'MyEnum.bar': 'Bar', |
|||
baz: 'Baz', |
|||
}, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}), |
|||
} as unknown) as Store, |
|||
null, |
|||
null, |
|||
null, |
|||
); |
|||
|
|||
it('should create a generator that returns observable options from enums', async () => { |
|||
const options = createEnumOptions('MyCompanyName.MyProjectName.MyEnum', { |
|||
fields, |
|||
localizationResource: null, |
|||
transformed: createEnum(fields), |
|||
}); |
|||
|
|||
const propData = new MockPropData({}); |
|||
propData.getInjected = () => service as any; |
|||
|
|||
const resolved = await options(propData) |
|||
.pipe(take(1)) |
|||
.toPromise(); |
|||
|
|||
expect(resolved).toEqual([ |
|||
{ key: 'Foo', value: 1 }, |
|||
{ key: 'Bar', value: 2 }, |
|||
{ key: 'Baz', value: 3 }, |
|||
]); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,50 @@ |
|||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
|||
import { EntityActionsFactory } from '../lib/models/entity-actions'; |
|||
import { EntityPropsFactory } from '../lib/models/entity-props'; |
|||
import { CreateFormPropsFactory, EditFormPropsFactory } from '../lib/models/form-props'; |
|||
import { ToolbarActionsFactory } from '../lib/models/toolbar-actions'; |
|||
import { ExtensionsService } from '../lib/services/extensions.service'; |
|||
|
|||
describe('ExtensionsService', () => { |
|||
let service: ExtensionsService; |
|||
let spectator: SpectatorService<ExtensionsService>; |
|||
|
|||
const createService = createServiceFactory({ |
|||
service: ExtensionsService, |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
service = spectator.service; |
|||
}); |
|||
|
|||
describe('#entityActions', () => { |
|||
it('should be an instance of EntityActionsFactory class', () => { |
|||
expect(service.entityActions).toBeInstanceOf(EntityActionsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#toolbarActions', () => { |
|||
it('should be an instance of ToolbarActionsFactory class', () => { |
|||
expect(service.toolbarActions).toBeInstanceOf(ToolbarActionsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#entityProps', () => { |
|||
it('should be an instance of EntityPropsFactory class', () => { |
|||
expect(service.entityProps).toBeInstanceOf(EntityPropsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createFormProps', () => { |
|||
it('should be an instance of CreateFormPropsFactory class', () => { |
|||
expect(service.createFormProps).toBeInstanceOf(CreateFormPropsFactory); |
|||
}); |
|||
}); |
|||
|
|||
describe('#editFormProps', () => { |
|||
it('should be an instance of EditFormPropsFactory class', () => { |
|||
expect(service.editFormProps).toBeInstanceOf(EditFormPropsFactory); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,28 @@ |
|||
import { selfFactory } from '../lib/utils/factory.util'; |
|||
|
|||
describe('Factory Utils', () => { |
|||
describe('#selfFactory', () => { |
|||
const arr = []; |
|||
const obj = {}; |
|||
const date = new Date(); |
|||
const promise = Promise.resolve(null); |
|||
|
|||
test.each` |
|||
parameter |
|||
${'x'} |
|||
${''} |
|||
${1} |
|||
${0} |
|||
${true} |
|||
${false} |
|||
${arr} |
|||
${obj} |
|||
${date} |
|||
${promise} |
|||
${null} |
|||
${undefined} |
|||
`('should return given $parameter back', ({ parameter }) => {
|
|||
expect(selfFactory(parameter)).toBe(parameter); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,193 @@ |
|||
import { LinkedList } from '@abp/utils'; |
|||
import { NEVER } from 'rxjs'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { |
|||
CreateFormPropContributorCallback, |
|||
CreateFormPropsFactory, |
|||
EditFormPropsFactory, |
|||
FormProp, |
|||
FormPropList, |
|||
FormProps, |
|||
} from '../lib/models/form-props'; |
|||
|
|||
describe('FormPropList', () => { |
|||
it('should inherit from LinkedList', () => { |
|||
expect(new FormPropList() instanceof LinkedList).toBe(true); |
|||
}); |
|||
}); |
|||
|
|||
describe('FormProps', () => { |
|||
const add1toTail: CreateFormPropContributorCallback = propList => propList.addTail(1 as any); |
|||
const add2toTail: CreateFormPropContributorCallback = propList => propList.addTail(2 as any); |
|||
const add3toTail: CreateFormPropContributorCallback = propList => propList.addTail(3 as any); |
|||
const dropIndex1: CreateFormPropContributorCallback = propList => propList.dropByIndex(1); |
|||
|
|||
describe('#props', () => { |
|||
test.each` |
|||
callbackList | expected |
|||
${[]} | ${''} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${'1 <-> 2 <-> 3'} |
|||
${[add3toTail, add2toTail, add1toTail]} | ${'3 <-> 2 <-> 1'} |
|||
${[add1toTail, add2toTail, add3toTail, dropIndex1]} | ${'1 <-> 3'} |
|||
`(
|
|||
'should return $expected when given callbackList is $callbackList', |
|||
({ callbackList, expected }) => { |
|||
const creator = new FormProps(callbackList); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
|
|||
describe('#addContributor', () => { |
|||
const creator = new FormProps([]); |
|||
|
|||
test.each` |
|||
callbackList | callback | expected |
|||
${[]} | ${add1toTail} | ${'1'} |
|||
${[add1toTail]} | ${add2toTail} | ${'1 <-> 2'} |
|||
${[add1toTail, add2toTail]} | ${add3toTail} | ${'1 <-> 2 <-> 3'} |
|||
${[add1toTail, add2toTail, add3toTail]} | ${dropIndex1} | ${'1 <-> 3'} |
|||
`(
|
|||
'should set props to $expected when callbackList is $callbackList and given callback is $callback', |
|||
({ callback, expected }) => { |
|||
creator.addContributor(callback); |
|||
expect(creator.props.toString()).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
|
|||
describe('FormPropsFactory', () => { |
|||
describe('#get', () => { |
|||
it('should create and return an FormProps instance', () => { |
|||
const formProps = new CreateFormPropsFactory(); |
|||
const creator = formProps.get(''); |
|||
|
|||
expect(creator).toBeInstanceOf(FormProps); |
|||
}); |
|||
|
|||
it('should store and pass contributorCallbacks to FormPropsCreator instance it returns', () => { |
|||
const formProps = new EditFormPropsFactory(); |
|||
const creatorX1 = formProps.get('X'); |
|||
|
|||
expect(creatorX1).toBeInstanceOf(FormProps); |
|||
expect(creatorX1.props.toArray()).toHaveLength(0); |
|||
|
|||
creatorX1.addContributor(propList => propList.addTail(1 as any)); |
|||
|
|||
const creatorX2 = formProps.get('X'); |
|||
expect(creatorX2.props.toArray()).toHaveLength(1); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
describe('FormProp', () => { |
|||
it('should be created when options object is passed as argument', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
displayName: 'DISPLAY NAME', |
|||
permission: 'PERMISSION', |
|||
visible: () => false, |
|||
asyncValidators: () => [null], |
|||
validators: () => [null], |
|||
disabled: () => true, |
|||
readonly: () => true, |
|||
autocomplete: 'AUTOCOMPLETE', |
|||
defaultValue: 'DEFAULT VALUE', |
|||
options: () => NEVER, |
|||
id: 'ID', |
|||
}; |
|||
|
|||
const prop = new FormProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.displayName); |
|||
expect(prop.permission).toBe(options.permission); |
|||
expect(prop.visible()).toBe(options.visible()); |
|||
expect(prop.asyncValidators()).toEqual(options.asyncValidators()); |
|||
expect(prop.validators()).toEqual(options.validators()); |
|||
expect(prop.disabled()).toBe(options.disabled()); |
|||
expect(prop.readonly()).toBe(options.readonly()); |
|||
expect(prop.autocomplete).toBe(options.autocomplete); |
|||
expect(prop.defaultValue).toBe(options.defaultValue); |
|||
expect(prop.options()).toBe(options.options()); |
|||
expect(prop.id).toBe(options.id); |
|||
}); |
|||
|
|||
it('should be created when only required options are passed', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = new FormProp(options); |
|||
|
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
expect(prop.displayName).toBe(options.name); |
|||
expect(prop.permission).toBeUndefined(); |
|||
expect(prop.visible()).toBe(true); |
|||
expect(prop.asyncValidators()).toEqual([]); |
|||
expect(prop.validators()).toEqual([]); |
|||
expect(prop.disabled()).toBe(false); |
|||
expect(prop.readonly()).toBe(false); |
|||
expect(prop.autocomplete).toBe('off'); |
|||
expect(prop.defaultValue).toBeNull(); |
|||
expect(prop.options).toBeUndefined(); |
|||
expect(prop.id).toBe(options.name); |
|||
}); |
|||
|
|||
test.each` |
|||
defaultValue | expected |
|||
${0} | ${0} |
|||
${''} | ${''} |
|||
${false} | ${false} |
|||
${undefined} | ${null} |
|||
`(
|
|||
'should set defaultValue as $expected when $defaultValue is given', |
|||
({ defaultValue, expected }) => { |
|||
const options = { type: null, name: null, defaultValue }; |
|||
const prop = new FormProp(options); |
|||
expect(prop.defaultValue).toBe(expected); |
|||
}, |
|||
); |
|||
|
|||
describe('#create', () => { |
|||
it('should return a new instance from given options', () => { |
|||
const options = { |
|||
type: ePropType.String, |
|||
name: 'NAME', |
|||
}; |
|||
|
|||
const prop = FormProp.create(options); |
|||
|
|||
expect(prop).toBeInstanceOf(FormProp); |
|||
expect(prop.type).toBe(options.type); |
|||
expect(prop.name).toBe(options.name); |
|||
}); |
|||
}); |
|||
|
|||
describe('#createMany', () => { |
|||
it('should return multiple instances from given options array', () => { |
|||
const options1 = { |
|||
type: ePropType.String, |
|||
name: 'NAME 1', |
|||
}; |
|||
const options2 = { |
|||
type: ePropType.Boolean, |
|||
name: 'NAME 2', |
|||
}; |
|||
|
|||
const [prop1, prop2] = FormProp.createMany([options1, options2]); |
|||
|
|||
expect(prop1).toBeInstanceOf(FormProp); |
|||
expect(prop1.type).toBe(options1.type); |
|||
expect(prop1.name).toBe(options1.name); |
|||
expect(prop2).toBeInstanceOf(FormProp); |
|||
expect(prop2.type).toBe(options2.type); |
|||
expect(prop2.name).toBe(options2.name); |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,142 @@ |
|||
import { LocalizationService } from '@abp/ng.core'; |
|||
import { Injector } from '@angular/core'; |
|||
import { FormControl, FormGroup, Validators } from '@angular/forms'; |
|||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; |
|||
import { ePropType } from '../lib/enums/props.enum'; |
|||
import { FormProp, FormPropData } from '../lib/models/form-props'; |
|||
import { ExtensionsService } from '../lib/services/extensions.service'; |
|||
import { EXTENSIONS_IDENTIFIER } from '../lib/tokens/extensions.token'; |
|||
import { generateFormFromProps } from '../lib/utils/form-props.util'; |
|||
|
|||
describe('Form Prop Utils', () => { |
|||
describe('#generateFormFromProps', () => { |
|||
let spectator: SpectatorService<ExtensionsService<Foo>>; |
|||
let injector: Injector; |
|||
const identifier = 'X'; |
|||
|
|||
const createService = createServiceFactory({ |
|||
service: ExtensionsService, |
|||
providers: [ |
|||
{ |
|||
provide: EXTENSIONS_IDENTIFIER, |
|||
useValue: identifier, |
|||
}, |
|||
{ |
|||
provide: LocalizationService, |
|||
useValue: { currentLang: 'en' }, |
|||
}, |
|||
], |
|||
}); |
|||
|
|||
beforeEach(() => { |
|||
spectator = createService(); |
|||
const props = FormProp.createMany<Foo>([ |
|||
{ |
|||
type: ePropType.String, |
|||
name: 'foo', |
|||
validators: () => [Validators.required], |
|||
defaultValue: 'bar', |
|||
}, |
|||
{ |
|||
type: ePropType.Boolean, |
|||
name: 'bool', |
|||
}, |
|||
{ |
|||
type: ePropType.Date, |
|||
name: 'date', |
|||
}, |
|||
{ |
|||
type: ePropType.DateTime, |
|||
name: 'dateTime', |
|||
}, |
|||
{ |
|||
type: ePropType.Time, |
|||
name: 'time', |
|||
}, |
|||
]); |
|||
|
|||
spectator.service.createFormProps |
|||
.get(identifier) |
|||
.addContributor(propList => propList.addManyTail(props)); |
|||
spectator.service.editFormProps |
|||
.get(identifier) |
|||
.addContributor(propList => propList.addManyTail(props)); |
|||
|
|||
const generator = getInjected(spectator); |
|||
injector = { |
|||
get: () => generator.next().value as any, |
|||
}; |
|||
}); |
|||
|
|||
it('should return a blank FormGroup instance', () => { |
|||
const data = new FormPropData<Foo>(injector, null); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
expect(formGroup).toBeInstanceOf(FormGroup); |
|||
expect(formGroup.value.foo).toBe('bar'); |
|||
|
|||
const formControl = formGroup.get('foo'); |
|||
expect(formControl).toBeInstanceOf(FormControl); |
|||
expect(formControl.valid).toBe(true); |
|||
}); |
|||
|
|||
it('should return a prefilled FormGroup instance', () => { |
|||
const data = new FormPropData<Foo>(injector, { id: 1, foo: null }); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
expect(formGroup).toBeInstanceOf(FormGroup); |
|||
expect(formGroup.value.foo).toBe(null); |
|||
|
|||
const formControl = formGroup.get('foo'); |
|||
expect(formControl).toBeInstanceOf(FormControl); |
|||
expect(formControl.invalid).toBe(true); |
|||
}); |
|||
|
|||
it('should add a FormGroup named extraProperties', () => { |
|||
const data = new FormPropData<Foo>(injector, null); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
const extraPropertiesGroup = formGroup.get('extraProperties'); |
|||
expect(extraPropertiesGroup).toBeInstanceOf(FormGroup); |
|||
}); |
|||
|
|||
it('should add extraProperties to extraProperties FormGroup', () => { |
|||
const data = new FormPropData<Foo>(injector, { |
|||
id: 1, |
|||
foo: undefined, |
|||
extraProperties: { |
|||
bool: true, |
|||
date: '03/30/2002', |
|||
dateTime: '2002-03-30 13:30:59Z', |
|||
time: '13:30:59', |
|||
}, |
|||
}); |
|||
|
|||
const formGroup = generateFormFromProps(data); |
|||
const extraPropertiesGroup = formGroup.get('extraProperties'); |
|||
expect(extraPropertiesGroup.value).toEqual({ |
|||
bool: true, |
|||
date: '2002-03-30', |
|||
dateTime: '2002-03-30T13:30:59.000Z', |
|||
time: '13:30', |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
function* getInjected(spectator: SpectatorService<ExtensionsService>) { |
|||
yield spectator.service; |
|||
yield spectator.get(EXTENSIONS_IDENTIFIER); |
|||
yield spectator.get(LocalizationService); |
|||
} |
|||
|
|||
interface Foo { |
|||
id: number; |
|||
foo: string; |
|||
extraProperties?: { |
|||
bool: boolean; |
|||
date: string; |
|||
dateTime: string; |
|||
time: string; |
|||
}; |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
import { createDisplayNameLocalizationPipeKeyGenerator } from '../lib/utils/localization.util'; |
|||
|
|||
describe('Localization Utils', () => { |
|||
describe('#createDisplayNameLocalizationPipeKeyGenerator', () => { |
|||
const generateDisplayName = createDisplayNameLocalizationPipeKeyGenerator({ |
|||
values: { |
|||
Foo: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' }, |
|||
Default: { Bar: 'Bar', 'DisplayName:Bar': 'Bar' }, |
|||
}, |
|||
defaultResourceName: 'Default', |
|||
currentCulture: null, |
|||
languages: [], |
|||
}); |
|||
|
|||
test.each` |
|||
displayName | fallback | expected |
|||
${{ name: 'Bar', resource: 'Foo' }} | ${null} | ${'Foo::Bar'} |
|||
${{ name: 'Baz', resource: 'Foo' }} | ${null} | ${'Baz'} |
|||
${null} | ${{ name: 'Bar', resource: 'Foo' }} | ${'Foo::DisplayName:Bar'} |
|||
${null} | ${{ name: 'Bar', resource: 'Default' }} | ${'Default::DisplayName:Bar'} |
|||
${null} | ${{ name: 'Baz', resource: 'Default' }} | ${'Baz'} |
|||
`(
|
|||
'should return $expected when diplay name is $displayName and fallback is $fallback', |
|||
({ displayName, fallback, expected }) => { |
|||
const result = generateDisplayName(displayName, fallback); |
|||
|
|||
expect(result).toBe(expected); |
|||
}, |
|||
); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,22 @@ |
|||
import { PropData } from '../lib/models/props'; |
|||
|
|||
describe('PropData', () => { |
|||
describe('#data', () => { |
|||
it('should return record and getInjected', () => { |
|||
const spy = jest.fn(); |
|||
class Data extends PropData<string> { |
|||
index = 0; |
|||
record = 'X'; |
|||
getInjected = spy; |
|||
} |
|||
|
|||
const data = new Data(); |
|||
data.data.getInjected(null); |
|||
|
|||
expect(spy).toHaveBeenCalledTimes(1); |
|||
expect(spy).toHaveBeenCalledWith(null); |
|||
expect(data.data.index).toBe(0); |
|||
expect(data.data.record).toBe('X'); |
|||
}); |
|||
}); |
|||
}); |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue