Browse Source

Merge branch 'dev' into bs5

pull/10355/head
maliming 5 years ago
parent
commit
a472d8beb1
  1. 6
      docs/zh-Hans/Text-Templating.md
  2. 3
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs
  3. 2
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ApiVersionInfo.cs
  4. 269
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs
  5. 6
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestContext.cs
  6. 13
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs
  7. 17
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs
  8. 63
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs
  9. 19
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptorClientProxy.cs
  10. 267
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs
  11. 12
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs
  12. 3
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/AccountProfilePasswordManagementGroupViewComponent.cs
  13. 3
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/AccountProfilePersonalInfoManagementGroupViewComponent.cs
  14. 13
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/ProfileManagementScriptBundleContributor.cs
  15. 3
      modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Menus/MenuItemAdminAppService.cs
  16. 12
      modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs
  17. 3
      modules/setting-management/src/Volo.Abp.SettingManagement.Application/Volo/Abp/SettingManagement/EmailSettingsAppService.cs
  18. 15
      modules/setting-management/src/Volo.Abp.SettingManagement.Web/Settings/EmailingPageContributor.cs
  19. 27
      npm/ng-packs/packages/core/src/lib/tests/application-configuration.service.spec.ts
  20. 12
      npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts
  21. 29
      npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts
  22. 2
      npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts
  23. 126
      npm/ng-packs/packages/core/src/lib/tests/visibility.directive.spec.ts
  24. 2
      npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html
  25. 2
      npm/ng-packs/packages/identity/src/lib/components/roles/roles.component.html
  26. 2
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.html
  27. 4
      npm/ng-packs/packages/tenant-management/src/lib/components/tenants/tenants.component.html
  28. 2
      npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/tenant-box/tenant-box.component.html
  29. 7
      npm/ng-packs/packages/theme-shared/extensions/src/tests/enum.util.spec.ts
  30. 5
      npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts
  31. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts
  32. 34
      npm/ng-packs/packages/theme-shared/src/lib/tests/modal-container.component.spec.ts
  33. 2
      npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts
  34. 87
      npm/ng-packs/packages/theme-shared/src/lib/tests/modal.service.spec.ts
  35. 46
      npm/ng-packs/packages/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts
  36. 36
      npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts
  37. 1
      npm/ng-packs/packages/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts
  38. 9
      npm/ng-packs/scripts/prod-build.ts
  39. 2
      npm/ng-packs/tsconfig.json

6
docs/zh-Hans/Text-Templating.md

@ -187,9 +187,9 @@ var result = await _templateRenderer.RenderAsync(
示例中我们并没有创建模型类,但是创建了一个匿名对象模型. 示例中我们并没有创建模型类,但是创建了一个匿名对象模型.
### 大驼峰 与 小驼峰 ### PascalCase 与 snake_case
PascalCase 属性名(如 `UserName`) 在模板中用做小驼峰(如 `userName`). PascalCase 属性名(如 `UserName`) 在模板中使用蛇形命名(如 `user_name`).
## 本地化 ## 本地化
@ -454,4 +454,4 @@ public class MyTemplateContentProvider
* 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo). * 本文开发和引用的[应用程序示例源码](https://github.com/abpframework/abp-samples/tree/master/TextTemplateDemo).
* [本地化系统](Localization.md). * [本地化系统](Localization.md).
* [虚拟文件系统](Virtual-File-System.md). * [虚拟文件系统](Virtual-File-System.md).

3
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/AbpHttpClientModule.cs

@ -5,6 +5,7 @@ using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading; using Volo.Abp.Threading;
using Volo.Abp.Validation; using Volo.Abp.Validation;
using Volo.Abp.ExceptionHandling; using Volo.Abp.ExceptionHandling;
using Volo.Abp.Http.Client.DynamicProxying;
namespace Volo.Abp.Http.Client namespace Volo.Abp.Http.Client
{ {
@ -22,6 +23,8 @@ namespace Volo.Abp.Http.Client
{ {
var configuration = context.Services.GetConfiguration(); var configuration = context.Services.GetConfiguration();
Configure<AbpRemoteServiceOptions>(configuration); Configure<AbpRemoteServiceOptions>(configuration);
context.Services.AddTransient(typeof(DynamicHttpProxyInterceptorClientProxy<>));
} }
} }
} }

2
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/ApiVersionInfo.cs → framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ApiVersionInfo.cs

@ -1,6 +1,6 @@
using System; using System;
namespace Volo.Abp.Http.Client.Proxying namespace Volo.Abp.Http.Client.ClientProxying
{ {
public class ApiVersionInfo //TODO: Rename to not conflict with api versioning apis public class ApiVersionInfo //TODO: Rename to not conflict with api versioning apis
{ {

269
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs

@ -1,9 +1,23 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Volo.Abp.Content;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Client.Proxying;
using Volo.Abp.Http.Modeling; using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Tracing;
namespace Volo.Abp.Http.Client.ClientProxying namespace Volo.Abp.Http.Client.ClientProxying
{ {
@ -11,42 +25,265 @@ namespace Volo.Abp.Http.Client.ClientProxying
{ {
public IAbpLazyServiceProvider LazyServiceProvider { get; set; } public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
protected IHttpProxyExecuter HttpProxyExecuter => LazyServiceProvider.LazyGetRequiredService<IHttpProxyExecuter>();
protected IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder => LazyServiceProvider.LazyGetRequiredService<IClientProxyApiDescriptionFinder>(); protected IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder => LazyServiceProvider.LazyGetRequiredService<IClientProxyApiDescriptionFinder>();
protected ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetRequiredService<ICancellationTokenProvider>();
protected ICorrelationIdProvider CorrelationIdProvider => LazyServiceProvider.LazyGetRequiredService<ICorrelationIdProvider>();
protected ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
protected IOptions<AbpCorrelationIdOptions> AbpCorrelationIdOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpCorrelationIdOptions>>();
protected IProxyHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService<IProxyHttpClientFactory>();
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceConfigurationProvider>();
protected IOptions<AbpHttpClientOptions> ClientOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<AbpHttpClientOptions>>();
protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService<IJsonSerializer>();
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService<IRemoteServiceHttpClientAuthenticator>();
protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyRequestPayloadBuilder>();
protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService<ClientProxyUrlBuilder>();
protected virtual async Task RequestAsync(string methodName, params object[] arguments) protected virtual async Task RequestAsync(string methodName, params object[] arguments)
{ {
await HttpProxyExecuter.MakeRequestAsync(BuildHttpProxyExecuterContext(methodName, arguments)); await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments));
} }
protected virtual async Task<T> RequestAsync<T>(string methodName, params object[] arguments) protected virtual async Task<T> RequestAsync<T>(string methodName, params object[] arguments)
{ {
return await HttpProxyExecuter.MakeRequestAndGetResultAsync<T>(BuildHttpProxyExecuterContext(methodName, arguments)); return await RequestAsync<T>(BuildHttpProxyClientProxyContext(methodName, arguments));
} }
protected virtual HttpProxyExecuterContext BuildHttpProxyExecuterContext(string methodName, params object[] arguments) protected virtual ClientProxyRequestContext BuildHttpProxyClientProxyContext(string methodName, params object[] arguments)
{ {
var actionKey = GetActionKey(methodName, arguments); var methodUniqueName = $"{typeof(TService).FullName}.{methodName}.{string.Join("-", arguments.Select(x => x.GetType().FullName))}";
var action = ClientProxyApiDescriptionFinder.FindAction(actionKey); var action = ClientProxyApiDescriptionFinder.FindAction(methodUniqueName);
return new HttpProxyExecuterContext(action, BuildArguments(action, arguments), typeof(TService)); if (action == null)
{
throw new AbpException($"The API description of the {typeof(TService).FullName}.{methodName} method was not found!");
}
return new ClientProxyRequestContext(
action,
action.Parameters
.GroupBy(x => x.NameOnMethod)
.Select((x, i) => new KeyValuePair<string, object>(x.Key, arguments[i]))
.ToDictionary(x => x.Key, x => x.Value),
typeof(TService));
} }
protected virtual Dictionary<string, object> BuildArguments(ActionApiDescriptionModel action, object[] arguments) protected virtual async Task<T> RequestAsync<T>(ClientProxyRequestContext requestContext)
{ {
var parameters = action.Parameters.GroupBy(x => x.NameOnMethod).Select(x => x.Key).ToList(); var responseContent = await RequestAsync(requestContext);
var dict = new Dictionary<string, object>();
if (typeof(T) == typeof(IRemoteStreamContent) ||
typeof(T) == typeof(RemoteStreamContent))
{
/* returning a class that holds a reference to response
* content just to be sure that GC does not dispose of
* it before we finish doing our work with the stream */
return (T)(object)new RemoteStreamContent(
await responseContent.ReadAsStreamAsync(),
responseContent.Headers?.ContentDisposition?.FileNameStar ??
RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString(),
responseContent.Headers?.ContentType?.ToString(),
responseContent.Headers?.ContentLength);
}
var stringContent = await responseContent.ReadAsStringAsync();
if (typeof(T) == typeof(string))
{
return (T)(object)stringContent;
}
if (stringContent.IsNullOrWhiteSpace())
{
return default;
}
return JsonSerializer.Deserialize<T>(stringContent);
}
protected virtual async Task<HttpContent> RequestAsync(ClientProxyRequestContext requestContext)
{
var clientConfig = ClientOptions.Value.HttpClientProxies.GetOrDefault(requestContext.ServiceType) ?? throw new AbpException($"Could not get HttpClientProxyConfig for {requestContext.ServiceType.FullName}.");
var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName);
var client = HttpClientFactory.Create(clientConfig.RemoteServiceName);
var apiVersion = await GetApiVersionInfoAsync(requestContext);
var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + await GetUrlWithParametersAsync(requestContext, apiVersion);
var requestMessage = new HttpRequestMessage(requestContext.Action.GetHttpMethod(), url)
{
Content = ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion)
};
AddHeaders(requestContext.Arguments, requestContext.Action, requestMessage, apiVersion);
if (requestContext.Action.AllowAnonymous != true)
{
await ClientAuthenticator.Authenticate(
new RemoteServiceHttpClientAuthenticateContext(
client,
requestMessage,
remoteServiceConfig,
clientConfig.RemoteServiceName
)
);
}
var response = await client.SendAsync(
requestMessage,
HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/,
GetCancellationToken(requestContext.Arguments)
);
for (var i = 0; i < parameters.Count; i++) if (!response.IsSuccessStatusCode)
{ {
dict[parameters[i]] = arguments[i]; await ThrowExceptionForResponseAsync(response);
} }
return dict; return response.Content;
}
protected virtual async Task<ApiVersionInfo> GetApiVersionInfoAsync(ClientProxyRequestContext requestContext)
{
var apiVersion = await FindBestApiVersionAsync(requestContext);
//TODO: Make names configurable?
var versionParam = requestContext.Action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ??
requestContext.Action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query);
return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion);
}
protected virtual Task<string> GetUrlWithParametersAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion)
{
return Task.FromResult(ClientProxyUrlBuilder.GenerateUrlWithParameters(requestContext.Action, requestContext.Arguments, apiVersion));
} }
private static string GetActionKey(string methodName, params object[] arguments) protected virtual Task<HttpContent> GetHttpContentAsync(ClientProxyRequestContext requestContext, ApiVersionInfo apiVersion)
{ {
return $"{typeof(TService).FullName}.{methodName}.{string.Join("-", arguments.Select(x => x.GetType().FullName))}"; return Task.FromResult(ClientProxyRequestPayloadBuilder.BuildContent(requestContext.Action, requestContext.Arguments, JsonSerializer, apiVersion));
}
protected virtual async Task<string> FindBestApiVersionAsync(ClientProxyRequestContext requestContext)
{
var configuredVersion = await GetConfiguredApiVersionAsync(requestContext);
if (requestContext.Action.SupportedVersions.IsNullOrEmpty())
{
return configuredVersion ?? "1.0";
}
if (requestContext.Action.SupportedVersions.Contains(configuredVersion))
{
return configuredVersion;
}
return requestContext.Action.SupportedVersions.Last(); //TODO: Ensure to get the latest version!
}
protected virtual async Task<string> GetConfiguredApiVersionAsync(ClientProxyRequestContext requestContext)
{
var clientConfig = ClientOptions.Value.HttpClientProxies.GetOrDefault(requestContext.ServiceType)
?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {requestContext.ServiceType.FullName}.");
return (await RemoteServiceConfigurationProvider
.GetConfigurationOrDefaultOrNullAsync(clientConfig.RemoteServiceName))?.Version;
}
protected virtual async Task ThrowExceptionForResponseAsync(HttpResponseMessage response)
{
if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat))
{
var errorResponse = JsonSerializer.Deserialize<RemoteServiceErrorResponse>(
await response.Content.ReadAsStringAsync()
);
throw new AbpRemoteCallException(errorResponse.Error)
{
HttpStatusCode = (int) response.StatusCode
};
}
throw new AbpRemoteCallException(
new RemoteServiceErrorInfo
{
Message = response.ReasonPhrase,
Code = response.StatusCode.ToString()
}
)
{
HttpStatusCode = (int) response.StatusCode
};
}
protected virtual void AddHeaders(
IReadOnlyDictionary<string, object> argumentsDictionary,
ActionApiDescriptionModel action,
HttpRequestMessage requestMessage,
ApiVersionInfo apiVersion)
{
//API Version
if (!apiVersion.Version.IsNullOrEmpty())
{
//TODO: What about other media types?
requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}");
requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}");
requestMessage.Headers.Add("api-version", apiVersion.Version);
}
//Header parameters
var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray();
foreach (var headerParameter in headers)
{
var value = HttpActionParameterHelper.FindParameterValue(argumentsDictionary, headerParameter);
if (value != null)
{
requestMessage.Headers.Add(headerParameter.Name, value.ToString());
}
}
//CorrelationId
requestMessage.Headers.Add(AbpCorrelationIdOptions.Value.HttpHeaderName, CorrelationIdProvider.Get());
//TenantId
if (CurrentTenant.Id.HasValue)
{
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
}
//Culture
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name;
if (!currentCulture.IsNullOrEmpty())
{
requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture));
}
//X-Requested-With
requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest");
}
protected virtual StringSegment RemoveQuotes(StringSegment input)
{
if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"')
{
input = input.Subsegment(1, input.Length - 2);
}
return input;
}
protected virtual CancellationToken GetCancellationToken(IReadOnlyDictionary<string, object> arguments)
{
var cancellationTokenArg = arguments.LastOrDefault();
if (cancellationTokenArg.Value is CancellationToken cancellationToken)
{
if (cancellationToken != default)
{
return cancellationToken;
}
}
return CancellationTokenProvider.Token;
} }
} }
} }

6
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuterContext.cs → framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestContext.cs

@ -3,9 +3,9 @@ using System.Collections.Generic;
using JetBrains.Annotations; using JetBrains.Annotations;
using Volo.Abp.Http.Modeling; using Volo.Abp.Http.Modeling;
namespace Volo.Abp.Http.Client.Proxying namespace Volo.Abp.Http.Client.ClientProxying
{ {
public class HttpProxyExecuterContext public class ClientProxyRequestContext
{ {
[NotNull] [NotNull]
public ActionApiDescriptionModel Action { get; } public ActionApiDescriptionModel Action { get; }
@ -16,7 +16,7 @@ namespace Volo.Abp.Http.Client.Proxying
[NotNull] [NotNull]
public Type ServiceType { get; } public Type ServiceType { get; }
public HttpProxyExecuterContext( public ClientProxyRequestContext(
[NotNull] ActionApiDescriptionModel action, [NotNull] ActionApiDescriptionModel action,
[NotNull] IReadOnlyDictionary<string, object> arguments, [NotNull] IReadOnlyDictionary<string, object> arguments,
[NotNull] Type serviceType) [NotNull] Type serviceType)

13
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/RequestPayloadBuilder.cs → framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyRequestPayloadBuilder.cs

@ -6,17 +6,18 @@ using System.Net.Http.Headers;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Volo.Abp.Content; using Volo.Abp.Content;
using Volo.Abp.Http.Client.DynamicProxying; using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Proxying;
using Volo.Abp.Http.Modeling; using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json; using Volo.Abp.Json;
namespace Volo.Abp.Http.Client.Proxying namespace Volo.Abp.Http.Client.ClientProxying
{ {
public static class RequestPayloadBuilder public class ClientProxyRequestPayloadBuilder : ITransientDependency
{ {
[CanBeNull] [CanBeNull]
public static HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion) public virtual HttpContent BuildContent(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer, ApiVersionInfo apiVersion)
{ {
var body = GenerateBody(action, methodArguments, jsonSerializer); var body = GenerateBody(action, methodArguments, jsonSerializer);
if (body != null) if (body != null)
@ -29,7 +30,7 @@ namespace Volo.Abp.Http.Client.Proxying
return body; return body;
} }
private static HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer) protected virtual HttpContent GenerateBody(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, IJsonSerializer jsonSerializer)
{ {
var parameters = action var parameters = action
.Parameters .Parameters
@ -57,7 +58,7 @@ namespace Volo.Abp.Http.Client.Proxying
return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json); return new StringContent(jsonSerializer.Serialize(value), Encoding.UTF8, MimeTypes.Application.Json);
} }
private static HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments) protected virtual HttpContent GenerateFormPostData(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments)
{ {
var parameters = action var parameters = action
.Parameters .Parameters

17
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/UrlBuilder.cs → framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs

@ -5,16 +5,17 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Volo.Abp.Http.Client.DynamicProxying; using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Proxying;
using Volo.Abp.Http.Modeling; using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Localization; using Volo.Abp.Localization;
namespace Volo.Abp.Http.Client.Proxying namespace Volo.Abp.Http.Client.ClientProxying
{ {
internal static class UrlBuilder public class ClientProxyUrlBuilder : ITransientDependency
{ {
public static string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion) public string GenerateUrlWithParameters(ActionApiDescriptionModel action, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
{ {
// The ASP.NET Core route value provider and query string value provider: // The ASP.NET Core route value provider and query string value provider:
// Treat values as invariant culture. // Treat values as invariant culture.
@ -30,7 +31,7 @@ namespace Volo.Abp.Http.Client.Proxying
} }
} }
private static void ReplacePathVariables(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion) protected virtual void ReplacePathVariables(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
{ {
var pathParameters = actionParameters var pathParameters = actionParameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Path) .Where(p => p.BindingSourceId == ParameterBindingSources.Path)
@ -72,7 +73,7 @@ namespace Volo.Abp.Http.Client.Proxying
} }
} }
private static void AddQueryStringParameters(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion) protected virtual void AddQueryStringParameters(StringBuilder urlBuilder, IList<ParameterApiDescriptionModel> actionParameters, IReadOnlyDictionary<string, object> methodArguments, ApiVersionInfo apiVersion)
{ {
var queryStringParameters = actionParameters var queryStringParameters = actionParameters
.Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query)) .Where(p => p.BindingSourceId.IsIn(ParameterBindingSources.ModelBinding, ParameterBindingSources.Query))
@ -100,7 +101,7 @@ namespace Volo.Abp.Http.Client.Proxying
} }
} }
private static bool AddQueryStringParameter( protected virtual bool AddQueryStringParameter(
StringBuilder urlBuilder, StringBuilder urlBuilder,
bool isFirstParam, bool isFirstParam,
string name, string name,
@ -133,7 +134,7 @@ namespace Volo.Abp.Http.Client.Proxying
return true; return true;
} }
private static string ConvertValueToString([CanBeNull] object value) protected virtual string ConvertValueToString([CanBeNull] object value)
{ {
if (value is DateTime dateTimeValue) if (value is DateTime dateTimeValue)
{ {

63
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs

@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy; using Volo.Abp.DynamicProxy;
using Volo.Abp.Http.Client.ClientProxying;
using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Client.Proxying;
using Volo.Abp.Http.Modeling; using Volo.Abp.Http.Modeling;
@ -15,32 +16,32 @@ namespace Volo.Abp.Http.Client.DynamicProxying
{ {
public class DynamicHttpProxyInterceptor<TService> : AbpInterceptor, ITransientDependency public class DynamicHttpProxyInterceptor<TService> : AbpInterceptor, ITransientDependency
{ {
// ReSharper disable once StaticMemberInGenericType // ReSharper disable once StaticMemberInGenericType
protected static MethodInfo MakeRequestAndGetResultAsyncMethod { get; } protected static MethodInfo CallRequestAsyncMethod { get; }
static DynamicHttpProxyInterceptor()
{
CallRequestAsyncMethod = typeof(DynamicHttpProxyInterceptor<TService>)
.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
.First(m => m.Name == nameof(CallRequestAsync) && m.IsGenericMethodDefinition);
}
public ILogger<DynamicHttpProxyInterceptor<TService>> Logger { get; set; }
protected DynamicHttpProxyInterceptorClientProxy<TService> InterceptorClientProxy { get; }
protected AbpHttpClientOptions ClientOptions { get; } protected AbpHttpClientOptions ClientOptions { get; }
protected IHttpProxyExecuter HttpProxyExecuter { get; }
protected IProxyHttpClientFactory HttpClientFactory { get; } protected IProxyHttpClientFactory HttpClientFactory { get; }
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; } protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; }
protected IApiDescriptionFinder ApiDescriptionFinder { get; } protected IApiDescriptionFinder ApiDescriptionFinder { get; }
public ILogger<DynamicHttpProxyInterceptor<TService>> Logger { get; set; }
static DynamicHttpProxyInterceptor()
{
MakeRequestAndGetResultAsyncMethod = typeof(HttpProxyExecuter)
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.First(m => m.Name == nameof(IHttpProxyExecuter.MakeRequestAndGetResultAsync) && m.IsGenericMethodDefinition);
}
public DynamicHttpProxyInterceptor( public DynamicHttpProxyInterceptor(
IHttpProxyExecuter httpProxyExecuter, DynamicHttpProxyInterceptorClientProxy<TService> interceptorClientProxy,
IOptions<AbpHttpClientOptions> clientOptions, IOptions<AbpHttpClientOptions> clientOptions,
IProxyHttpClientFactory httpClientFactory, IProxyHttpClientFactory httpClientFactory,
IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider, IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider,
IApiDescriptionFinder apiDescriptionFinder) IApiDescriptionFinder apiDescriptionFinder)
{ {
HttpProxyExecuter = httpProxyExecuter; InterceptorClientProxy = interceptorClientProxy;
HttpClientFactory = httpClientFactory; HttpClientFactory = httpClientFactory;
RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider; RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider;
ApiDescriptionFinder = apiDescriptionFinder; ApiDescriptionFinder = apiDescriptionFinder;
@ -49,34 +50,32 @@ namespace Volo.Abp.Http.Client.DynamicProxying
Logger = NullLogger<DynamicHttpProxyInterceptor<TService>>.Instance; Logger = NullLogger<DynamicHttpProxyInterceptor<TService>>.Instance;
} }
public override async Task InterceptAsync(IAbpMethodInvocation invocation) public override async Task InterceptAsync(IAbpMethodInvocation invocation)
{ {
var context = new HttpProxyExecuterContext( var context = new ClientProxyRequestContext(
await GetActionApiDescriptionModel(invocation), await GetActionApiDescriptionModel(invocation),
invocation.ArgumentsDictionary, invocation.ArgumentsDictionary,
typeof(TService)); typeof(TService));
if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty()) if (invocation.Method.ReturnType.GenericTypeArguments.IsNullOrEmpty())
{ {
await HttpProxyExecuter.MakeRequestAsync(context); await InterceptorClientProxy.CallRequestAsync(context);
} }
else else
{ {
var result = (Task)MakeRequestAndGetResultAsyncMethod var returnType = invocation.Method.ReturnType.GenericTypeArguments[0];
.MakeGenericMethod(invocation.Method.ReturnType.GenericTypeArguments[0]) var result = (Task)CallRequestAsyncMethod
.Invoke(HttpProxyExecuter, new object[] { context }); .MakeGenericMethod(returnType)
.Invoke(this, new object[] { context });
invocation.ReturnValue = await GetResultAsync( invocation.ReturnValue = await GetResultAsync(result, returnType);
result,
invocation.Method.ReturnType.GetGenericArguments()[0]
);
} }
} }
private async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel(IAbpMethodInvocation invocation) protected virtual async Task<ActionApiDescriptionModel> GetActionApiDescriptionModel(IAbpMethodInvocation invocation)
{ {
var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}."); var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(typeof(TService)) ??
throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {typeof(TService).FullName}.");
var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName); var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName);
var client = HttpClientFactory.Create(clientConfig.RemoteServiceName); var client = HttpClientFactory.Create(clientConfig.RemoteServiceName);
@ -88,13 +87,19 @@ namespace Volo.Abp.Http.Client.DynamicProxying
); );
} }
private async Task<object> GetResultAsync(Task task, Type resultType) protected virtual async Task<T> CallRequestAsync<T>(ClientProxyRequestContext context)
{
return await InterceptorClientProxy.CallRequestAsync<T>(context);
}
protected virtual async Task<object> GetResultAsync(Task task, Type resultType)
{ {
await task; await task;
return typeof(Task<>) var resultProperty = typeof(Task<>)
.MakeGenericType(resultType) .MakeGenericType(resultType)
.GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public) .GetProperty(nameof(Task<object>.Result), BindingFlags.Instance | BindingFlags.Public);
.GetValue(task); Check.NotNull(resultProperty, nameof(resultProperty));
return resultProperty.GetValue(task);
} }
} }
} }

19
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptorClientProxy.cs

@ -0,0 +1,19 @@
using System.Net.Http;
using System.Threading.Tasks;
using Volo.Abp.Http.Client.ClientProxying;
namespace Volo.Abp.Http.Client.DynamicProxying
{
public class DynamicHttpProxyInterceptorClientProxy<TService> : ClientProxyBase<TService>
{
public virtual async Task<T> CallRequestAsync<T>(ClientProxyRequestContext requestContext)
{
return await base.RequestAsync<T>(requestContext);
}
public virtual async Task<HttpContent> CallRequestAsync(ClientProxyRequestContext requestContext)
{
return await base.RequestAsync(requestContext);
}
}
}

267
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/HttpProxyExecuter.cs

@ -1,267 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Volo.Abp.Content;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Json;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Tracing;
namespace Volo.Abp.Http.Client.Proxying
{
public class HttpProxyExecuter : IHttpProxyExecuter, ITransientDependency
{
protected ICancellationTokenProvider CancellationTokenProvider { get; }
protected ICorrelationIdProvider CorrelationIdProvider { get; }
protected ICurrentTenant CurrentTenant { get; }
protected AbpCorrelationIdOptions AbpCorrelationIdOptions { get; }
protected IProxyHttpClientFactory HttpClientFactory { get; }
protected IRemoteServiceConfigurationProvider RemoteServiceConfigurationProvider { get; }
protected AbpHttpClientOptions ClientOptions { get; }
protected IJsonSerializer JsonSerializer { get; }
protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator { get; }
public HttpProxyExecuter(
ICancellationTokenProvider cancellationTokenProvider,
ICorrelationIdProvider correlationIdProvider,
ICurrentTenant currentTenant,
IOptions<AbpCorrelationIdOptions> abpCorrelationIdOptions,
IProxyHttpClientFactory httpClientFactory,
IRemoteServiceConfigurationProvider remoteServiceConfigurationProvider,
IOptions<AbpHttpClientOptions> clientOptions,
IRemoteServiceHttpClientAuthenticator clientAuthenticator,
IJsonSerializer jsonSerializer)
{
CancellationTokenProvider = cancellationTokenProvider;
CorrelationIdProvider = correlationIdProvider;
CurrentTenant = currentTenant;
AbpCorrelationIdOptions = abpCorrelationIdOptions.Value;
HttpClientFactory = httpClientFactory;
RemoteServiceConfigurationProvider = remoteServiceConfigurationProvider;
ClientOptions = clientOptions.Value;
ClientAuthenticator = clientAuthenticator;
JsonSerializer = jsonSerializer;
}
public virtual async Task<T> MakeRequestAndGetResultAsync<T>(HttpProxyExecuterContext context)
{
var responseContent = await MakeRequestAsync(context);
if (typeof(T) == typeof(IRemoteStreamContent) ||
typeof(T) == typeof(RemoteStreamContent))
{
/* returning a class that holds a reference to response
* content just to be sure that GC does not dispose of
* it before we finish doing our work with the stream */
return (T) (object) new RemoteStreamContent(
await responseContent.ReadAsStreamAsync(),
responseContent.Headers?.ContentDisposition?.FileNameStar ?? RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString(),
responseContent.Headers?.ContentType?.ToString(),
responseContent.Headers?.ContentLength);
}
var stringContent = await responseContent.ReadAsStringAsync();
if (typeof(T) == typeof(string))
{
return (T)(object)stringContent;
}
if (stringContent.IsNullOrWhiteSpace())
{
return default;
}
return JsonSerializer.Deserialize<T>(stringContent);
}
public virtual async Task<HttpContent> MakeRequestAsync(HttpProxyExecuterContext context)
{
var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType) ?? throw new AbpException($"Could not get HttpClientProxyConfig for {context.ServiceType.FullName}.");
var remoteServiceConfig = await RemoteServiceConfigurationProvider.GetConfigurationOrDefaultAsync(clientConfig.RemoteServiceName);
var client = HttpClientFactory.Create(clientConfig.RemoteServiceName);
var apiVersion = await GetApiVersionInfoAsync(context);
var url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + UrlBuilder.GenerateUrlWithParameters(context.Action, context.Arguments, apiVersion);
var requestMessage = new HttpRequestMessage(context.Action.GetHttpMethod(), url)
{
Content = RequestPayloadBuilder.BuildContent(context.Action, context.Arguments, JsonSerializer, apiVersion)
};
AddHeaders(context.Arguments, context.Action, requestMessage, apiVersion);
if (context.Action.AllowAnonymous != true)
{
await ClientAuthenticator.Authenticate(
new RemoteServiceHttpClientAuthenticateContext(
client,
requestMessage,
remoteServiceConfig,
clientConfig.RemoteServiceName
)
);
}
var response = await client.SendAsync(
requestMessage,
HttpCompletionOption.ResponseHeadersRead /*this will buffer only the headers, the content will be used as a stream*/,
GetCancellationToken(context.Arguments)
);
if (!response.IsSuccessStatusCode)
{
await ThrowExceptionForResponseAsync(response);
}
return response.Content;
}
private async Task<ApiVersionInfo> GetApiVersionInfoAsync(HttpProxyExecuterContext context)
{
var apiVersion = await FindBestApiVersionAsync(context);
//TODO: Make names configurable?
var versionParam = context.Action.Parameters.FirstOrDefault(p => p.Name == "apiVersion" && p.BindingSourceId == ParameterBindingSources.Path) ??
context.Action.Parameters.FirstOrDefault(p => p.Name == "api-version" && p.BindingSourceId == ParameterBindingSources.Query);
return new ApiVersionInfo(versionParam?.BindingSourceId, apiVersion);
}
private async Task<string> FindBestApiVersionAsync(HttpProxyExecuterContext context)
{
var configuredVersion = await GetConfiguredApiVersionAsync(context);
if (context.Action.SupportedVersions.IsNullOrEmpty())
{
return configuredVersion ?? "1.0";
}
if (context.Action.SupportedVersions.Contains(configuredVersion))
{
return configuredVersion;
}
return context.Action.SupportedVersions.Last(); //TODO: Ensure to get the latest version!
}
private async Task<string> GetConfiguredApiVersionAsync(HttpProxyExecuterContext context)
{
var clientConfig = ClientOptions.HttpClientProxies.GetOrDefault(context.ServiceType)
?? throw new AbpException($"Could not get DynamicHttpClientProxyConfig for {context.ServiceType.FullName}.");
return (await RemoteServiceConfigurationProvider
.GetConfigurationOrDefaultOrNullAsync(clientConfig.RemoteServiceName))?.Version;
}
private async Task ThrowExceptionForResponseAsync(HttpResponseMessage response)
{
if (response.Headers.Contains(AbpHttpConsts.AbpErrorFormat))
{
var errorResponse = JsonSerializer.Deserialize<RemoteServiceErrorResponse>(
await response.Content.ReadAsStringAsync()
);
throw new AbpRemoteCallException(errorResponse.Error)
{
HttpStatusCode = (int) response.StatusCode
};
}
throw new AbpRemoteCallException(
new RemoteServiceErrorInfo
{
Message = response.ReasonPhrase,
Code = response.StatusCode.ToString()
}
)
{
HttpStatusCode = (int) response.StatusCode
};
}
protected virtual void AddHeaders(
IReadOnlyDictionary<string, object> argumentsDictionary,
ActionApiDescriptionModel action,
HttpRequestMessage requestMessage,
ApiVersionInfo apiVersion)
{
//API Version
if (!apiVersion.Version.IsNullOrEmpty())
{
//TODO: What about other media types?
requestMessage.Headers.Add("accept", $"{MimeTypes.Text.Plain}; v={apiVersion.Version}");
requestMessage.Headers.Add("accept", $"{MimeTypes.Application.Json}; v={apiVersion.Version}");
requestMessage.Headers.Add("api-version", apiVersion.Version);
}
//Header parameters
var headers = action.Parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Header).ToArray();
foreach (var headerParameter in headers)
{
var value = HttpActionParameterHelper.FindParameterValue(argumentsDictionary, headerParameter);
if (value != null)
{
requestMessage.Headers.Add(headerParameter.Name, value.ToString());
}
}
//CorrelationId
requestMessage.Headers.Add(AbpCorrelationIdOptions.HttpHeaderName, CorrelationIdProvider.Get());
//TenantId
if (CurrentTenant.Id.HasValue)
{
//TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key
requestMessage.Headers.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString());
}
//Culture
//TODO: Is that the way we want? Couldn't send the culture (not ui culture)
var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name;
if (!currentCulture.IsNullOrEmpty())
{
requestMessage.Headers.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture));
}
//X-Requested-With
requestMessage.Headers.Add("X-Requested-With", "XMLHttpRequest");
}
protected virtual StringSegment RemoveQuotes(StringSegment input)
{
if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"')
{
input = input.Subsegment(1, input.Length - 2);
}
return input;
}
protected virtual CancellationToken GetCancellationToken(IReadOnlyDictionary<string, object> arguments)
{
var cancellationTokenArg = arguments.LastOrDefault();
if (cancellationTokenArg.Value is CancellationToken cancellationToken)
{
if (cancellationToken != default)
{
return cancellationToken;
}
}
return CancellationTokenProvider.Token;
}
}
}

12
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/Proxying/IHttpProxyExecuter.cs

@ -1,12 +0,0 @@
using System.Net.Http;
using System.Threading.Tasks;
namespace Volo.Abp.Http.Client.Proxying
{
public interface IHttpProxyExecuter
{
Task<HttpContent> MakeRequestAsync(HttpProxyExecuterContext context);
Task<T> MakeRequestAndGetResultAsync<T>(HttpProxyExecuterContext context);
}
}

3
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/Password/AccountProfilePasswordManagementGroupViewComponent.cs

@ -9,9 +9,6 @@ using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.Password
{ {
[Widget(
ScriptTypes = new[] { typeof(ProfileManagementScriptBundleContributor) }
)]
public class AccountProfilePasswordManagementGroupViewComponent : AbpViewComponent public class AccountProfilePasswordManagementGroupViewComponent : AbpViewComponent
{ {
private readonly IProfileAppService _profileAppService; private readonly IProfileAppService _profileAppService;

3
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/AccountProfilePersonalInfoManagementGroupViewComponent.cs

@ -9,9 +9,6 @@ using Volo.Abp.Validation;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo
{ {
[Widget(
ScriptTypes = new[] { typeof(ProfileManagementScriptBundleContributor) }
)]
public class AccountProfilePersonalInfoManagementGroupViewComponent : AbpViewComponent public class AccountProfilePersonalInfoManagementGroupViewComponent : AbpViewComponent
{ {
private readonly IProfileAppService _profileAppService; private readonly IProfileAppService _profileAppService;

13
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/ProfileManagementScriptBundleContributor.cs

@ -1,13 +0,0 @@
using System.Collections.Generic;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
namespace Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup
{
public class ProfileManagementScriptBundleContributor: BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/client-proxies/identity-proxy.js");
}
}
}

3
modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/Menus/MenuItemAdminAppService.cs

@ -60,7 +60,8 @@ namespace Volo.CmsKit.Admin.Menus
input.Order, input.Order,
input.Target, input.Target,
input.ElementId, input.ElementId,
input.CssClass input.CssClass,
CurrentTenant.Id
); );
await MenuItemRepository.InsertAsync(menuItem); await MenuItemRepository.InsertAsync(menuItem);

12
modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs

@ -23,7 +23,7 @@ namespace Volo.CmsKit.Menus
public string DisplayName { get; protected set; } public string DisplayName { get; protected set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
[NotNull] [NotNull]
public string Url { get; protected set; } public string Url { get; protected set; }
@ -39,7 +39,7 @@ namespace Volo.CmsKit.Menus
public Guid? PageId { get; protected set; } public Guid? PageId { get; protected set; }
public Guid? TenantId { get; protected set; } public Guid? TenantId { get; protected set; }
public MenuItem(Guid id, public MenuItem(Guid id,
[NotNull] string displayName, [NotNull] string displayName,
@ -50,8 +50,9 @@ namespace Volo.CmsKit.Menus
int order = 0, int order = 0,
[CanBeNull] string target = null, [CanBeNull] string target = null,
[CanBeNull] string elementId = null, [CanBeNull] string elementId = null,
[CanBeNull] string cssClass = null) [CanBeNull] string cssClass = null,
:base(id) [CanBeNull] Guid? tenantId = null)
: base(id)
{ {
SetDisplayName(displayName); SetDisplayName(displayName);
IsActive = isActive; IsActive = isActive;
@ -62,6 +63,7 @@ namespace Volo.CmsKit.Menus
Target = target; Target = target;
ElementId = elementId; ElementId = elementId;
CssClass = cssClass; CssClass = cssClass;
TenantId = tenantId;
} }
public void SetDisplayName([NotNull] string displayName) public void SetDisplayName([NotNull] string displayName)
@ -69,7 +71,7 @@ namespace Volo.CmsKit.Menus
DisplayName = Check.NotNullOrEmpty(displayName, nameof(displayName), MenuItemConsts.MaxDisplayNameLength); DisplayName = Check.NotNullOrEmpty(displayName, nameof(displayName), MenuItemConsts.MaxDisplayNameLength);
} }
public void SetUrl([NotNull]string url) public void SetUrl([NotNull] string url)
{ {
Url = Check.NotNullOrEmpty(url, nameof(url), MenuItemConsts.MaxUrlLength); Url = Check.NotNullOrEmpty(url, nameof(url), MenuItemConsts.MaxUrlLength);
} }

3
modules/setting-management/src/Volo.Abp.SettingManagement.Application/Volo/Abp/SettingManagement/EmailSettingsAppService.cs

@ -59,8 +59,9 @@ namespace Volo.Abp.SettingManagement
await SettingManager.SetForTenantOrGlobalAsync(CurrentTenant.Id, EmailSettingNames.DefaultFromDisplayName, input.DefaultFromDisplayName); await SettingManager.SetForTenantOrGlobalAsync(CurrentTenant.Id, EmailSettingNames.DefaultFromDisplayName, input.DefaultFromDisplayName);
} }
private async Task CheckFeatureAsync() protected virtual async Task CheckFeatureAsync()
{ {
await FeatureChecker.CheckEnabledAsync(SettingManagementFeatures.Enable);
if (CurrentTenant.IsAvailable) if (CurrentTenant.IsAvailable)
{ {
await FeatureChecker.CheckEnabledAsync(SettingManagementFeatures.AllowTenantsToChangeEmailSettings); await FeatureChecker.CheckEnabledAsync(SettingManagementFeatures.AllowTenantsToChangeEmailSettings);

15
modules/setting-management/src/Volo.Abp.SettingManagement.Web/Settings/EmailingPageContributor.cs

@ -48,17 +48,18 @@ namespace Volo.Abp.SettingManagement.Web.Settings
private async Task<bool> CheckFeatureAsync(SettingPageCreationContext context) private async Task<bool> CheckFeatureAsync(SettingPageCreationContext context)
{ {
var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>(); var featureCheck = context.ServiceProvider.GetRequiredService<IFeatureChecker>();
if (!await featureCheck.IsEnabledAsync(SettingManagementFeatures.Enable))
if (!currentTenant.IsAvailable)
{ {
return true; return false;
} }
var featureCheck = context.ServiceProvider.GetRequiredService<IFeatureChecker>(); if (context.ServiceProvider.GetRequiredService<ICurrentTenant>().IsAvailable)
{
return await featureCheck.IsEnabledAsync(SettingManagementFeatures.AllowTenantsToChangeEmailSettings); return await featureCheck.IsEnabledAsync(SettingManagementFeatures.AllowTenantsToChangeEmailSettings);
}
return true;
} }
} }
} }

27
npm/ng-packs/packages/core/src/lib/tests/application-configuration.service.spec.ts

@ -1,27 +0,0 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { ApplicationConfigurationService, RestService } from '../services';
describe('ApplicationConfigurationService', () => {
let spectator: SpectatorService<ApplicationConfigurationService>;
const createService = createServiceFactory({
service: ApplicationConfigurationService,
mocks: [RestService],
});
beforeEach(() => (spectator = createService()));
it('should send a GET to application-configuration API', () => {
const rest = spectator.inject(RestService);
const requestSpy = jest.spyOn(rest, 'request');
requestSpy.mockReturnValue(of(null));
spectator.service.getConfiguration().subscribe();
expect(requestSpy).toHaveBeenCalledWith(
{ method: 'GET', url: '/api/abp/application-configuration' },
{},
);
});
});

12
npm/ng-packs/packages/core/src/lib/tests/config-state.service.spec.ts

@ -1,5 +1,7 @@
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service';
import { import {
ApplicationConfigurationDto, ApplicationConfigurationDto,
CurrentUserDto, CurrentUserDto,
@ -107,14 +109,20 @@ describe('ConfigStateService', () => {
const createService = createServiceFactory({ const createService = createServiceFactory({
service: ConfigStateService, service: ConfigStateService,
imports: [HttpClientTestingModule], imports: [HttpClientTestingModule],
providers: [{ provide: CORE_OPTIONS, useValue: { skipGetAppConfiguration: true } }], providers: [
{ provide: CORE_OPTIONS, useValue: { skipGetAppConfiguration: true } },
{
provide: AbpApplicationConfigurationService,
useValue: { get: () => of(CONFIG_STATE_DATA) },
},
],
}); });
beforeEach(() => { beforeEach(() => {
spectator = createService(); spectator = createService();
configState = spectator.service; configState = spectator.service;
configState.setState(CONFIG_STATE_DATA); configState.refreshAppState();
}); });
describe('#getAll', () => { describe('#getAll', () => {

29
npm/ng-packs/packages/core/src/lib/tests/localization.service.spec.ts

@ -1,13 +1,15 @@
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest';
import { of } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service';
import { ConfigStateService, SessionStateService } from '../services'; import { ConfigStateService, SessionStateService } from '../services';
import { LocalizationService } from '../services/localization.service'; import { LocalizationService } from '../services/localization.service';
import { CORE_OPTIONS } from '../tokens/options.token'; import { CORE_OPTIONS } from '../tokens/options.token';
import { CONFIG_STATE_DATA } from './config-state.service.spec'; import { CONFIG_STATE_DATA } from './config-state.service.spec';
const appConfigData$ = new BehaviorSubject(CONFIG_STATE_DATA);
describe('LocalizationService', () => { describe('LocalizationService', () => {
let spectator: SpectatorService<LocalizationService>; let spectator: SpectatorService<LocalizationService>;
let sessionState: SpyObject<SessionStateService>; let sessionState: SpyObject<SessionStateService>;
@ -25,7 +27,7 @@ describe('LocalizationService', () => {
}, },
{ {
provide: AbpApplicationConfigurationService, provide: AbpApplicationConfigurationService,
useValue: { get: () => of(CONFIG_STATE_DATA) }, useValue: { get: () => appConfigData$ },
}, },
], ],
}); });
@ -36,8 +38,9 @@ describe('LocalizationService', () => {
configState = spectator.inject(ConfigStateService); configState = spectator.inject(ConfigStateService);
service = spectator.service; service = spectator.service;
configState.setState(CONFIG_STATE_DATA); configState.refreshAppState();
sessionState.setLanguage('tr'); sessionState.setLanguage('tr');
appConfigData$.next(CONFIG_STATE_DATA);
}); });
describe('#currentLang', () => { describe('#currentLang', () => {
@ -108,12 +111,13 @@ describe('LocalizationService', () => {
`( `(
'should return observable $expected when resource name is $resource and key is $key', 'should return observable $expected when resource name is $resource and key is $key',
async ({ resource, key, defaultValue, expected }) => { async ({ resource, key, defaultValue, expected }) => {
configState.setState({ appConfigData$.next({
localization: { localization: {
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, values: { foo: { bar: 'baz' }, x: { y: 'z' } },
defaultResourceName: 'x', defaultResourceName: 'x',
}, },
}); } as any);
configState.refreshAppState();
service.localize(resource, key, defaultValue).subscribe(result => { service.localize(resource, key, defaultValue).subscribe(result => {
expect(result).toBe(expected); expect(result).toBe(expected);
@ -149,12 +153,13 @@ describe('LocalizationService', () => {
`( `(
'should return $expected when resource name is $resource and key is $key', 'should return $expected when resource name is $resource and key is $key',
({ resource, key, defaultValue, expected }) => { ({ resource, key, defaultValue, expected }) => {
configState.setState({ appConfigData$.next({
localization: { localization: {
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, values: { foo: { bar: 'baz' }, x: { y: 'z' } },
defaultResourceName: 'x', defaultResourceName: 'x',
}, },
}); } as any);
configState.refreshAppState();
const result = service.localizeSync(resource, key, defaultValue); const result = service.localizeSync(resource, key, defaultValue);
@ -195,12 +200,13 @@ describe('LocalizationService', () => {
`( `(
'should return observable $expected when resource names are $resources and keys are $keys', 'should return observable $expected when resource names are $resources and keys are $keys',
async ({ resources, keys, defaultValue, expected }) => { async ({ resources, keys, defaultValue, expected }) => {
configState.setState({ appConfigData$.next({
localization: { localization: {
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, values: { foo: { bar: 'baz' }, x: { y: 'z' } },
defaultResourceName: 'x', defaultResourceName: 'x',
}, },
}); } as any);
configState.refreshAppState();
service.localizeWithFallback(resources, keys, defaultValue).subscribe(result => { service.localizeWithFallback(resources, keys, defaultValue).subscribe(result => {
expect(result).toBe(expected); expect(result).toBe(expected);
@ -241,12 +247,13 @@ describe('LocalizationService', () => {
`( `(
'should return $expected when resource names are $resources and keys are $keys', 'should return $expected when resource names are $resources and keys are $keys',
({ resources, keys, defaultValue, expected }) => { ({ resources, keys, defaultValue, expected }) => {
configState.setState({ appConfigData$.next({
localization: { localization: {
values: { foo: { bar: 'baz' }, x: { y: 'z' } }, values: { foo: { bar: 'baz' }, x: { y: 'z' } },
defaultResourceName: 'x', defaultResourceName: 'x',
}, },
}); } as any);
configState.refreshAppState();
const result = service.localizeWithFallbackSync(resources, keys, defaultValue); const result = service.localizeWithFallbackSync(resources, keys, defaultValue);

2
npm/ng-packs/packages/core/src/lib/tests/permission.directive.spec.ts

@ -17,7 +17,7 @@ describe('PermissionDirective', () => {
describe('with condition', () => { describe('with condition', () => {
beforeEach(() => { beforeEach(() => {
spectator = createDirective( spectator = createDirective(
`<div id="test-element" [abpPermission]="'test'">Testing Permission Directive</div>`, `<div id="test-element" *abpPermission="'test'">Testing Permission Directive</div>`,
); );
directive = spectator.directive; directive = spectator.directive;
}); });

126
npm/ng-packs/packages/core/src/lib/tests/visibility.directive.spec.ts

@ -1,126 +0,0 @@
import { SpectatorDirective, createDirectiveFactory } from '@ngneat/spectator/jest';
import { VisibilityDirective } from '../directives/visibility.directive';
describe('VisibilityDirective', () => {
let spectator: SpectatorDirective<VisibilityDirective>;
let directive: VisibilityDirective;
const createDirective = createDirectiveFactory({
directive: VisibilityDirective,
});
describe('without content', () => {
beforeEach(() => {
spectator = createDirective('<div [abpVisibility]></div>');
directive = spectator.directive;
});
it('should be created', () => {
expect(directive).toBeTruthy();
});
xit('should be removed', done => {
setTimeout(() => {
expect(spectator.query('div')).toBeFalsy();
done();
}, 0);
});
});
describe('without mutation observer and with content', () => {
beforeEach(() => {
spectator = createDirective('<div [abpVisibility]><p id="content">Content</p></div>');
directive = spectator.directive;
});
it('should not removed', done => {
setTimeout(() => {
expect(spectator.query('div')).toBeTruthy();
done();
}, 0);
});
});
describe('without mutation observer and with focused element', () => {
beforeEach(() => {
spectator = createDirective(
'<div id="main" [abpVisibility]="container"></div><div #container><p id="content">Content</p></div>',
);
directive = spectator.directive;
});
it('should not removed', done => {
setTimeout(() => {
expect(spectator.query('#main')).toBeTruthy();
done();
}, 0);
});
});
describe('without content and with focused element', () => {
beforeEach(() => {
spectator = createDirective(
'<div id="main" [abpVisibility]="container"></div><div #container></div>',
);
directive = spectator.directive;
});
xit('should be removed', done => {
setTimeout(() => {
expect(spectator.query('#main')).toBeFalsy();
done();
}, 0);
});
});
describe('with mutation observer and with content', () => {
beforeEach(() => {
spectator = createDirective('<div [abpVisibility]><div id="content">Content</div></div>');
directive = spectator.directive;
});
xit('should remove the main div element when content removed', done => {
spectator.query('#content').remove();
setTimeout(() => {
expect(spectator.query('div')).toBeFalsy();
done();
}, 0);
});
it('should not remove the main div element', done => {
spectator.query('div').appendChild(document.createElement('div'));
setTimeout(() => {
expect(spectator.query('div')).toBeTruthy();
done();
}, 100);
});
});
describe('with mutation observer and with focused element', () => {
beforeEach(() => {
spectator = createDirective(
'<div id="main" [abpVisibility]="container"></div><div #container><p id="content">Content</p></div>',
);
directive = spectator.directive;
});
xit('should remove the main div element when content removed', done => {
spectator.query('#content').remove();
setTimeout(() => {
expect(spectator.query('#main')).toBeFalsy();
done();
}, 0);
});
it('should not remove the main div element', done => {
spectator.query('#content').appendChild(document.createElement('div'));
setTimeout(() => {
expect(spectator.query('#main')).toBeTruthy();
done();
}, 100);
});
});
});

2
npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.html

@ -1,4 +1,4 @@
<abp-modal *ngIf="visible" size="lg" [(visible)]="visible" [busy]="modalBusy"> <abp-modal *ngIf="visible" [(visible)]="visible" [busy]="modalBusy" [options]="{ size: 'lg' }">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3>{{ 'AbpFeatureManagement::Features' | abpLocalization }}</h3> <h3>{{ 'AbpFeatureManagement::Features' | abpLocalization }}</h3>
</ng-template> </ng-template>

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

@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<abp-modal size="md" [(visible)]="isModalVisible" [busy]="modalBusy"> <abp-modal [(visible)]="isModalVisible" [busy]="modalBusy">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3>{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewRole') | abpLocalization }}</h3> <h3>{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewRole') | abpLocalization }}</h3>
</ng-template> </ng-template>

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

@ -29,7 +29,7 @@
</div> </div>
</div> </div>
<abp-modal size="md" [(visible)]="isModalVisible" [busy]="modalBusy"> <abp-modal [(visible)]="isModalVisible" [busy]="modalBusy">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3>{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewUser') | abpLocalization }}</h3> <h3>{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewUser') | abpLocalization }}</h3>
</ng-template> </ng-template>

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

@ -30,11 +30,11 @@
</div> </div>
</div> </div>
<abp-modal size="md" [(visible)]="isModalVisible" [busy]="modalBusy"> <abp-modal [(visible)]="isModalVisible" [busy]="modalBusy">
<ng-template #abpHeader> <ng-template #abpHeader>
<h3> <h3>
{{ {{
(selected?.id ? 'AbpTenantManagement::Edit' : 'AbpTenantManagement::NewTenant') (selected?.id ? 'AbpTenantManagement::Edit' : 'AbpTenantManagement::NewTenant')
| abpLocalization | abpLocalization
}} }}
</h3> </h3>

2
npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/tenant-box/tenant-box.component.html

@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<abp-modal size="md" [(visible)]="service.isModalVisible" [busy]="service.modalBusy"> <abp-modal [(visible)]="service.isModalVisible" [busy]="service.modalBusy">
<ng-template #abpHeader> <ng-template #abpHeader>
<h5>Switch Tenant</h5> <h5>Switch Tenant</h5>
</ng-template> </ng-template>

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

@ -1,5 +1,5 @@
import { ConfigStateService, LocalizationService } from '@abp/ng.core'; import { ConfigStateService, LocalizationService } from '@abp/ng.core';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject, of } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { PropData } from '../lib/models/props'; import { PropData } from '../lib/models/props';
import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util'; import { createEnum, createEnumOptions, createEnumValueResolver } from '../lib/utils/enum.util';
@ -109,8 +109,9 @@ describe('Enum Utils', () => {
}); });
function createMockLocalizationService() { function createMockLocalizationService() {
const configState = new ConfigStateService(null); const fakeAppConfigService = { get: () => of({ localization: mockL10n }) } as any;
configState.setState({ localization: mockL10n } as any); const configState = new ConfigStateService(fakeAppConfigService);
configState.refreshAppState();
return new LocalizationService(mockSessionState, null, null, configState); return new LocalizationService(mockSessionState, null, null, configState);
} }

5
npm/ng-packs/packages/theme-shared/extensions/src/tests/state.util.spec.ts

@ -10,8 +10,9 @@ import {
mapEntitiesToContributors, mapEntitiesToContributors,
} from '../lib/utils/state.util'; } from '../lib/utils/state.util';
const configState = new ConfigStateService(null); const fakeAppConfigService = { get: () => of(createMockState()) } as any;
configState.setState(createMockState() as any); const configState = new ConfigStateService(fakeAppConfigService);
configState.refreshAppState();
describe('State Utils', () => { describe('State Utils', () => {
describe('#getObjectExtensionEntitiesFromStore', () => { describe('#getObjectExtensionEntitiesFromStore', () => {

4
npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts

@ -143,8 +143,8 @@ export class ModalComponent implements OnInit, OnDestroy, DismissableModal {
setTimeout(() => this.listen(), 0); setTimeout(() => this.listen(), 0);
this.modalRef = this.modal.open(this.modalContent, { this.modalRef = this.modal.open(this.modalContent, {
size: 'lg', size: 'md',
centered: true, centered: false,
keyboard: false, keyboard: false,
scrollable: true, scrollable: true,
beforeDismiss: () => { beforeDismiss: () => {

34
npm/ng-packs/packages/theme-shared/src/lib/tests/modal-container.component.spec.ts

@ -1,34 +0,0 @@
import { Component, ComponentFactoryResolver, ComponentRef } from '@angular/core';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ModalContainerComponent } from '../components/modal/modal-container.component';
describe('ModalContainerComponent', () => {
@Component({ template: '<div class="foo">bar</div>' })
class TestComponent {}
let componentRef: ComponentRef<TestComponent>;
let spectator: Spectator<ModalContainerComponent>;
const createComponent = createComponentFactory({
component: ModalContainerComponent,
entryComponents: [TestComponent],
});
beforeEach(() => (spectator = createComponent()));
afterEach(() => componentRef.destroy());
describe('#container', () => {
it('should be a ViewContainerRef', () => {
let foo = document.querySelector('div.foo');
expect(foo).toBeNull();
const cfResolver = spectator.inject(ComponentFactoryResolver);
const factory = cfResolver.resolveComponentFactory(TestComponent);
componentRef = spectator.component.container.createComponent(factory);
foo = document.querySelector('div.foo');
expect(foo.textContent).toBe('bar');
});
});
});

2
npm/ng-packs/packages/theme-shared/src/lib/tests/modal.component.spec.ts

@ -103,7 +103,7 @@ describe('ModalComponent', () => {
xit('should close with the abpClose', async () => { xit('should close with the abpClose', async () => {
await wait0ms(); await wait0ms();
spectator.dispatchMouseEvent(spectator.component.abpClose, 'click'); spectator.dispatchMouseEvent(spectator.query('[abpClose]'), 'click');
await wait0ms(); await wait0ms();

87
npm/ng-packs/packages/theme-shared/src/lib/tests/modal.service.spec.ts

@ -1,87 +0,0 @@
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ModalContainerComponent } from '../components/modal/modal-container.component';
import { ModalService } from '../services';
describe('ModalContainerComponent', () => {
@Component({
template: `
<ng-template #ref>
<div class="foo">bar</div>
</ng-template>
`,
})
class TestComponent {
@ViewChild('ref', { static: true })
template: TemplateRef<any>;
constructor(public modalService: ModalService) {}
}
let spectator: Spectator<TestComponent>;
let service: ModalService;
const createComponent = createComponentFactory({
component: TestComponent,
entryComponents: [ModalContainerComponent],
});
beforeEach(() => {
spectator = createComponent();
service = spectator.component.modalService;
});
afterEach(() => {
service.getContainer().clear();
service['containerComponentRef'].changeDetectorRef.detectChanges();
service['containerComponentRef'].destroy();
});
describe('#getContainer', () => {
it('should return the ViewContainerRef of ModalContainerComponent', () => {
let foo = document.querySelector('div.foo');
expect(foo).toBeNull();
const containerRef = service.getContainer();
const embeddedViewRef = containerRef.createEmbeddedView(spectator.component.template);
foo = document.querySelector('div.foo');
expect(foo).toBe(embeddedViewRef.rootNodes[0]);
expect(foo.textContent).toBe('bar');
});
});
describe('#renderTemplate', () => {
it('should render given template using the ViewContainerRef of ModalContainerComponent', () => {
let foo = document.querySelector('div.foo');
expect(foo).toBeNull();
service.renderTemplate(spectator.component.template);
foo = document.querySelector('div.foo');
expect(foo.textContent).toBe('bar');
});
});
describe('#detectChanges', () => {
it('should call detectChanges on the containerComponentRef', () => {
const spy = jest.spyOn(service['containerComponentRef'].changeDetectorRef, 'detectChanges');
service.detectChanges();
expect(spy).toHaveBeenCalledTimes(1);
});
});
describe('#clearModal', () => {
it('should call clear on the ViewContainerRef and detectChanges', () => {
const clear = jest.spyOn(service.getContainer(), 'clear');
const detectChanges = jest.spyOn(service, 'detectChanges');
service.clearModal();
expect(clear).toHaveBeenCalledTimes(1);
expect(detectChanges).toHaveBeenCalledTimes(1);
});
});
});

46
npm/ng-packs/packages/theme-shared/src/lib/tests/sort-order-icon.component.spec.ts

@ -1,46 +0,0 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { SortOrderIconComponent } from '../components/sort-order-icon/sort-order-icon.component';
describe('SortOrderIconComponent', () => {
let spectator: SpectatorHost<SortOrderIconComponent>;
let component: SortOrderIconComponent;
const createHost = createHostFactory(SortOrderIconComponent);
beforeEach(() => {
spectator = createHost(
'<abp-sort-order-icon sortKey="testKey" [(selectedSortKey)]="selectedSortKey" [(order)]="order"></abp-sort-order-icon>',
{
hostProps: {
selectedSortKey: '',
order: '',
},
},
);
component = spectator.component;
});
test('should have correct icon class when selectedSortKey and sortKey are the same', () => {
const newKey = 'testKey';
component.sort(newKey);
expect(component.selectedSortKey).toBe(newKey);
expect(component.order).toBe('asc');
expect(component.icon).toBe('sorting_asc');
});
test("shouldn't have any icon class when sortKey and selectedSortKey are different", () => {
const newKey = 'otherKey';
component.sort(newKey);
expect(component.selectedSortKey).toBe(newKey);
expect(component.order).toBe('asc');
expect(component.icon).toBe('sorting');
});
test('should change order correctly when sort function called', () => {
component.sort('testKey');
expect(component.order).toBe('asc');
component.sort('testKey');
expect(component.order).toBe('desc');
component.sort('testKey');
expect(component.order).toBe('');
});
});

36
npm/ng-packs/packages/theme-shared/src/lib/tests/validation-utils.spec.ts

@ -1,4 +1,4 @@
import { ConfigStateService } from '@abp/ng.core'; import { AbpApplicationConfigurationService, ConfigStateService } from '@abp/ng.core';
import { CoreTestingModule } from '@abp/ng.core/testing'; import { CoreTestingModule } from '@abp/ng.core/testing';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component, Injector } from '@angular/core'; import { Component, Injector } from '@angular/core';
@ -6,6 +6,7 @@ import { Validators } from '@angular/forms';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { NgxValidateCoreModule, validatePassword } from '@ngx-validate/core'; import { NgxValidateCoreModule, validatePassword } from '@ngx-validate/core';
import { OAuthService } from 'angular-oauth2-oidc'; import { OAuthService } from 'angular-oauth2-oidc';
import { of } from 'rxjs';
import { getPasswordValidators } from '../utils'; import { getPasswordValidators } from '../utils';
@Component({ template: '', selector: 'abp-dummy' }) @Component({ template: '', selector: 'abp-dummy' })
class DummyComponent {} class DummyComponent {}
@ -16,6 +17,26 @@ describe('ValidationUtils', () => {
component: DummyComponent, component: DummyComponent,
imports: [CoreTestingModule.withConfig(), NgxValidateCoreModule.forRoot()], imports: [CoreTestingModule.withConfig(), NgxValidateCoreModule.forRoot()],
mocks: [HttpClient, OAuthService], mocks: [HttpClient, OAuthService],
providers: [
{
provide: AbpApplicationConfigurationService,
useValue: {
get: () =>
of({
setting: {
values: {
'Abp.Identity.Password.RequiredLength': '6',
'Abp.Identity.Password.RequiredUniqueChars': '1',
'Abp.Identity.Password.RequireNonAlphanumeric': 'True',
'Abp.Identity.Password.RequireLowercase': 'True',
'Abp.Identity.Password.RequireUppercase': 'True',
'Abp.Identity.Password.RequireDigit': 'True',
},
},
}),
},
},
],
}); });
beforeEach(() => (spectator = createComponent())); beforeEach(() => (spectator = createComponent()));
@ -23,18 +44,7 @@ describe('ValidationUtils', () => {
describe('#getPasswordValidators', () => { describe('#getPasswordValidators', () => {
it('should return password valdiators', () => { it('should return password valdiators', () => {
const configState = spectator.inject(ConfigStateService); const configState = spectator.inject(ConfigStateService);
configState.setState({ configState.refreshAppState();
setting: {
values: {
'Abp.Identity.Password.RequiredLength': '6',
'Abp.Identity.Password.RequiredUniqueChars': '1',
'Abp.Identity.Password.RequireNonAlphanumeric': 'True',
'Abp.Identity.Password.RequireLowercase': 'True',
'Abp.Identity.Password.RequireUppercase': 'True',
'Abp.Identity.Password.RequireDigit': 'True',
},
},
});
const validators = getPasswordValidators(spectator.inject(Injector)); const validators = getPasswordValidators(spectator.inject(Injector));
const expectedValidators = [ const expectedValidators = [

1
npm/ng-packs/packages/theme-shared/src/lib/tokens/suppress-unsaved-changes-warning.token.ts

@ -1,6 +1,5 @@
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
// TODO: Should be documented
export const SUPPRESS_UNSAVED_CHANGES_WARNING = new InjectionToken<boolean>( export const SUPPRESS_UNSAVED_CHANGES_WARNING = new InjectionToken<boolean>(
'SUPPRESS_UNSAVED_CHANGES_WARNING', 'SUPPRESS_UNSAVED_CHANGES_WARNING',
); );

9
npm/ng-packs/scripts/prod-build.ts

@ -17,15 +17,6 @@ import fse from 'fs-extra';
overwrite: true, overwrite: true,
}); });
// TODO: Will be removed in v3.1, it is added to fix the prod build error
await fse.copy(
'../node_modules/@swimlane',
'../../../templates/app/angular/node_modules/@swimlane',
{
overwrite: true,
},
);
await execa('yarn', ['ng', 'build', '--prod'], { await execa('yarn', ['ng', 'build', '--prod'], {
stdout: 'inherit', stdout: 'inherit',
cwd: '../../../templates/app/angular', cwd: '../../../templates/app/angular',

2
npm/ng-packs/tsconfig.json

@ -24,7 +24,7 @@
"@abp/ng.core/locale": ["packages/core/locale/src/public-api.ts"], "@abp/ng.core/locale": ["packages/core/locale/src/public-api.ts"],
"@abp/ng.core/testing": ["packages/core/testing/src/public-api.ts"], "@abp/ng.core/testing": ["packages/core/testing/src/public-api.ts"],
"@abp/ng.feature-management": ["packages/feature-management/src/public-api.ts"], "@abp/ng.feature-management": ["packages/feature-management/src/public-api.ts"],
"@abp/ng.identity": ["packages/identity//src/public-api.ts"], "@abp/ng.identity": ["packages/identity/src/public-api.ts"],
"@abp/ng.identity/config": ["packages/identity/config/src/public-api.ts"], "@abp/ng.identity/config": ["packages/identity/config/src/public-api.ts"],
"@abp/ng.permission-management": ["packages/permission-management/src/public-api.ts"], "@abp/ng.permission-management": ["packages/permission-management/src/public-api.ts"],
"@abp/ng.setting-management": ["packages/setting-management/src/public-api.ts"], "@abp/ng.setting-management": ["packages/setting-management/src/public-api.ts"],

Loading…
Cancel
Save