Browse Source

Merge branch 'dev' into gterdem/logged_out_view

pull/4476/head
Galip Tolga Erdem 6 years ago
parent
commit
c3cdacfe40
  1. 14
      framework/Volo.Abp.sln
  2. 3
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/AbpPageSearchBoxViewComponent.cs
  3. 10
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml
  4. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/_ViewImports.cshtml
  5. 3
      framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xml
  6. 30
      framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xsd
  7. 25
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj
  8. 30
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs
  9. 49
      framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs
  10. 17
      framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo.Abp.Caching.StackExchangeRedis.Tests.csproj
  11. 12
      framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisTestBase.cs
  12. 15
      framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisTestModule.cs
  13. 22
      framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache_Tests.cs
  14. 66
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionStore.cs
  15. 55
      npm/ng-packs/apps/dev-app/src/app/app-routing.module.ts
  16. 2
      npm/ng-packs/apps/dev-app/src/app/app.module.ts
  17. 2
      npm/ng-packs/apps/dev-app/src/app/home/home-routing.module.ts
  18. 20
      npm/ng-packs/apps/dev-app/src/app/route.provider.ts
  19. 2
      npm/ng-packs/package.json
  20. 7
      npm/ng-packs/packages/account/ngcc.config.js
  21. 9
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  22. 7
      npm/ng-packs/packages/core/src/lib/directives/visibility.directive.ts
  23. 2
      npm/ng-packs/packages/core/src/lib/states/config.state.ts
  24. 7
      npm/ng-packs/packages/identity/ngcc.config.js
  25. 7
      npm/ng-packs/packages/setting-management/ngcc.config.js
  26. 7
      npm/ng-packs/packages/tenant-management/ngcc.config.js
  27. 1
      npm/ng-packs/packages/theme-basic/src/lib/actions/index.ts
  28. 11
      npm/ng-packs/packages/theme-basic/src/lib/actions/layout.actions.ts
  29. 12
      npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html
  30. 2
      npm/ng-packs/packages/theme-basic/src/lib/components/index.ts
  31. 67
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts
  32. 88
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts
  33. 89
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.html
  34. 123
      npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts
  35. 7
      npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html
  36. 2
      npm/ng-packs/packages/theme-basic/src/lib/enums/components.ts
  37. 1
      npm/ng-packs/packages/theme-basic/src/lib/enums/index.ts
  38. 4
      npm/ng-packs/packages/theme-basic/src/lib/enums/navigation-element-names.ts
  39. 1
      npm/ng-packs/packages/theme-basic/src/lib/providers/index.ts
  40. 31
      npm/ng-packs/packages/theme-basic/src/lib/providers/nav-item.provider.ts
  41. 1
      npm/ng-packs/packages/theme-basic/src/lib/services/index.ts
  42. 24
      npm/ng-packs/packages/theme-basic/src/lib/services/layout-state.service.ts
  43. 1
      npm/ng-packs/packages/theme-basic/src/lib/states/index.ts
  44. 67
      npm/ng-packs/packages/theme-basic/src/lib/states/layout.state.ts
  45. 57
      npm/ng-packs/packages/theme-basic/src/lib/tests/layout-state.service.spec.ts
  46. 28
      npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts
  47. 3
      npm/ng-packs/packages/theme-basic/src/public-api.ts
  48. 7
      npm/ng-packs/packages/theme-shared/extensions/ng-package.json
  49. 56
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts
  50. 29
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date.adapter.ts
  51. 35
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/time.adapter.ts
  52. 32
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts
  53. 74
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts
  54. 92
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html
  55. 80
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts
  56. 22
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html
  57. 82
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts
  58. 39
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html
  59. 88
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts
  60. 24
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html
  61. 36
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts
  62. 18
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html
  63. 49
      npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts
  64. 1
      npm/ng-packs/packages/theme-shared/extensions/src/lib/constants/extra-properties.ts
  65. 19
      npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/disabled.directive.ts
  66. 54
      npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/prop-data.directive.ts
  67. 12
      npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
  68. 79
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/actions.ts
  69. 53
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-actions.ts
  70. 67
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts
  71. 109
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/form-props.ts
  72. 96
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/internal/object-extensions.ts
  73. 3
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/object-extensions.ts
  74. 85
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts
  75. 78
      npm/ng-packs/packages/theme-shared/extensions/src/lib/models/toolbar-actions.ts
  76. 16
      npm/ng-packs/packages/theme-shared/extensions/src/lib/services/extensions.service.ts
  77. 13
      npm/ng-packs/packages/theme-shared/extensions/src/lib/tokens/extensions.token.ts
  78. 48
      npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts
  79. 49
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/actions.util.ts
  80. 73
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts
  81. 3
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/factory.util.ts
  82. 61
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/form-props.util.ts
  83. 27
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/localization.util.ts
  84. 62
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/props.util.ts
  85. 174
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts
  86. 14
      npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/validation.util.ts
  87. 70
      npm/ng-packs/packages/theme-shared/extensions/src/public-api.ts
  88. 22
      npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.spec.ts
  89. 53
      npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.util.spec.ts
  90. 35
      npm/ng-packs/packages/theme-shared/extensions/src/tests/date-time.adapter.spec.ts
  91. 31
      npm/ng-packs/packages/theme-shared/extensions/src/tests/date.adapter.spec.ts
  92. 151
      npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-actions.spec.ts
  93. 166
      npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-props.spec.ts
  94. 133
      npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts
  95. 50
      npm/ng-packs/packages/theme-shared/extensions/src/tests/extensions.service.spec.ts
  96. 28
      npm/ng-packs/packages/theme-shared/extensions/src/tests/factory.util.spec.ts
  97. 193
      npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.spec.ts
  98. 142
      npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.util.spec.ts
  99. 31
      npm/ng-packs/packages/theme-shared/extensions/src/tests/localization.util.spec.ts
  100. 22
      npm/ng-packs/packages/theme-shared/extensions/src/tests/props.spec.ts

14
framework/Volo.Abp.sln

@ -307,6 +307,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Azure.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.Oracle", "src\Volo.Abp.EntityFrameworkCore.Oracle\Volo.Abp.EntityFrameworkCore.Oracle.csproj", "{1738845A-5348-4EB8-B736-CD1D22A808B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Caching.StackExchangeRedis", "src\Volo.Abp.Caching.StackExchangeRedis\Volo.Abp.Caching.StackExchangeRedis.csproj", "{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Caching.StackExchangeRedis.Tests", "test\Volo.Abp.Caching.StackExchangeRedis.Tests\Volo.Abp.Caching.StackExchangeRedis.Tests.csproj", "{60D0E384-965E-4F81-9D71-B28F419254FC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -913,6 +917,14 @@ Global
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1738845A-5348-4EB8-B736-CD1D22A808B4}.Release|Any CPU.Build.0 = Release|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4}.Release|Any CPU.Build.0 = Release|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{60D0E384-965E-4F81-9D71-B28F419254FC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1068,6 +1080,8 @@ Global
{C44242F7-D55D-4867-AAF4-A786E404312E} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{A80E9A0B-8932-4B5D-83FB-6751708FD484} = {447C8A77-E5F0-4538-8687-7383196D04EA}
{1738845A-5348-4EB8-B736-CD1D22A808B4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{2B83DF1F-0FD2-4DEA-ABC5-E324B51401D4} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6}
{60D0E384-965E-4F81-9D71-B28F419254FC} = {447C8A77-E5F0-4538-8687-7383196D04EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5}

3
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/AbpPageSearchBoxViewComponent.cs

@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Pages.Shared.Components.AbpPageSearchBox
{

10
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/Default.cshtml

@ -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>

4
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Pages/Shared/Components/AbpPageSearchBox/_ViewImports.cshtml

@ -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

3
framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xml

@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ConfigureAwait ContinueOnCapturedContext="false" />
</Weavers>

30
framework/src/Volo.Abp.Caching.StackExchangeRedis/FodyWeavers.xsd

@ -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>

25
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj

@ -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>

30
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisModule.cs

@ -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>());
}
}
}

49
framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache.cs

@ -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});
}
}
}

17
framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo.Abp.Caching.StackExchangeRedis.Tests.csproj

@ -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>

12
framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisTestBase.cs

@ -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();
}
}
}

15
framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo/Abp/Caching/StackExchangeRedis/AbpCachingStackExchangeRedisTestModule.cs

@ -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
{
}
}

22
framework/test/Volo.Abp.Caching.StackExchangeRedis.Tests/Volo/Abp/Caching/StackExchangeRedis/AbpRedisCache_Tests.cs

@ -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();
}
}
}

66
modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionStore.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Authorization.Permissions;
@ -10,17 +11,21 @@ namespace Volo.Abp.PermissionManagement
public class PermissionStore : IPermissionStore, ITransientDependency
{
public ILogger<PermissionStore> Logger { get; set; }
protected IPermissionGrantRepository PermissionGrantRepository { get; }
protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
protected IDistributedCache<PermissionGrantCacheItem> Cache { get; }
public PermissionStore(
IPermissionGrantRepository permissionGrantRepository,
IDistributedCache<PermissionGrantCacheItem> cache)
IDistributedCache<PermissionGrantCacheItem> cache,
IPermissionDefinitionManager permissionDefinitionManager)
{
PermissionGrantRepository = permissionGrantRepository;
Cache = cache;
PermissionDefinitionManager = permissionDefinitionManager;
Logger = NullLogger<PermissionStore>.Instance;
}
@ -29,7 +34,10 @@ namespace Volo.Abp.PermissionManagement
return (await GetCacheItemAsync(name, providerName, providerKey)).IsGranted;
}
protected virtual async Task<PermissionGrantCacheItem> GetCacheItemAsync(string name, string providerName, string providerKey)
protected virtual async Task<PermissionGrantCacheItem> GetCacheItemAsync(
string name,
string providerName,
string providerKey)
{
var cacheKey = CalculateCacheKey(name, providerName, providerKey);
@ -43,23 +51,45 @@ namespace Volo.Abp.PermissionManagement
return cacheItem;
}
Logger.LogDebug($"Not found in the cache, getting from the repository: {cacheKey}");
Logger.LogDebug($"Not found in the cache: {cacheKey}");
cacheItem = new PermissionGrantCacheItem(name, false);
await SetCacheItemsAsync(providerName, providerKey, name, cacheItem);
cacheItem = new PermissionGrantCacheItem(
name,
(await PermissionGrantRepository.FindAsync(name, providerName, providerKey)) != null
);
return cacheItem;
}
Logger.LogDebug($"Setting the cache item: {cacheKey}");
protected virtual async Task SetCacheItemsAsync(
string providerName,
string providerKey,
string currentName,
PermissionGrantCacheItem currentCacheItem)
{
var permissions = PermissionDefinitionManager.GetPermissions();
Logger.LogDebug($"Getting all granted permissions from the repository for this provider name,key: {providerName},{providerKey}");
await Cache.SetAsync(
cacheKey,
cacheItem
);
var permissionGrants = await PermissionGrantRepository.GetListAsync(providerName, providerKey);
Logger.LogDebug($"Finished setting the cache item: {cacheKey}");
Logger.LogDebug($"Setting the cache items. Count: {permissions.Count}");
return cacheItem;
foreach (var permission in permissions)
{
var isGranted = permissionGrants.Any(pg => pg.Name == permission.Name); //TODO: Optimize? Dictionary/Hash
await Cache.SetAsync(
CalculateCacheKey(permission.Name, providerName, providerKey),
new PermissionGrantCacheItem(permission.Name, isGranted)
);
if (permission.Name == currentName)
{
currentCacheItem.IsGranted = isGranted;
}
}
Logger.LogDebug($"Finished setting the cache items. Count: {permissions.Count}");
}
protected virtual string CalculateCacheKey(string name, string providerName, string providerKey)
@ -67,4 +97,4 @@ namespace Volo.Abp.PermissionManagement
return PermissionGrantCacheItem.CalculateCacheKey(name, providerName, providerKey);
}
}
}
}

55
npm/ng-packs/apps/dev-app/src/app/app-routing.module.ts

@ -1,36 +1,37 @@
import { ABP } from '@abp/ng.core';
import { ABP, DynamicLayoutComponent } from '@abp/ng.core';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
data: {
routes: {
name: '::Menu:Home',
order: 1,
} as ABP.Route,
},
},
{
path: 'account',
loadChildren: () =>
import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () =>
import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () =>
import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()),
component: DynamicLayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () =>
import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () =>
import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () =>
import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()),
},
],
},
];

2
npm/ng-packs/apps/dev-app/src/app/app.module.ts

@ -13,6 +13,7 @@ import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { APP_ROUTE_PROVIDER } from './route.provider';
const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })];
@ -35,6 +36,7 @@ const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })];
ThemeBasicModule.forRoot(),
...(environment.production ? [] : LOGGERS),
],
providers: [APP_ROUTE_PROVIDER],
declarations: [AppComponent],
bootstrap: [AppComponent],
})

2
npm/ng-packs/apps/dev-app/src/app/home/home-routing.module.ts

@ -1,12 +1,10 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home.component';
import { ApplicationLayoutComponent } from '@abp/ng.theme.basic';
const routes: Routes = [
{
path: '',
component: ApplicationLayoutComponent,
children: [{ path: '', component: HomeComponent }],
},
];

20
npm/ng-packs/apps/dev-app/src/app/route.provider.ts

@ -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,
},
]);
};
}

2
npm/ng-packs/package.json

@ -19,7 +19,7 @@
"ci:test": "ng test --coverage=false",
"ci:build": "cd scripts && yarn build:prod",
"lerna": "lerna",
"compile:ivy": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points --tsconfig './tsconfig.prod.json'",
"compile:ivy": "yarn ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points --tsconfig './tsconfig.prod.json' --source node_modules --async false",
"postinstall": "npm run compile:ivy"
},
"devDependencies": {

7
npm/ng-packs/packages/account/ngcc.config.js

@ -0,0 +1,7 @@
module.exports = {
entryPoints: {
'.': {},
'./config': {},
'./dist': { ignore: true },
},
};

9
npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts

@ -1,4 +1,4 @@
import { Component, Injector, OnDestroy, Type } from '@angular/core';
import { Component, Injector, OnDestroy, Type, Optional, SkipSelf } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngxs/store';
import { eLayoutType } from '../enums/common';
@ -23,7 +23,12 @@ import { TreeNode } from '../utils/tree-utils';
export class DynamicLayoutComponent implements OnDestroy {
layout: Type<any>;
constructor(injector: Injector, private store: Store) {
constructor(
injector: Injector,
private store: Store,
@Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent,
) {
if (dynamicLayoutComponent) return;
const route = injector.get(ActivatedRoute);
const router = injector.get(Router);
const routes = injector.get(RoutesService);

7
npm/ng-packs/packages/core/src/lib/directives/visibility.directive.ts

@ -2,6 +2,10 @@ import { Directive, Input, Optional, ElementRef, Renderer2, AfterViewInit } from
import { Subject } from 'rxjs';
import snq from 'snq';
/**
*
* @deprecated To be deleted in v3.3
*/
@Directive({
selector: '[abpVisibility]',
})
@ -40,7 +44,8 @@ export class VisibilityDirective implements AfterViewInit {
setTimeout(() => {
const htmlNodes = snq(
() => Array.from(this.focusedElement.childNodes).filter(node => node instanceof HTMLElement),
() =>
Array.from(this.focusedElement.childNodes).filter(node => node instanceof HTMLElement),
[],
);

2
npm/ng-packs/packages/core/src/lib/states/config.state.ts

@ -190,7 +190,7 @@ export class ConfigState {
@Action(GetAppConfiguration)
addData({ patchState, dispatch }: StateContext<Config.State>) {
const apiName = this.store.selectSnapshot(ConfigState.getDeep('environment.application.name'));
const apiName = 'default';
const api = this.store.selectSnapshot(ConfigState.getApiUrl(apiName));
return this.http
.get<ApplicationConfiguration.Response>(`${api}/api/abp/application-configuration`)

7
npm/ng-packs/packages/identity/ngcc.config.js

@ -0,0 +1,7 @@
module.exports = {
entryPoints: {
'.': {},
'./config': {},
'./dist': { ignore: true },
},
};

7
npm/ng-packs/packages/setting-management/ngcc.config.js

@ -0,0 +1,7 @@
module.exports = {
entryPoints: {
'.': {},
'./config': {},
'./dist': { ignore: true },
},
};

7
npm/ng-packs/packages/tenant-management/ngcc.config.js

@ -0,0 +1,7 @@
module.exports = {
entryPoints: {
'.': {},
'./config': {},
'./dist': { ignore: true },
},
};

1
npm/ng-packs/packages/theme-basic/src/lib/actions/index.ts

@ -1 +0,0 @@
export * from './layout.actions';

11
npm/ng-packs/packages/theme-basic/src/lib/actions/layout.actions.ts

@ -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) {}
}

12
npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.html

@ -34,21 +34,15 @@
<abp-nav-items
*abpReplaceableTemplate="{
componentKey: navItemsComponentKey,
inputs: {
smallScreen: { value: smallScreen }
}
componentKey: navItemsComponentKey
}"
[smallScreen]="smallScreen"
></abp-nav-items>
</ng-template>
</div>
</div>
</nav>
<div
[@slideFromBottom]="outlet?.isActivated && outlet.activatedRoute?.routeConfig?.path"
class="container"
>
<!-- [@slideFromBottom]="outlet.isActivated && outlet.activatedRoute?.routeConfig?.path" TODO: throws ExpressionChangedAfterItHasBeenCheck when animation is active. It should be fixed -->
<div class="container">
<router-outlet #outlet="outlet"></router-outlet>
</div>

2
npm/ng-packs/packages/theme-basic/src/lib/components/index.ts

@ -2,6 +2,8 @@ export * from './account-layout/account-layout.component';
export * from './application-layout/application-layout.component';
export * from './empty-layout/empty-layout.component';
export * from './logo/logo.component';
export * from './nav-items/current-user.component';
export * from './nav-items/languages.component';
export * from './nav-items/nav-items.component';
export * from './routes/routes.component';
export * from './validation-error/validation-error.component';

67
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts

@ -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 } });
});
}
}

88
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts

@ -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));
}
}

89
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.html

@ -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>

123
npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts

@ -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) {}
}

7
npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.html

@ -18,10 +18,10 @@
<ng-template #dropdownLink let-route>
<li
#navbarRootDropdown
*abpPermission="route.requiredPolicy"
[abpVisibility]="routeContainer"
class="nav-item dropdown"
display="static"
*ngIf="route.children?.length"
[abpPermission]="route.requiredPolicy"
(click)="
navbarRootDropdown.expand
? (navbarRootDropdown.expand = false)
@ -69,13 +69,12 @@
<ng-template #dropdownChild let-child>
<div
[abpVisibility]="childrenContainer"
class="dropdown-submenu"
ngbDropdown
#dropdownSubmenu="ngbDropdown"
placement="right-top"
[autoClose]="true"
*abpPermission="child.requiredPolicy"
[abpPermission]="child.requiredPolicy"
>
<div ngbDropdownToggle [class.dropdown-toggle]="false">
<a

2
npm/ng-packs/packages/theme-basic/src/lib/enums/components.ts

@ -5,4 +5,6 @@ export const enum eThemeBasicComponents {
Logo = 'Theme.LogoComponent',
Routes = 'Theme.RoutesComponent',
NavItems = 'Theme.NavItemsComponent',
CurrentUser = 'Theme.CurrentUserComponent',
Languages = 'Theme.LanguagesComponent',
}

1
npm/ng-packs/packages/theme-basic/src/lib/enums/index.ts

@ -1,2 +1 @@
export * from './components';
export * from './navigation-element-names';

4
npm/ng-packs/packages/theme-basic/src/lib/enums/navigation-element-names.ts

@ -1,4 +0,0 @@
export const enum eNavigationElementNames {
Language = 'LanguageRef',
User = 'CurrentUserRef',
}

1
npm/ng-packs/packages/theme-basic/src/lib/providers/index.ts

@ -1 +1,2 @@
export * from './nav-item.provider';
export * from './styles.provider';

31
npm/ng-packs/packages/theme-basic/src/lib/providers/nav-item.provider.ts

@ -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
npm/ng-packs/packages/theme-basic/src/lib/services/index.ts

@ -1 +0,0 @@
export * from './layout-state.service';

24
npm/ng-packs/packages/theme-basic/src/lib/services/layout-state.service.ts

@ -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
npm/ng-packs/packages/theme-basic/src/lib/states/index.ts

@ -1 +0,0 @@
export * from './layout.state';

67
npm/ng-packs/packages/theme-basic/src/lib/states/layout.state.ts

@ -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,
});
}
}

57
npm/ng-packs/packages/theme-basic/src/lib/tests/layout-state.service.spec.ts

@ -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));
});
});
});

28
npm/ng-packs/packages/theme-basic/src/lib/theme-basic.module.ts

@ -3,7 +3,6 @@ import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { NgbCollapseModule, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxValidateCoreModule } from '@ngx-validate/core';
import { NgxsModule } from '@ngxs/store';
import { AccountLayoutComponent } from './components/account-layout/account-layout.component';
import { ApplicationLayoutComponent } from './components/application-layout/application-layout.component';
import { EmptyLayoutComponent } from './components/empty-layout/empty-layout.component';
@ -11,8 +10,10 @@ import { LogoComponent } from './components/logo/logo.component';
import { NavItemsComponent } from './components/nav-items/nav-items.component';
import { RoutesComponent } from './components/routes/routes.component';
import { ValidationErrorComponent } from './components/validation-error/validation-error.component';
import { CurrentUserComponent } from './components/nav-items/current-user.component';
import { LanguagesComponent } from './components/nav-items/languages.component';
import { BASIC_THEME_STYLES_PROVIDERS } from './providers/styles.provider';
import { LayoutState } from './states/layout.state';
import { BASIC_THEME_NAV_ITEM_PROVIDERS } from './providers/nav-item.provider';
export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, EmptyLayoutComponent];
@ -23,6 +24,17 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
LogoComponent,
NavItemsComponent,
RoutesComponent,
CurrentUserComponent,
LanguagesComponent,
],
exports: [
...LAYOUTS,
ValidationErrorComponent,
LogoComponent,
NavItemsComponent,
RoutesComponent,
CurrentUserComponent,
LanguagesComponent,
],
imports: [
CoreModule,
@ -30,7 +42,6 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
NgbCollapseModule,
NgbDropdownModule,
NgxValidateCoreModule,
NgxsModule.forFeature([LayoutState]),
NgxValidateCoreModule.forRoot({
targetSelector: '.form-group',
blueprints: {
@ -52,20 +63,13 @@ export const LAYOUTS = [ApplicationLayoutComponent, AccountLayoutComponent, Empt
errorTemplate: ValidationErrorComponent,
}),
],
exports: [
...LAYOUTS,
ValidationErrorComponent,
LogoComponent,
NavItemsComponent,
RoutesComponent,
],
entryComponents: [...LAYOUTS, ValidationErrorComponent],
entryComponents: [...LAYOUTS, ValidationErrorComponent, CurrentUserComponent, LanguagesComponent],
})
export class ThemeBasicModule {
static forRoot(): ModuleWithProviders<ThemeBasicModule> {
return {
ngModule: ThemeBasicModule,
providers: [BASIC_THEME_STYLES_PROVIDERS],
providers: [BASIC_THEME_NAV_ITEM_PROVIDERS, BASIC_THEME_STYLES_PROVIDERS],
};
}
}

3
npm/ng-packs/packages/theme-basic/src/public-api.ts

@ -2,11 +2,8 @@
* Public API Surface of theme-basic
*/
export * from './lib/actions';
export * from './lib/components';
export * from './lib/enums';
export * from './lib/models';
export * from './lib/providers';
export * from './lib/services';
export * from './lib/states';
export * from './lib/theme-basic.module';

7
npm/ng-packs/packages/theme-shared/extensions/ng-package.json

@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/theme-shared/extensions",
"lib": {
"entryFile": "src/public-api.ts"
}
}

56
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.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;

29
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date.adapter.ts

@ -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;
}
}

35
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/time.adapter.ts

@ -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));
}

32
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/abstract-actions/abstract-actions.component.ts

@ -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;

74
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/date-time-picker/date-time-picker.component.ts

@ -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);
}
}

92
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.html

@ -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>

80
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form-prop.component.ts

@ -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';
}
}
}

22
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.html

@ -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>

82
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-form/extensible-form.component.ts

@ -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();
}
}

39
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html

@ -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>

88
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.ts

@ -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
}
}),
);
}
}

24
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.html

@ -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>

36
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/grid-actions/grid-actions.component.ts

@ -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);
}
}

18
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.html

@ -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>

49
npm/ng-packs/packages/theme-shared/extensions/src/lib/components/page-toolbar/page-toolbar.component.ts

@ -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 };
}
}

1
npm/ng-packs/packages/theme-shared/extensions/src/lib/constants/extra-properties.ts

@ -0,0 +1 @@
export const EXTRA_PROPERTIES_KEY = 'extraProperties';

19
npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/disabled.directive.ts

@ -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']();
}
}
}

54
npm/ng-packs/packages/theme-shared/extensions/src/lib/directives/prop-data.directive.ts

@ -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;

12
npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts

@ -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',
}

79
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/actions.ts

@ -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;

53
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-actions.ts

@ -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>
>;

67
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts

@ -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>>;

109
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/form-props.ts

@ -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;
}

96
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/internal/object-extensions.ts

@ -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;
}

3
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/object-extensions.ts

@ -0,0 +1,3 @@
import * as ObjectExtensions from './internal/object-extensions';
export { ObjectExtensions };

85
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/props.ts

@ -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;

78
npm/ng-packs/packages/theme-shared/extensions/src/lib/models/toolbar-actions.ts

@ -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>
>;

16
npm/ng-packs/packages/theme-shared/extensions/src/lib/services/extensions.service.ts

@ -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>();
}

13
npm/ng-packs/packages/theme-shared/extensions/src/lib/tokens/extensions.token.ts

@ -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',
);

48
npm/ng-packs/packages/theme-shared/extensions/src/lib/ui-extensions.module.ts

@ -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 {}

49
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/actions.util.ts

@ -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;

73
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/enum.util.ts

@ -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();
}

3
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/factory.util.ts

@ -0,0 +1,3 @@
export function selfFactory(dependency?: any) {
return dependency;
}

61
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/form-props.util.ts

@ -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;
}

27
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/localization.util.ts

@ -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);
};
}

62
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/props.util.ts

@ -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;

174
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/state.util.ts

@ -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;

14
npm/ng-packs/packages/theme-shared/extensions/src/lib/utils/validation.util.ts

@ -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;
}

70
npm/ng-packs/packages/theme-shared/extensions/src/public-api.ts

@ -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';

22
npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.spec.ts

@ -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');
});
});
});

53
npm/ng-packs/packages/theme-shared/extensions/src/tests/actions.util.spec.ts

@ -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');
});
});
});

35
npm/ng-packs/packages/theme-shared/extensions/src/tests/date-time.adapter.spec.ts

@ -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);
});
});
});

31
npm/ng-packs/packages/theme-shared/extensions/src/tests/date.adapter.spec.ts

@ -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);
});
});
});

151
npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-actions.spec.ts

@ -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);
});
});
});

166
npm/ng-packs/packages/theme-shared/extensions/src/tests/entity-props.spec.ts

@ -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);
});
});
});

133
npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts

@ -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 },
]);
});
});
});

50
npm/ng-packs/packages/theme-shared/extensions/src/tests/extensions.service.spec.ts

@ -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);
});
});
});

28
npm/ng-packs/packages/theme-shared/extensions/src/tests/factory.util.spec.ts

@ -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);
});
});
});

193
npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.spec.ts

@ -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);
});
});
});

142
npm/ng-packs/packages/theme-shared/extensions/src/tests/form-props.util.spec.ts

@ -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;
};
}

31
npm/ng-packs/packages/theme-shared/extensions/src/tests/localization.util.spec.ts

@ -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);
},
);
});
});

22
npm/ng-packs/packages/theme-shared/extensions/src/tests/props.spec.ts

@ -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…
Cancel
Save