Browse Source

Merge remote-tracking branch 'abpframework/dev' into docs

pull/3164/head
liangshiwei 6 years ago
parent
commit
b4758627ee
  1. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json
  2. 4
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  3. 52
      docs/en/Application-Services.md
  4. 4
      docs/en/CLI.md
  5. 10
      docs/en/Multi-Tenancy.md
  6. 6
      docs/zh-Hans/Multi-Tenancy.md
  7. 49
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml
  8. 38
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs
  9. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs
  10. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs
  11. 8
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs
  12. 53
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs
  13. 15
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  14. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
  15. 1
      framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs
  16. 6
      framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs
  17. 341
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs
  18. 262
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs
  19. 16
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs
  20. 6
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs
  21. 34
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs
  22. 20
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs
  23. 33
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  24. 8
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs
  25. 10
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  26. 5
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs
  27. 30
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictAppService.cs
  28. 11
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictKey.cs
  29. 14
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DistrictDto.cs
  30. 2
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs
  31. 16
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Basic_Tests.cs
  32. 4
      modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj
  33. 2
      modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml
  34. 2
      modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityRoleAppService.cs
  35. 7
      modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityRoleAppService.cs
  36. 2
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Localization/en.json
  37. 1
      modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs
  38. 5
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs
  39. 8
      modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityRoleController.cs
  40. 4
      modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs
  41. 4
      modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs
  42. 12
      modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs
  43. 17
      modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateDto.cs
  44. 6
      modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateOrUpdateDtoBase.cs
  45. 11
      modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs
  46. 4
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json
  47. 4
      modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json
  48. 1
      modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs
  49. 6
      modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml
  50. 10
      modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml.cs
  51. 4
      modules/tenant-management/test/Volo.Abp.TenantManagement.Application.Tests/Volo/Abp/TenantManagement/TenantAppService_Tests.cs
  52. 3
      npm/ng-packs/apps/dev-app/src/app/shared/shared.module.ts
  53. 26
      npm/ng-packs/package.json
  54. 8
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html
  55. 6
      npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts
  56. 49
      npm/ng-packs/packages/account/src/lib/components/tenant-box/tenant-box.component.ts
  57. 13
      npm/ng-packs/packages/identity/src/lib/components/users/users.component.ts
  58. 9
      npm/ng-packs/packages/identity/src/lib/services/identity.service.ts
  59. 2
      npm/ng-packs/scripts/install-new-dependencies.ts
  60. 25
      templates/app/react-native/.eslintrc.json
  61. 14
      templates/app/react-native/.gitignore
  62. 8
      templates/app/react-native/.prettierrc
  63. 3
      templates/app/react-native/.vscode/extensions.json
  64. 25
      templates/app/react-native/App.js
  65. 31
      templates/app/react-native/Environment.js
  66. 30
      templates/app/react-native/app.json
  67. BIN
      templates/app/react-native/assets/avatar.png
  68. BIN
      templates/app/react-native/assets/icon.png
  69. BIN
      templates/app/react-native/assets/logo.png
  70. BIN
      templates/app/react-native/assets/splash.png
  71. 6
      templates/app/react-native/babel.config.js
  72. 62
      templates/app/react-native/package.json
  73. 10
      templates/app/react-native/src/api/API.js
  74. 41
      templates/app/react-native/src/api/AccountAPI.js
  75. 30
      templates/app/react-native/src/api/ApplicationConfigurationAPI.js
  76. 26
      templates/app/react-native/src/api/IdentityAPI.js
  77. 21
      templates/app/react-native/src/api/TenantManagementAPI.js
  78. 97
      templates/app/react-native/src/components/AppContainer/AppContainer.js
  79. 139
      templates/app/react-native/src/components/DataList/DataList.js
  80. 121
      templates/app/react-native/src/components/DrawerContent/DrawerContent.js
  81. 82
      templates/app/react-native/src/components/FormButtons/FormButtons.js
  82. 63
      templates/app/react-native/src/components/Loading/Loading.js
  83. 31
      templates/app/react-native/src/components/LoadingButton/LoadingButton.js
  84. 19
      templates/app/react-native/src/components/MenuIcon/MenuIcon.js
  85. 128
      templates/app/react-native/src/components/TenantBox/TenantBox.js
  86. 18
      templates/app/react-native/src/components/ValidationMessage/ValidationMessage.js
  87. 3
      templates/app/react-native/src/contexts/LocalizationContext.js
  88. 17
      templates/app/react-native/src/hocs/PermissionHOC.js
  89. 16
      templates/app/react-native/src/hooks/UsePermission.js
  90. 102
      templates/app/react-native/src/interceptors/APIInterceptor.js
  91. 22
      templates/app/react-native/src/navigators/AuthNavigator.js
  92. 20
      templates/app/react-native/src/navigators/DrawerNavigator.js
  93. 24
      templates/app/react-native/src/navigators/HomeNavigator.js
  94. 41
      templates/app/react-native/src/navigators/SettingsNavigator.js
  95. 34
      templates/app/react-native/src/navigators/TenantsNavigator.js
  96. 32
      templates/app/react-native/src/navigators/UsersNavigator.js
  97. 99
      templates/app/react-native/src/screens/ChangePassword/ChangePasswordForm.js
  98. 33
      templates/app/react-native/src/screens/ChangePassword/ChangePasswordScreen.js
  99. 84
      templates/app/react-native/src/screens/CreateUpdateTenant/CreateUpdateTenantForm.js
  100. 77
      templates/app/react-native/src/screens/CreateUpdateTenant/CreateUpdateTenantScreen.js

2
abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json

@ -1,7 +1,7 @@
{
"culture": "en",
"texts": {
"Account": "Account",
"Account": "ABP Account - Login & Register | ABP.IO",
"Welcome": "Welcome",
"UseOneOfTheFollowingLinksToContinue": "Use one of the following links to continue",
"FrameworkHomePage": "Framework home page",

4
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

@ -1,7 +1,7 @@
{
"culture": "en",
"texts": {
"GetStarted": "Get Started",
"GetStarted": "Get Started - Startup Templates",
"Create": "Create",
"NewProject": "New Project",
"DirectDownload": "Direct Download",
@ -86,7 +86,7 @@
"BasedOnFamiliarToolsExplanation": "Built on and integrated to popular tools you already know. Low learning curve, easy adaptation, comfortable development.",
"ORMIndependent": "ORM Independent",
"ORMIndependentExplanation": "The core framework is ORM/database independent and can work with any data source. Entity Framework Core and MongoDB providers are already available.",
"Features": "Features",
"Features": "Explore the ABP Framework Features",
"ABPCLI": "ABP CLI",
"Modularity": "Modularity",
"BootstrapTagHelpers": "Bootstrap Tag Helpers",

52
docs/en/Application-Services.md

@ -201,7 +201,7 @@ See the [authorization document](Authorization.md) for more.
## CRUD Application Services
If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from `CrudAppService`.
If you need to create a simple **CRUD application service** which has Create, Update, Delete and Get methods, you can use ABP's **base classes** to easily build your services. You can inherit from the `CrudAppService`.
### Example
@ -219,7 +219,9 @@ public interface IBookAppService :
}
````
* `ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface).
`ICrudAppService` has generic arguments to get the primary key type of the entity and the DTO types for the CRUD operations (it does not get the entity type since the entity type is not exposed to the clients use this interface).
> Creating interface for an application service is a good practice, but not required by the ABP Framework. You can skip the interface part.
`ICrudAppService` declares the following methods:
@ -292,6 +294,52 @@ public class BookAppService :
`CrudAppService` implements all methods declared in the `ICrudAppService` interface. You can then add your own custom methods or override and customize base methods.
> `CrudAppService` has different versions gets different number of generic arguments. Use the one suitable for you.
### AbstractKeyCrudAppService
`CrudAppService` requires to have an Id property as the primary key of your entity. If you are using composite keys then you can not utilize it.
`AbstractKeyCrudAppService` implements the same `ICrudAppService` interface, but this time without making assumption about your primary key.
#### Example
Assume that you have a `District` entity with `CityId` and `Name` as a composite primary key. Using `AbstractKeyCrudAppService` requires to implement `DeleteByIdAsync` and `GetEntityByIdAsync` methods yourself:
````csharp
public class DistrictAppService
: AbstractKeyCrudAppService<District, DistrictDto, DistrictKey>
{
public DistrictAppService(IRepository<District> repository)
: base(repository)
{
}
protected override async Task DeleteByIdAsync(DistrictKey id)
{
await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name);
}
protected override async Task<District> GetEntityByIdAsync(DistrictKey id)
{
return await AsyncQueryableExecuter.FirstOrDefaultAsync(
Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name)
);
}
}
````
This implementation requires you to create a class represents your composite key:
````csharp
public class DistrictKey
{
public Guid CityId { get; set; }
public string Name { get; set; }
}
````
## Lifetime
Lifetime of application services are [transient](Dependency-Injection.md) and they are automatically registered to the dependency injection system.

4
docs/en/CLI.md

@ -141,7 +141,7 @@ abp switch-to-preview [options]
````
#### Options
`--solution-path` or `-sp`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory.
`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
### switch-to-stable
@ -154,7 +154,7 @@ abp switch-to-stable [options]
````
#### Options
`--solution-path` or `-sp`: Specifies the solution (.sln) file path. If not specified, CLI tries to find a .sln file in the current directory.
`--solution-directory` or `-sd`: Specifies the directory. The solution should be in that directory or in any of its sub directories. If not specified, default is the current directory.
### login

10
docs/en/Multi-Tenancy.md

@ -302,7 +302,7 @@ TODO:...
Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box:
* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be stay as the first contributor for security**.
* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for security**.
* **QueryStringTenantResolver**: Tries to find current tenant id from query string parameter. Parameter name is "__tenant" by default.
* **RouteTenantResolver**: Tries to find current tenant id from route (URL path). Variable name is "__tenant" by default. So, if you defined a route with this variable, then it can determine the current tenant from the route.
* **HeaderTenantResolver**: Tries to find current tenant id from HTTP header. Header name is "__tenant" by default.
@ -343,8 +343,10 @@ namespace MyCompany.MyProject
{
Configure<AbpTenantResolveOptions>(options =>
{
//Subdomain format: {0}.mydomain.com (adding as the highest priority resolver)
options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));
//Subdomain format: {0}.mydomain.com
//Adding as the second highest priority resolver after 'CurrentUserTenantResolveContributor' to
//ensure the user cannot impersonate a different tenant.
options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));
});
//...
@ -355,7 +357,7 @@ namespace MyCompany.MyProject
{0} is the the placeholder to determine current tenant's unique name.
Instead of ``options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));`` you can use this shortcut:
Instead of ``options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));`` you can use this shortcut:
````C#
options.AddDomainTenantResolver("{0}.mydomain.com");

6
docs/zh-Hans/Multi-Tenancy.md

@ -343,8 +343,8 @@ namespace MyCompany.MyProject
{
Configure<AbpTenantResolveOptions>(options =>
{
//子域名格式: {0}.mydomain.com (作为最高优先级解析器添加)
options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));
//子域名格式: {0}.mydomain.com (作为第二优先级解析器添加, 位于CurrentUserTenantResolveContributor之后)
options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));
});
//...
@ -355,7 +355,7 @@ namespace MyCompany.MyProject
{0}是用来确定当前租户唯一名称的占位符.
你可以使用下面的方法,代替``options.TenantResolvers.Insert(0, new DomainTenantResolver("{0}.mydomain.com"));``:
你可以使用下面的方法,代替``options.TenantResolvers.Insert(1, new DomainTenantResolver("{0}.mydomain.com"));``:
````C#
options.AddDomainTenantResolver("{0}.mydomain.com");

49
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic/Themes/Basic/Layouts/Account.cshtml

@ -23,6 +23,7 @@
Layout = null;
AbpAntiForgeryManager.SetCookie();
var containerClass = ViewBag.FluidLayout == true ? "container-fluid" : "container"; //TODO: Better and type-safe options
}
<!DOCTYPE html>
@ -37,6 +38,8 @@
<title>@(ViewBag.Title == null ? BrandingProvider.AppName : ViewBag.Title)</title>
<meta name="description" content="@(ViewBag.Description != null ? ViewBag.Description as string : "Login or register to check out your ABP account. You need to be logged in to to view pay statements, generate new project and manage your license.")" />
<abp-style-bundle name="@BasicThemeBundles.Styles.Global" />
@await RenderSectionAsync("styles", false)
@ -54,32 +57,32 @@
<abp-row>
<abp-column size-md="_5" class="mx-auto">
@if (MultiTenancyOptions.Value.IsEnabled &&
(TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true))
{
<div class="card shadow-sm rounded mb-3">
<div class="card-body px-5">
<div class="row">
<div class="col">
<span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br/>
<h6 class="m-0 d-inline-block">
@if (CurrentTenant.Id == null)
{
<span>
@MultiTenancyStringLocalizer["NotSelected"]
</span>
}
else
{
<strong>@(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString())</strong>
}
</h6>
</div>
<div class="col-auto">
<a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
(TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true))
{
<div class="card shadow-sm rounded mb-3">
<div class="card-body px-5">
<div class="row">
<div class="col">
<span style="font-size: .8em;" class="text-uppercase text-muted">@MultiTenancyStringLocalizer["Tenant"]</span><br />
<h6 class="m-0 d-inline-block">
@if (CurrentTenant.Id == null)
{
<span>
@MultiTenancyStringLocalizer["NotSelected"]
</span>
}
else
{
<strong>@(CurrentTenant.Name ?? CurrentTenant.Id.Value.ToString())</strong>
}
</h6>
</div>
<div class="col-auto">
<a id="AbpTenantSwitchLink" href="javascript:;" class="btn btn-sm mt-3 btn-outline-primary">@MultiTenancyStringLocalizer["Switch"]</a>
</div>
</div>
</div>
</div>
</div>
}
@(await Component.InvokeAsync<PageAlertsViewComponent>())
@RenderBody()

38
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs

@ -12,6 +12,7 @@ using Volo.Abp.Application.Services;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Http.ProxyScripting.Generators;
using Volo.Abp.Reflection;
namespace Volo.Abp.AspNetCore.Mvc.Conventions
@ -81,7 +82,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
continue;
}
if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType))
if (!TypeHelper.IsPrimitiveExtended(prm.ParameterInfo.ParameterType, includeEnums: true))
{
if (CanUseFormBodyBinding(action, prm))
{
@ -94,7 +95,15 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
protected virtual bool CanUseFormBodyBinding(ActionModel action, ParameterModel parameter)
{
if (_options.ConventionalControllers.FormBodyBindingIgnoredTypes.Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType)))
//We want to use "id" as path parameter, not body!
if (parameter.ParameterName == "id")
{
return false;
}
if (_options.ConventionalControllers
.FormBodyBindingIgnoredTypes
.Any(t => t.IsAssignableFrom(parameter.ParameterInfo.ParameterType)))
{
return false;
}
@ -251,7 +260,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
if (!selector.ActionConstraints.OfType<HttpMethodActionConstraint>().Any())
{
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] {httpMethod}));
selector.ActionConstraints.Add(new HttpMethodActionConstraint(new[] { httpMethod }));
}
}
}
@ -295,9 +304,24 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
var url = $"api/{rootPath}/{controllerNameInUrl.ToCamelCase()}";
//Add {id} path if needed
if (action.Parameters.Any(p => p.ParameterName == "id"))
var idParameterModel = action.Parameters.FirstOrDefault(p => p.ParameterName == "id");
if (idParameterModel != null)
{
url += "/{id}";
if (TypeHelper.IsPrimitiveExtended(idParameterModel.ParameterType, includeEnums: true))
{
url += "/{id}";
}
else
{
var properties = idParameterModel
.ParameterType
.GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in properties)
{
url += "/{" + property.Name + "}";
}
}
}
//Add action name if needed
@ -341,7 +365,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
protected virtual string NormalizeUrlControllerName(string rootPath, string controllerName, ActionModel action, string httpMethod, [CanBeNull] ConventionalControllerSetting configuration)
{
if(configuration?.UrlControllerNameNormalizer == null)
if (configuration?.UrlControllerNameNormalizer == null)
{
return controllerName;
}
@ -364,7 +388,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
protected virtual bool IsEmptySelector(SelectorModel selector)
{
return selector.AttributeRouteModel == null
return selector.AttributeRouteModel == null
&& selector.ActionConstraints.IsNullOrEmpty()
&& selector.EndpointMetadata.IsNullOrEmpty();
}

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchNightlyPreviewCommand.cs

@ -29,7 +29,7 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine(" abp switch-to-preview [options]");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("-sp|--solution-path");
sb.AppendLine("-sd|--solution-directory");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SwitchStableCommand.cs

@ -29,7 +29,7 @@ namespace Volo.Abp.Cli.Commands
sb.AppendLine(" abp switch-to-stable [options]");
sb.AppendLine("");
sb.AppendLine("Options:");
sb.AppendLine("-sp|--solution-path");
sb.AppendLine("-sd|--solution-directory");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");

8
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/NuGet/NuGetService.cs

@ -95,21 +95,19 @@ namespace Volo.Abp.Cli.NuGet
.OrResult(msg => !msg.IsSuccessStatusCode)
.WaitAndRetryAsync(new[]
{
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(3),
TimeSpan.FromSeconds(7)
TimeSpan.FromSeconds(1)
},
(responseMessage, timeSpan, retryCount, context) =>
{
if (responseMessage.Exception != null)
{
Logger.LogWarning(
Logger.LogDebug(
$"{retryCount}. HTTP request attempt failed to {url} with an error: HTTP {(int)responseMessage.Result.StatusCode}-{responseMessage.Exception.Message}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}
else if (responseMessage.Result != null)
{
Logger.LogWarning(
Logger.LogDebug(
$"{retryCount}. HTTP request attempt failed to {url} with an error: {(int)responseMessage.Result.StatusCode}-{responseMessage.Result.ReasonPhrase}. " +
$"Waiting {timeSpan.TotalSeconds} secs for the next try...");
}

53
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/PackageSourceSwitcher.cs

@ -31,55 +31,74 @@ namespace Volo.Abp.Cli.ProjectModification
{
_packageSourceAdder.Add("ABP Nightly", "https://www.myget.org/F/abp-nightly/api/v3/index.json");
var solutionPath = GetSolutionPath(commandLineArgs);
var solutionFolder = GetSolutionFolder(commandLineArgs);
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
GetSolutionPath(commandLineArgs),
solutionPath,
true);
await _npmPackagesUpdater.Update(
Path.GetFileName(GetSolutionPath(commandLineArgs)),
solutionFolder,
true);
}
public async Task SwitchToStable(CommandLineArgs commandLineArgs)
{
var solutionPath = GetSolutionPath(commandLineArgs);
var solutionFolder = GetSolutionFolder(commandLineArgs);
await _nugetPackagesVersionUpdater.UpdateSolutionAsync(
GetSolutionPath(commandLineArgs),
solutionPath,
false,
true);
await _npmPackagesUpdater.Update(
Path.GetFileName(GetSolutionPath(commandLineArgs)),
false,
solutionFolder,
false,
true);
}
private string GetSolutionPath(CommandLineArgs commandLineArgs)
{
var solutionPath = commandLineArgs.Options.GetOrNull(Options.SolutionPath.Short, Options.SolutionPath.Long);
var directory = commandLineArgs.Options.GetOrNull(Options.SolutionDirectory.Short, Options.SolutionDirectory.Long)
?? Directory.GetCurrentDirectory();
var solutionPath = Directory.GetFiles(directory, "*.sln").FirstOrDefault();
if (solutionPath == null)
{
try
{
solutionPath = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.sln").Single();
}
catch (Exception)
var subDirectories = Directory.GetDirectories(directory);
foreach (var subDirectory in subDirectories)
{
Logger.LogError("There is no solution or more that one solution in current directory.");
throw;
var slnInSubDirectory = Directory.GetFiles(subDirectory, "*.sln").FirstOrDefault();
if (slnInSubDirectory != null)
{
return Path.Combine(subDirectory, slnInSubDirectory);
}
}
Logger.LogError("There is no solution or more that one solution in current directory.");
return null;
}
return solutionPath;
}
private string GetSolutionFolder(CommandLineArgs commandLineArgs)
{
return commandLineArgs.Options.GetOrNull(Options.SolutionDirectory.Short, Options.SolutionDirectory.Long)
?? Directory.GetCurrentDirectory();
}
public static class Options
{
public static class SolutionPath
public static class SolutionDirectory
{
public const string Short = "sp";
public const string Long = "solution-path";
public const string Short = "sd";
public const string Long = "solution-directory";
}
}
}

15
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs

@ -107,14 +107,18 @@ namespace Volo.Abp.Cli.ProjectModification
private async Task DownloadSourceCodesToSolutionFolder(ModuleWithMastersInfo module, string modulesFolderInSolution, string version = null)
{
var targetModuleFolder = Path.Combine(modulesFolderInSolution, module.Name);
await SourceCodeDownloadService.DownloadAsync(
module.Name,
Path.Combine(modulesFolderInSolution, module.Name),
targetModuleFolder,
version,
null,
null
);
await DeleteAppFolderAsync(targetModuleFolder);
if (module.MasterModuleInfos == null)
{
return;
@ -126,6 +130,15 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private async Task DeleteAppFolderAsync(string targetModuleFolder)
{
var appFolder = Path.Combine(targetModuleFolder, "app");
if (Directory.Exists(appFolder))
{
Directory.Delete(appFolder, true);
}
}
private async Task AddNugetAndNpmReferences(ModuleWithMastersInfo module, string[] projectFiles)
{
foreach (var nugetPackage in module.NugetPackages)

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs

@ -106,7 +106,7 @@ namespace Volo.Abp.Cli.ProjectModification
}
else
{
Logger.LogDebug("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion);
Logger.LogInformation("Package: \"{0}-v{1}\" is up to date.", packageId, packageVersion);
}
}
}

1
framework/src/Volo.Abp.Cli/Volo/Abp/Cli/Program.cs

@ -14,6 +14,7 @@ namespace Volo.Abp.Cli
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Volo.Abp", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
#if DEBUG
.MinimumLevel.Override("Volo.Abp.Cli", LogEventLevel.Debug)
#else

6
framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Services/ICrudAppService.cs

@ -5,36 +5,30 @@ namespace Volo.Abp.Application.Services
{
public interface ICrudAppService<TEntityDto, in TKey>
: ICrudAppService<TEntityDto, TKey, PagedAndSortedResultRequestDto>
where TEntityDto : IEntityDto<TKey>
{
}
public interface ICrudAppService<TEntityDto, in TKey, in TGetListInput>
: ICrudAppService<TEntityDto, TKey, TGetListInput, TEntityDto, TEntityDto>
where TEntityDto : IEntityDto<TKey>
{
}
public interface ICrudAppService<TEntityDto, in TKey, in TGetListInput, in TCreateInput>
: ICrudAppService<TEntityDto, TKey, TGetListInput, TCreateInput, TCreateInput>
where TEntityDto : IEntityDto<TKey>
{
}
public interface ICrudAppService<TEntityDto, in TKey, in TGetListInput, in TCreateInput, in TUpdateInput>
: ICrudAppService<TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntityDto : IEntityDto<TKey>
{
}
public interface ICrudAppService<TGetOutputDto, TGetListOutputDto, in TKey, in TGetListInput, in TCreateInput, in TUpdateInput>
: IApplicationService
where TGetOutputDto : IEntityDto<TKey>
where TGetListOutputDto : IEntityDto<TKey>
{
Task<TGetOutputDto> GetAsync(TKey id);

341
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs

@ -0,0 +1,341 @@
using System;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectMapping;
namespace Volo.Abp.Application.Services
{
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey>
: AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, PagedAndSortedResultRequestDto>
where TEntity : class, IEntity
{
protected AbstractKeyCrudAppService(IRepository<TEntity> repository)
: base(repository)
{
}
}
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput>
: AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TEntityDto, TEntityDto>
where TEntity : class, IEntity
{
protected AbstractKeyCrudAppService(IRepository<TEntity> repository)
: base(repository)
{
}
}
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput>
: AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TCreateInput>
where TEntity : class, IEntity
{
protected AbstractKeyCrudAppService(IRepository<TEntity> repository)
: base(repository)
{
}
}
public abstract class AbstractKeyCrudAppService<TEntity, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
: AbstractKeyCrudAppService<TEntity, TEntityDto, TEntityDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntity : class, IEntity
{
protected AbstractKeyCrudAppService(IRepository<TEntity> repository)
: base(repository)
{
}
protected override TEntityDto MapToGetListOutputDto(TEntity entity)
{
return MapToGetOutputDto(entity);
}
}
public abstract class AbstractKeyCrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
: ApplicationService,
ICrudAppService<TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntity : class, IEntity
{
public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; }
protected IRepository<TEntity> Repository { get; }
protected virtual string GetPolicyName { get; set; }
protected virtual string GetListPolicyName { get; set; }
protected virtual string CreatePolicyName { get; set; }
protected virtual string UpdatePolicyName { get; set; }
protected virtual string DeletePolicyName { get; set; }
protected AbstractKeyCrudAppService(IRepository<TEntity> repository)
{
Repository = repository;
AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance;
}
public virtual async Task<TGetOutputDto> GetAsync(TKey id)
{
await CheckGetPolicyAsync();
var entity = await GetEntityByIdAsync(id);
return MapToGetOutputDto(entity);
}
public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input)
{
await CheckGetListPolicyAsync();
var query = CreateFilteredQuery(input);
var totalCount = await AsyncQueryableExecuter.CountAsync(query);
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var entities = await AsyncQueryableExecuter.ToListAsync(query);
return new PagedResultDto<TGetListOutputDto>(
totalCount,
entities.Select(MapToGetListOutputDto).ToList()
);
}
public virtual async Task<TGetOutputDto> CreateAsync(TCreateInput input)
{
await CheckCreatePolicyAsync();
var entity = MapToEntity(input);
TryToSetTenantId(entity);
await Repository.InsertAsync(entity, autoSave: true);
return MapToGetOutputDto(entity);
}
public virtual async Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input)
{
await CheckUpdatePolicyAsync();
var entity = await GetEntityByIdAsync(id);
//TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise
MapToEntity(input, entity);
await Repository.UpdateAsync(entity, autoSave: true);
return MapToGetOutputDto(entity);
}
public virtual async Task DeleteAsync(TKey id)
{
await CheckDeletePolicyAsync();
await DeleteByIdAsync(id);
}
protected abstract Task DeleteByIdAsync(TKey id);
protected abstract Task<TEntity> GetEntityByIdAsync(TKey id);
protected virtual async Task CheckGetPolicyAsync()
{
await CheckPolicyAsync(GetPolicyName);
}
protected virtual async Task CheckGetListPolicyAsync()
{
await CheckPolicyAsync(GetListPolicyName);
}
protected virtual async Task CheckCreatePolicyAsync()
{
await CheckPolicyAsync(CreatePolicyName);
}
protected virtual async Task CheckUpdatePolicyAsync()
{
await CheckPolicyAsync(UpdatePolicyName);
}
protected virtual async Task CheckDeletePolicyAsync()
{
await CheckPolicyAsync(DeletePolicyName);
}
/// <summary>
/// Should apply sorting if needed.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="input">The input.</param>
protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TGetListInput input)
{
//Try to sort query if available
if (input is ISortedResultRequest sortInput)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
return query.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used.
if (input is ILimitedResultRequest)
{
return ApplyDefaultSorting(query);
}
//No sorting
return query;
}
/// <summary>
/// Applies sorting if no sorting specified but a limited result requested.
/// </summary>
/// <param name="query">The query.</param>
protected virtual IQueryable<TEntity> ApplyDefaultSorting(IQueryable<TEntity> query)
{
if (typeof(TEntity).IsAssignableTo<ICreationAuditedObject>())
{
return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime);
}
throw new AbpException("No sorting specified but this query requires sorting. Override the ApplyDefaultSorting method for your application service derived from AbstractKeyCrudAppService!");
}
/// <summary>
/// Should apply paging if needed.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="input">The input.</param>
protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TGetListInput input)
{
//Try to use paging if available
if (input is IPagedResultRequest pagedInput)
{
return query.PageBy(pagedInput);
}
//Try to limit query result if available
if (input is ILimitedResultRequest limitedInput)
{
return query.Take(limitedInput.MaxResultCount);
}
//No paging
return query;
}
/// <summary>
/// This method should create <see cref="IQueryable{TEntity}"/> based on given input.
/// It should filter query if needed, but should not do sorting or paging.
/// Sorting should be done in <see cref="ApplySorting"/> and paging should be done in <see cref="ApplyPaging"/>
/// methods.
/// </summary>
/// <param name="input">The input.</param>
protected virtual IQueryable<TEntity> CreateFilteredQuery(TGetListInput input)
{
return Repository;
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetOutputDto"/>.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity)
{
return ObjectMapper.Map<TEntity, TGetOutputDto>(entity);
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetListOutputDto"/>.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity)
{
return ObjectMapper.Map<TEntity, TGetListOutputDto>(entity);
}
/// <summary>
/// Maps <see cref="TCreateInput"/> to <see cref="TEntity"/> to create a new entity.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual TEntity MapToEntity(TCreateInput createInput)
{
var entity = ObjectMapper.Map<TCreateInput, TEntity>(createInput);
SetIdForGuids(entity);
return entity;
}
/// <summary>
/// Sets Id value for the entity if <see cref="TKey"/> is <see cref="Guid"/>.
/// It's used while creating a new entity.
/// </summary>
protected virtual void SetIdForGuids(TEntity entity)
{
var entityWithGuidId = entity as IEntity<Guid>;
if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty)
{
return;
}
EntityHelper.TrySetId(
entityWithGuidId,
() => GuidGenerator.Create(),
true
);
}
/// <summary>
/// Maps <see cref="TUpdateInput"/> to <see cref="TEntity"/> to update the entity.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity)
{
ObjectMapper.Map(updateInput, entity);
}
protected virtual void TryToSetTenantId(TEntity entity)
{
if (entity is IMultiTenant && HasTenantIdProperty(entity))
{
var tenantId = CurrentTenant.Id;
if (!tenantId.HasValue)
{
return;
}
var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId));
if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null)
{
return;
}
propertyInfo.SetValue(entity, tenantId);
}
}
protected virtual bool HasTenantIdProperty(TEntity entity)
{
return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null;
}
}
}

262
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs

@ -1,13 +1,10 @@
using System;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Linq;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectMapping;
namespace Volo.Abp.Application.Services
{
@ -65,274 +62,49 @@ namespace Volo.Abp.Application.Services
}
public abstract class CrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
: ApplicationService,
ICrudAppService<TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntity : class, IEntity<TKey>
: AbstractKeyCrudAppService<TEntity, TGetOutputDto, TGetListOutputDto, TKey, TGetListInput, TCreateInput, TUpdateInput>
where TEntity : class, IEntity<TKey>
where TGetOutputDto : IEntityDto<TKey>
where TGetListOutputDto : IEntityDto<TKey>
{
public IAsyncQueryableExecuter AsyncQueryableExecuter { get; set; }
protected IRepository<TEntity, TKey> Repository { get; }
protected virtual string GetPolicyName { get; set; }
protected virtual string GetListPolicyName { get; set; }
protected virtual string CreatePolicyName { get; set; }
protected virtual string UpdatePolicyName { get; set; }
protected virtual string DeletePolicyName { get; set; }
protected new IRepository<TEntity, TKey> Repository { get; }
protected CrudAppService(IRepository<TEntity, TKey> repository)
: base(repository)
{
Repository = repository;
AsyncQueryableExecuter = DefaultAsyncQueryableExecuter.Instance;
}
public virtual async Task<TGetOutputDto> GetAsync(TKey id)
protected override async Task DeleteByIdAsync(TKey id)
{
await CheckGetPolicyAsync();
var entity = await GetEntityByIdAsync(id);
return MapToGetOutputDto(entity);
}
public virtual async Task<PagedResultDto<TGetListOutputDto>> GetListAsync(TGetListInput input)
{
await CheckGetListPolicyAsync();
var query = CreateFilteredQuery(input);
var totalCount = await AsyncQueryableExecuter.CountAsync(query);
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
var entities = await AsyncQueryableExecuter.ToListAsync(query);
return new PagedResultDto<TGetListOutputDto>(
totalCount,
entities.Select(MapToGetListOutputDto).ToList()
);
}
public virtual async Task<TGetOutputDto> CreateAsync(TCreateInput input)
{
await CheckCreatePolicyAsync();
var entity = MapToEntity(input);
TryToSetTenantId(entity);
await Repository.InsertAsync(entity, autoSave: true);
return MapToGetOutputDto(entity);
}
public virtual async Task<TGetOutputDto> UpdateAsync(TKey id, TUpdateInput input)
{
await CheckUpdatePolicyAsync();
var entity = await GetEntityByIdAsync(id);
//TODO: Check if input has id different than given id and normalize if it's default value, throw ex otherwise
MapToEntity(input, entity);
await Repository.UpdateAsync(entity, autoSave: true);
return MapToGetOutputDto(entity);
}
public virtual async Task DeleteAsync(TKey id)
{
await CheckDeletePolicyAsync();
await Repository.DeleteAsync(id);
}
protected virtual Task<TEntity> GetEntityByIdAsync(TKey id)
{
return Repository.GetAsync(id);
}
protected virtual async Task CheckGetPolicyAsync()
{
await CheckPolicyAsync(GetPolicyName);
}
protected virtual async Task CheckGetListPolicyAsync()
{
await CheckPolicyAsync(GetListPolicyName);
}
protected virtual async Task CheckCreatePolicyAsync()
{
await CheckPolicyAsync(CreatePolicyName);
}
protected virtual async Task CheckUpdatePolicyAsync()
{
await CheckPolicyAsync(UpdatePolicyName);
}
protected virtual async Task CheckDeletePolicyAsync()
protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
{
await CheckPolicyAsync(DeletePolicyName);
return await Repository.GetAsync(id);
}
/// <summary>
/// Should apply sorting if needed.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="input">The input.</param>
protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity> query, TGetListInput input)
{
//Try to sort query if available
if (input is ISortedResultRequest sortInput)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
return query.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used.
if (input is ILimitedResultRequest)
{
return query.OrderByDescending(e => e.Id);
}
//No sorting
return query;
}
/// <summary>
/// Should apply paging if needed.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="input">The input.</param>
protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity> query, TGetListInput input)
{
//Try to use paging if available
if (input is IPagedResultRequest pagedInput)
{
return query.PageBy(pagedInput);
}
//Try to limit query result if available
if (input is ILimitedResultRequest limitedInput)
{
return query.Take(limitedInput.MaxResultCount);
}
//No paging
return query;
}
/// <summary>
/// This method should create <see cref="IQueryable{TEntity}"/> based on given input.
/// It should filter query if needed, but should not do sorting or paging.
/// Sorting should be done in <see cref="ApplySorting"/> and paging should be done in <see cref="ApplyPaging"/>
/// methods.
/// </summary>
/// <param name="input">The input.</param>
protected virtual IQueryable<TEntity> CreateFilteredQuery(TGetListInput input)
{
return Repository;
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetOutputDto"/>.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual TGetOutputDto MapToGetOutputDto(TEntity entity)
{
return ObjectMapper.Map<TEntity, TGetOutputDto>(entity);
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetListOutputDto"/>.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual TGetListOutputDto MapToGetListOutputDto(TEntity entity)
{
return ObjectMapper.Map<TEntity, TGetListOutputDto>(entity);
}
/// <summary>
/// Maps <see cref="TCreateInput"/> to <see cref="TEntity"/> to create a new entity.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual TEntity MapToEntity(TCreateInput createInput)
{
var entity = ObjectMapper.Map<TCreateInput, TEntity>(createInput);
SetIdForGuids(entity);
return entity;
}
/// <summary>
/// Sets Id value for the entity if <see cref="TKey"/> is <see cref="Guid"/>.
/// It's used while creating a new entity.
/// </summary>
protected virtual void SetIdForGuids(TEntity entity)
{
var entityWithGuidId = entity as IEntity<Guid>;
if (entityWithGuidId == null || entityWithGuidId.Id != Guid.Empty)
{
return;
}
EntityHelper.TrySetId(
entityWithGuidId,
() => GuidGenerator.Create(),
true
);
}
/// <summary>
/// Maps <see cref="TUpdateInput"/> to <see cref="TEntity"/> to update the entity.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
protected virtual void MapToEntity(TUpdateInput updateInput, TEntity entity)
protected override void MapToEntity(TUpdateInput updateInput, TEntity entity)
{
if (updateInput is IEntityDto<TKey> entityDto)
{
entityDto.Id = entity.Id;
}
ObjectMapper.Map(updateInput, entity);
base.MapToEntity(updateInput, entity);
}
protected virtual void TryToSetTenantId(TEntity entity)
protected override IQueryable<TEntity> ApplyDefaultSorting(IQueryable<TEntity> query)
{
if (entity is IMultiTenant && HasTenantIdProperty(entity))
if (typeof(TEntity).IsAssignableTo<ICreationAuditedObject>())
{
var tenantId = CurrentTenant.Id;
if (!tenantId.HasValue)
{
return;
}
var propertyInfo = entity.GetType().GetProperty(nameof(IMultiTenant.TenantId));
if (propertyInfo == null || propertyInfo.GetSetMethod(true) == null)
{
return;
}
propertyInfo.SetValue(entity, tenantId);
return query.OrderByDescending(e => ((ICreationAuditedObject)e).CreationTime);
}
else
{
return query.OrderByDescending(e => e.Id);
}
}
protected virtual bool HasTenantIdProperty(TEntity entity)
{
return entity.GetType().GetProperty(nameof(IMultiTenant.TenantId)) != null;
}
}
}

16
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityHelper.cs

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
namespace Volo.Abp.Domain.Entities
{
@ -13,12 +11,24 @@ namespace Volo.Abp.Domain.Entities
/// </summary>
public static class EntityHelper
{
public static bool IsEntity([NotNull] Type type)
{
return typeof(IEntity).IsAssignableFrom(type);
}
public static bool IsEntityWithId([NotNull] Type type)
{
foreach (var interfaceType in type.GetInterfaces())
{
if (interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(IEntity<>))
{
return true;
}
}
return false;
}
public static bool HasDefaultId<TKey>(IEntity<TKey> entity)
{
if (EqualityComparer<TKey>.Default.Equals(entity.Id, default))

6
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/EntityNotFoundException.cs

@ -47,7 +47,11 @@ namespace Volo.Abp.Domain.Entities
/// Creates a new <see cref="EntityNotFoundException"/> object.
/// </summary>
public EntityNotFoundException(Type entityType, object id, Exception innerException)
: base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException)
: base(
id == null
? $"There is no such an entity given given id. Entity type: {entityType.FullName}"
: $"There is no such an entity. Entity type: {entityType.FullName}, id: {id}",
innerException)
{
EntityType = entityType;
Id = id;

34
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs

@ -18,6 +18,34 @@ namespace Volo.Abp.Domain.Repositories
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>, IBasicRepository<TEntity>
where TEntity : class, IEntity
{
/// <summary>
/// Get a single entity by the given <paramref name="predicate"/>.
/// It returns null if no entity with the given <paramref name="predicate"/>.
/// It throws <see cref="InvalidOperationException"/> if there are multiple entities with the given <paramref name="predicate"/>.
/// </summary>
/// <param name="predicate">A condition to find the entity</param>
/// <param name="includeDetails">Set true to include all children of this entity</param>
/// <param name="cancellationToken">A <see cref="T:System.Threading.CancellationToken" /> to observe while waiting for the task to complete.</param>
Task<TEntity> FindAsync(
[NotNull] Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default
);
/// <summary>
/// Get a single entity by the given <paramref name="predicate"/>.
/// It throws <see cref="EntityNotFoundException"/> if there is no entity with the given <paramref name="predicate"/>.
/// It throws <see cref="InvalidOperationException"/> if there are multiple entities with the given <paramref name="predicate"/>.
/// </summary>
/// <param name="predicate">A condition to filter entities</param>
/// <param name="includeDetails">Set true to include all children of this entity</param>
/// <param name="cancellationToken">A <see cref="T:System.Threading.CancellationToken" /> to observe while waiting for the task to complete.</param>
Task<TEntity> GetAsync(
[NotNull] Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default
);
/// <summary>
/// Deletes many entities by function.
/// Notice that: All entities fits to given predicate are retrieved and deleted.
@ -30,7 +58,11 @@ namespace Volo.Abp.Domain.Repositories
/// This is useful for ORMs / database APIs those only save changes with an explicit method call, but you need to immediately save changes to the database.
/// </param>
/// <param name="cancellationToken">A <see cref="T:System.Threading.CancellationToken" /> to observe while waiting for the task to complete.</param>
Task DeleteAsync([NotNull] Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default);
Task DeleteAsync(
[NotNull] Expression<Func<TEntity, bool>> predicate,
bool autoSave = false,
CancellationToken cancellationToken = default
);
}
public interface IRepository<TEntity, TKey> : IRepository<TEntity>, IReadOnlyRepository<TEntity, TKey>, IBasicRepository<TEntity, TKey>

20
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs

@ -49,6 +49,26 @@ namespace Volo.Abp.Domain.Repositories
protected abstract IQueryable<TEntity> GetQueryable();
public abstract Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default);
public async Task<TEntity> GetAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
var entity = await FindAsync(predicate, includeDetails, cancellationToken);
if (entity == null)
{
throw new EntityNotFoundException(typeof(TEntity));
}
return entity;
}
public abstract Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default);
protected virtual TQueryable ApplyDataFilters<TQueryable>(TQueryable query)

33
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs

@ -93,6 +93,20 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return DbSet.AsQueryable();
}
public override async Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return includeDetails
? await WithDetails()
.Where(predicate)
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken))
: await DbSet
.Where(predicate)
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public override async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entities = await GetQueryable()
@ -173,18 +187,6 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
}
public virtual TEntity Get(TKey id, bool includeDetails = true)
{
var entity = Find(id, includeDetails);
if (entity == null)
{
throw new EntityNotFoundException(typeof(TEntity), id);
}
return entity;
}
public virtual async Task<TEntity> GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
var entity = await FindAsync(id, includeDetails, GetCancellationToken(cancellationToken));
@ -197,13 +199,6 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return entity;
}
public virtual TEntity Find(TKey id, bool includeDetails = true)
{
return includeDetails
? WithDetails().FirstOrDefault(e => e.Id.Equals(id))
: DbSet.Find(id);
}
public virtual async Task<TEntity> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return includeDetails

8
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs

@ -31,6 +31,14 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
return ApplyDataFilters(Collection.AsQueryable());
}
public override Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return Task.FromResult(Collection.AsQueryable().Where(predicate).SingleOrDefault());
}
public override Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entities = Collection.AsQueryable().Where(predicate).ToList();

10
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs

@ -169,6 +169,16 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
return GetMongoQueryable();
}
public override async Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await GetMongoQueryable()
.Where(predicate)
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public virtual IMongoQueryable<TEntity> GetMongoQueryable()
{
return ApplyDataFilters(

5
framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs

@ -246,6 +246,11 @@ namespace Volo.Abp.Domain.Repositories
throw new NotImplementedException();
}
public override Task<TEntity> FindAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public override Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();

30
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictAppService.cs

@ -0,0 +1,30 @@
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.TestApp.Application.Dto;
namespace Volo.Abp.TestApp.Application
{
//This is especially used to test the AbstractKeyCrudAppService
public class DistrictAppService : AbstractKeyCrudAppService<District, DistrictDto, DistrictKey>
{
public DistrictAppService(IRepository<District> repository)
: base(repository)
{
}
protected override async Task DeleteByIdAsync(DistrictKey id)
{
await Repository.DeleteAsync(d => d.CityId == id.CityId && d.Name == id.Name);
}
protected override async Task<District> GetEntityByIdAsync(DistrictKey id)
{
return await AsyncQueryableExecuter.FirstOrDefaultAsync(
Repository.Where(d => d.CityId == id.CityId && d.Name == id.Name)
);
}
}
}

11
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/DistrictKey.cs

@ -0,0 +1,11 @@
using System;
namespace Volo.Abp.TestApp.Application
{
public class DistrictKey
{
public Guid CityId { get; set; }
public string Name { get; set; }
}
}

14
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/DistrictDto.cs

@ -0,0 +1,14 @@
using System;
using Volo.Abp.Application.Dtos;
namespace Volo.Abp.TestApp.Application.Dto
{
public class DistrictDto : EntityDto
{
public Guid CityId { get; set; }
public string Name { get; set; }
public int Population { get; set; }
}
}

2
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs

@ -41,7 +41,7 @@ namespace Volo.Abp.TestApp
{
var istanbul = new City(IstanbulCityId, "Istanbul");
istanbul.Districts.Add(new District(istanbul.Id, "Bakirkoy", 1283999));
istanbul.Districts.Add(new District(istanbul.Id, "Mecidiyeky", 2222321));
istanbul.Districts.Add(new District(istanbul.Id, "Mecidiyekoy", 2222321));
istanbul.Districts.Add(new District(istanbul.Id, "Uskudar", 726172));
await _cityRepository.InsertAsync(new City(Guid.NewGuid(), "Tokyo"));

16
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/Repository_Basic_Tests.cs

@ -28,6 +28,14 @@ namespace Volo.Abp.TestApp.Testing
person.Phones.Count.ShouldBe(2);
}
[Fact]
public async Task GetAsync_With_Predicate()
{
var person = await PersonRepository.GetAsync(p => p.Name == "Douglas");
person.Name.ShouldBe("Douglas");
person.Phones.Count.ShouldBe(2);
}
[Fact]
public async Task FindAsync_Should_Return_Null_For_Not_Found_Entity()
{
@ -35,6 +43,14 @@ namespace Volo.Abp.TestApp.Testing
person.ShouldBeNull();
}
[Fact]
public async Task FindAsync_Should_Return_Null_For_Not_Found_Entity_With_Predicate()
{
var randomName = Guid.NewGuid().ToString();
var person = await PersonRepository.FindAsync(p => p.Name == randomName);
person.ShouldBeNull();
}
[Fact]
public async Task DeleteAsync()
{

4
modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj

@ -32,4 +32,8 @@
<ProjectReference Include="..\Volo.Abp.Account.Web\Volo.Abp.Account.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Security.Principal.Windows" Version="4.7.0" />
</ItemGroup>
</Project>

2
modules/blogging/src/Volo.Blogging.Web/Pages/Blogs/Posts/Index.cshtml

@ -7,7 +7,7 @@
@inject IAuthorizationService Authorization
@model Volo.Blogging.Pages.Blog.Posts.IndexModel
@{
ViewBag.PageTitle = "Blog";
ViewBag.Title = "Blog";
}
@section scripts {
<abp-script-bundle name="@typeof(IndexModel).FullName">

2
modules/identity/src/Volo.Abp.Identity.Application.Contracts/Volo/Abp/Identity/IIdentityRoleAppService.cs

@ -8,6 +8,8 @@ namespace Volo.Abp.Identity
{
public interface IIdentityRoleAppService : IApplicationService
{
Task<ListResultDto<IdentityRoleDto>> GetAllListAsync();
Task<PagedResultDto<IdentityRoleDto>> GetListAsync(PagedAndSortedResultRequestDto input);
Task<IdentityRoleDto> CreateAsync(IdentityRoleCreateDto input);

7
modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityRoleAppService.cs

@ -27,6 +27,13 @@ namespace Volo.Abp.Identity
await _roleManager.GetByIdAsync(id));
}
public virtual async Task<ListResultDto<IdentityRoleDto>> GetAllListAsync()
{
var list = await _roleRepository.GetListAsync();
return new ListResultDto<IdentityRoleDto>(
ObjectMapper.Map<List<IdentityRole>, List<IdentityRoleDto>>(list));
}
public virtual async Task<PagedResultDto<IdentityRoleDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
var list = await _roleRepository.GetListAsync(input.Sorting, input.MaxResultCount, input.SkipCount);

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

@ -82,6 +82,7 @@
"DisplayName:Abp.Identity.Lockout.LockoutDuration": "Lockout duration(seconds)",
"DisplayName:Abp.Identity.Lockout.MaxFailedAccessAttempts": "Max failed access attempts",
"DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail": "Require confirmed email",
"DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Enable phone number confirmation",
"DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Require confirmed phoneNumber",
"DisplayName:Abp.Identity.User.IsUserNameUpdateEnabled": "Is username update enabled",
"DisplayName:Abp.Identity.User.IsEmailUpdateEnabled": "Is email update enabled",
@ -95,6 +96,7 @@
"Description:Abp.Identity.Lockout.LockoutDuration": "The duration a user is locked out for when a lockout occurs.",
"Description:Abp.Identity.Lockout.MaxFailedAccessAttempts": "The number of failed access attempts allowed before a user is locked out, assuming lock out is enabled.",
"Description:Abp.Identity.SignIn.RequireConfirmedEmail": "Whether a confirmed email address is required to sign in.",
"Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation": "Whether the phoneNumber can be confirmed by the user.",
"Description:Abp.Identity.SignIn.RequireConfirmedPhoneNumber": "Whether a confirmed telephone number is required to sign in.",
"Description:Abp.Identity.User.IsUserNameUpdateEnabled": "Whether the username can be updated by the user.",
"Description:Abp.Identity.User.IsEmailUpdateEnabled": "Whether the email can be updated by the user."

1
modules/identity/src/Volo.Abp.Identity.Domain.Shared/Volo/Abp/Identity/Settings/IdentitySettingNames.cs

@ -30,6 +30,7 @@
private const string SignInPrefix = Prefix + ".SignIn";
public const string RequireConfirmedEmail = SignInPrefix + ".RequireConfirmedEmail";
public const string EnablePhoneNumberConfirmation = SignInPrefix + ".EnablePhoneNumberConfirmation";
public const string RequireConfirmedPhoneNumber = SignInPrefix + ".RequireConfirmedPhoneNumber";
}

5
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentitySettingDefinitionProvider.cs

@ -72,6 +72,11 @@ namespace Volo.Abp.Identity
false.ToString(), L("DisplayName:Abp.Identity.SignIn.RequireConfirmedEmail"),
L("Description:Abp.Identity.SignIn.RequireConfirmedEmail"),
true),
new SettingDefinition(
IdentitySettingNames.SignIn.EnablePhoneNumberConfirmation,
true.ToString(), L("DisplayName:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"),
L("Description:Abp.Identity.SignIn.EnablePhoneNumberConfirmation"),
true),
new SettingDefinition(
IdentitySettingNames.SignIn.RequireConfirmedPhoneNumber,
false.ToString(), L("DisplayName:Abp.Identity.SignIn.RequireConfirmedPhoneNumber"),

8
modules/identity/src/Volo.Abp.Identity.HttpApi/Volo/Abp/Identity/IdentityRoleController.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
@ -19,6 +20,13 @@ namespace Volo.Abp.Identity
_roleAppService = roleAppService;
}
[HttpGet]
[Route("all")]
public virtual Task<ListResultDto<IdentityRoleDto>> GetAllListAsync()
{
return _roleAppService.GetAllListAsync();
}
[HttpGet]
public virtual Task<PagedResultDto<IdentityRoleDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{

4
modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/CreateModal.cshtml.cs

@ -29,9 +29,9 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Users
{
UserInfo = new UserInfoViewModel();
var roleDtoList = await _identityRoleAppService.GetListAsync(new PagedAndSortedResultRequestDto());
var roleDtoList = (await _identityRoleAppService.GetAllListAsync()).Items;
Roles = ObjectMapper.Map<IReadOnlyList<IdentityRoleDto>, AssignedRoleViewModel[]>(roleDtoList.Items);
Roles = ObjectMapper.Map<IReadOnlyList<IdentityRoleDto>, AssignedRoleViewModel[]>(roleDtoList);
foreach (var role in Roles)
{

4
modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs

@ -31,9 +31,7 @@ namespace Volo.Abp.Identity.Web.Pages.Identity.Users
{
UserInfo = ObjectMapper.Map<IdentityUserDto, UserInfoViewModel>(await _identityUserAppService.GetAsync(id));
Roles = ObjectMapper.Map<IReadOnlyList<IdentityRoleDto>, AssignedRoleViewModel[]>(
(await _identityRoleAppService.GetListAsync(new PagedAndSortedResultRequestDto())).Items
);
Roles = ObjectMapper.Map<IReadOnlyList<IdentityRoleDto>, AssignedRoleViewModel[]>((await _identityRoleAppService.GetAllListAsync()).Items);
var userRoleNames = (await _identityUserAppService.GetRolesAsync(UserInfo.Id)).Items.Select(r => r.Name).ToList();
foreach (var role in Roles)

12
modules/identity/test/Volo.Abp.Identity.Application.Tests/Volo/Abp/Identity/IdentityRoleAppService_Tests.cs

@ -34,6 +34,18 @@ namespace Volo.Abp.Identity
result.Id.ShouldBe(moderator.Id);
}
[Fact]
public async Task GetAllListAsync()
{
//Act
var result = await _roleAppService.GetAllListAsync();
//Assert
result.Items.Count.ShouldBeGreaterThan(0);
}
[Fact]
public async Task GetListAsync()
{

17
modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateDto.cs

@ -1,7 +1,22 @@
namespace Volo.Abp.TenantManagement
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Volo.Abp.TenantManagement.Localization;
namespace Volo.Abp.TenantManagement
{
public class TenantCreateDto : TenantCreateOrUpdateDtoBase
{
[Required]
[EmailAddress]
[MaxLength(256)]
public string AdminEmailAddress { get; set; }
[Required]
[MaxLength(128)]
public string AdminPassword { get; set; }
}
}

6
modules/tenant-management/src/Volo.Abp.TenantManagement.Application.Contracts/Volo/Abp/TenantManagement/TenantCreateOrUpdateDtoBase.cs

@ -1,7 +1,11 @@
namespace Volo.Abp.TenantManagement
using System.ComponentModel.DataAnnotations;
namespace Volo.Abp.TenantManagement
{
public abstract class TenantCreateOrUpdateDtoBase
{
[Required]
[StringLength(TenantConsts.MaxNameLength)]
public string Name { get; set; }
}
}

11
modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/TenantAppService.cs

@ -15,7 +15,7 @@ namespace Volo.Abp.TenantManagement
protected ITenantManager TenantManager { get; }
public TenantAppService(
ITenantRepository tenantRepository,
ITenantRepository tenantRepository,
ITenantManager tenantManager,
IDataSeeder dataSeeder)
{
@ -51,10 +51,13 @@ namespace Volo.Abp.TenantManagement
{
//TODO: Handle database creation?
//TODO: Set admin email & password..?
await DataSeeder.SeedAsync(tenant.Id);
await DataSeeder.SeedAsync(
new DataSeedContext(tenant.Id)
.WithProperty("AdminEmail", input.AdminEmailAddress)
.WithProperty("AdminPassword", input.AdminPassword)
);
}
return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}

4
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json

@ -15,6 +15,8 @@
"Permission:Edit": "Edit",
"Permission:Delete": "Delete",
"Permission:ManageConnectionStrings": "Manage connection strings",
"Permission:ManageFeatures": "Manage features"
"Permission:ManageFeatures": "Manage features",
"DisplayName:AdminEmailAddress": "Admin Email Address",
"DisplayName:AdminPassword": "Admin Password"
}
}

4
modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json

@ -15,6 +15,8 @@
"Permission:Edit": "Düzenleme",
"Permission:Delete": "Silme",
"Permission:ManageConnectionStrings": "Bağlantı cümlelerini yönet",
"Permission:ManageFeatures": "Özellikleri yönet"
"Permission:ManageFeatures": "Özellikleri yönet",
"DisplayName:AdminEmailAddress": "Admin Eposta Adresi",
"DisplayName:AdminPassword": "Admin Şifresi"
}
}

1
modules/tenant-management/src/Volo.Abp.TenantManagement.HttpApi/Volo/Abp/TenantManagement/TenantController.cs

@ -35,6 +35,7 @@ namespace Volo.Abp.TenantManagement
[HttpPost]
public virtual Task<TenantDto> CreateAsync(TenantCreateDto input)
{
ValidateModel();
return _service.CreateAsync(input);
}

6
modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml

@ -12,7 +12,11 @@
<abp-modal>
<abp-modal-header title="@L["NewTenant"]"></abp-modal-header>
<abp-modal-body>
<abp-input asp-for="Tenant.Name" label="@L["TenantName"].Value" />
<abp-input asp-for="Tenant.Name" />
<abp-input asp-for="Tenant.AdminEmailAddress" />
<abp-input asp-for="Tenant.AdminPassword" />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>

10
modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/CreateModal.cshtml.cs

@ -31,8 +31,16 @@ namespace Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants
{
[Required]
[StringLength(TenantConsts.MaxNameLength)]
[Display(Name = "DisplayName:TenantName")]
public string Name { get; set; }
[Required]
[EmailAddress]
[MaxLength(256)]
public string AdminEmailAddress { get; set; }
[Required]
[MaxLength(128)]
public string AdminPassword { get; set; }
}
}
}

4
modules/tenant-management/test/Volo.Abp.TenantManagement.Application.Tests/Volo/Abp/TenantManagement/TenantAppService_Tests.cs

@ -59,7 +59,7 @@ namespace Volo.Abp.TenantManagement
public async Task CreateAsync()
{
var tenancyName = Guid.NewGuid().ToString("N").ToLowerInvariant();
var tenant = await _tenantAppService.CreateAsync(new TenantCreateDto { Name = tenancyName });
var tenant = await _tenantAppService.CreateAsync(new TenantCreateDto { Name = tenancyName , AdminEmailAddress = "admin@admin.com", AdminPassword = "123456"});
tenant.Name.ShouldBe(tenancyName);
tenant.Id.ShouldNotBe(default(Guid));
}
@ -69,7 +69,7 @@ namespace Volo.Abp.TenantManagement
{
await Assert.ThrowsAsync<UserFriendlyException>(async () =>
{
await _tenantAppService.CreateAsync(new TenantCreateDto { Name = "acme" });
await _tenantAppService.CreateAsync(new TenantCreateDto { Name = "acme", AdminEmailAddress = "admin@admin.com", AdminPassword = "123456" });
});
}

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

@ -3,7 +3,6 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { NgModule } from '@angular/core';
import { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { TableModule } from 'primeng/table';
import { NgxValidateCoreModule } from '@ngx-validate/core';
@NgModule({
@ -12,7 +11,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
CoreModule,
ThemeSharedModule,
ThemeBasicModule,
TableModule,
NgbDropdownModule,
NgxValidateCoreModule,
],
@ -20,7 +18,6 @@ import { NgxValidateCoreModule } from '@ngx-validate/core';
CoreModule,
ThemeSharedModule,
ThemeBasicModule,
TableModule,
NgbDropdownModule,
NgxValidateCoreModule,
],

26
npm/ng-packs/package.json

@ -21,19 +21,19 @@
"generate:changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"devDependencies": {
"@abp/ng.account": "~2.1.0",
"@abp/ng.account.config": "~2.1.0",
"@abp/ng.core": "~2.1.0",
"@abp/ng.feature-management": "~2.1.0",
"@abp/ng.identity": "~2.1.0",
"@abp/ng.identity.config": "~2.1.0",
"@abp/ng.permission-management": "~2.1.0",
"@abp/ng.setting-management": "~2.1.0",
"@abp/ng.setting-management.config": "~2.1.0",
"@abp/ng.tenant-management": "~2.1.0",
"@abp/ng.tenant-management.config": "~2.1.0",
"@abp/ng.theme.basic": "~2.1.0",
"@abp/ng.theme.shared": "~2.1.0",
"@abp/ng.account": "~2.2.0",
"@abp/ng.account.config": "~2.2.0",
"@abp/ng.core": "~2.2.0",
"@abp/ng.feature-management": "~2.2.0",
"@abp/ng.identity": "~2.2.0",
"@abp/ng.identity.config": "~2.2.0",
"@abp/ng.permission-management": "~2.2.0",
"@abp/ng.setting-management": "~2.2.0",
"@abp/ng.setting-management.config": "~2.2.0",
"@abp/ng.tenant-management": "~2.2.0",
"@abp/ng.tenant-management.config": "~2.2.0",
"@abp/ng.theme.basic": "~2.2.0",
"@abp/ng.theme.shared": "~2.2.0",
"@angular-builders/jest": "^8.2.0",
"@angular-devkit/build-angular": "~0.803.21",
"@angular-devkit/build-ng-packagr": "~0.803.21",

8
npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.html

@ -1,8 +1,10 @@
<div class="row">
<div class="mx-auto col col-md-5">
<abp-tenant-box
*abpReplaceableTemplate="{ componentKey: 'Account.TenantBoxComponent' }"
></abp-tenant-box>
<ng-container *ngIf="isMultiTenancyEnabled$ | async">
<abp-tenant-box
*abpReplaceableTemplate="{ componentKey: 'Account.TenantBoxComponent' }"
></abp-tenant-box>
</ng-container>
<div class="abp-account-container">
<div

6
npm/ng-packs/packages/account/src/lib/components/auth-wrapper/auth-wrapper.component.ts

@ -1,6 +1,7 @@
import { ConfigState, takeUntilDestroy } from '@abp/ng.core';
import { Component, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Store } from '@ngxs/store';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { Account } from '../../models/account';
@Component({
@ -20,6 +21,9 @@ export class AuthWrapperComponent
@Input()
readonly cancelContentRef: TemplateRef<any>;
@Select(ConfigState.getDeep('multiTenancy.isEnabled'))
isMultiTenancyEnabled$: Observable<boolean>;
enableLocalLogin = true;
constructor(private store: Store) {}

49
npm/ng-packs/packages/account/src/lib/components/tenant-box/tenant-box.component.ts

@ -1,9 +1,9 @@
import { ABP, SetTenant, SessionState } from '@abp/ng.core';
import { ABP, SetTenant, SessionState, GetAppConfiguration } from '@abp/ng.core';
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { throwError } from 'rxjs';
import { catchError, take, finalize } from 'rxjs/operators';
import { catchError, take, finalize, switchMap } from 'rxjs/operators';
import snq from 'snq';
import { AccountService } from '../../services/account.service';
import { Account } from '../../models/account';
@ -52,29 +52,32 @@ export class TenantBoxComponent
);
return throwError(err);
}),
switchMap(({ success, tenantId }) => {
if (success) {
this.tenant = {
id: tenantId,
name: this.tenant.name,
};
this.tenantName = this.tenant.name;
this.isModalVisible = false;
} else {
this.toasterService.error(
'AbpUiMultiTenancy::GivenTenantIsNotAvailable',
'AbpUi::Error',
{
messageLocalizationParams: [this.tenant.name],
},
);
this.tenant = {} as ABP.BasicItem;
this.tenantName = '';
}
this.store.dispatch(new SetTenant(success ? this.tenant : null));
return this.store.dispatch(new GetAppConfiguration());
}),
)
.subscribe(({ success, tenantId }) => {
if (success) {
this.tenant = {
id: tenantId,
name: this.tenant.name,
};
this.tenantName = this.tenant.name;
this.isModalVisible = false;
} else {
this.toasterService.error(
'AbpUiMultiTenancy::GivenTenantIsNotAvailable',
'AbpUi::Error',
{
messageLocalizationParams: [this.tenant.name],
},
);
this.tenant = {} as ABP.BasicItem;
}
this.store.dispatch(new SetTenant(success ? this.tenant : null));
});
.subscribe();
} else {
this.store.dispatch(new SetTenant(null));
this.store.dispatch([new SetTenant(null), new GetAppConfiguration()]);
this.tenantName = null;
this.isModalVisible = false;
}

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

@ -1,5 +1,5 @@
import { ABP, ConfigState } from '@abp/ng.core';
import { ConfirmationService, Toaster } from '@abp/ng.theme.shared';
import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared';
import { Component, OnInit, TemplateRef, TrackByFunction, ViewChild } from '@angular/core';
import {
AbstractControl,
@ -17,7 +17,6 @@ import snq from 'snq';
import {
CreateUser,
DeleteUser,
GetRoles,
GetUserById,
GetUserRoles,
GetUsers,
@ -25,6 +24,7 @@ import {
} from '../../actions/identity.actions';
import { Identity } from '../../models/identity';
import { IdentityState } from '../../states/identity.state';
import { IdentityService } from '../../services/identity.service';
@Component({
selector: 'abp-users',
templateUrl: './users.component.html',
@ -81,6 +81,7 @@ export class UsersComponent implements OnInit {
private confirmationService: ConfirmationService,
private fb: FormBuilder,
private store: Store,
private identityService: IdentityService,
) {}
ngOnInit() {
@ -119,8 +120,8 @@ export class UsersComponent implements OnInit {
}
buildForm() {
this.store.dispatch(new GetRoles({ maxResultCount: 1000, skipCount: 0 })).subscribe(() => {
this.roles = this.store.selectSnapshot(IdentityState.getRoles);
this.identityService.getAllRoles().subscribe(({ items }) => {
this.roles = items;
this.form = this.fb.group({
userName: [this.selected.userName || '', [Validators.required, Validators.maxLength(256)]],
email: [
@ -223,8 +224,8 @@ export class UsersComponent implements OnInit {
.warn('AbpIdentity::UserDeletionConfirmationMessage', 'AbpIdentity::AreYouSure', {
messageLocalizationParams: [userName],
})
.subscribe((status: Toaster.Status) => {
if (status === Toaster.Status.confirm) {
.subscribe((status: Confirmation.Status) => {
if (status === Confirmation.Status.confirm) {
this.store.dispatch(new DeleteUser(id)).subscribe(() => this.get());
}
});

9
npm/ng-packs/packages/identity/src/lib/services/identity.service.ts

@ -19,6 +19,15 @@ export class IdentityService {
return this.rest.request<null, Identity.RoleResponse>(request);
}
getAllRoles(): Observable<Identity.RoleResponse> {
const request: Rest.Request<null> = {
method: 'GET',
url: '/api/identity/roles/all',
};
return this.rest.request<null, Identity.RoleResponse>(request);
}
getRoleById(id: string): Observable<Identity.RoleItem> {
const request: Rest.Request<null> = {
method: 'GET',

2
npm/ng-packs/scripts/install-new-dependencies.ts

@ -17,7 +17,7 @@ const updateAndInstall = async () => {
...packageJson.devDependencies,
...dependencies,
...peerDependencies,
...{ [name]: `^${version}` },
...{ [name]: `~${version}` },
};
packageJson.devDependencies = Object.keys(packageJson.devDependencies)

25
templates/app/react-native/.eslintrc.json

@ -0,0 +1,25 @@
{
"extends": ["airbnb", "prettier", "prettier/react"],
"parser": "babel-eslint",
"env": {
"jest": true
},
"rules": {
"no-use-before-define": 0,
"react/jsx-filename-extension": 0,
"react/prop-types": ["error", { "ignore": ["navigation", "children"] }],
"react/require-default-props": 0,
"react/jsx-props-no-spreading": 0,
"react/forbid-prop-types": 0,
"import/prefer-default-export": 0,
"comma-dangle": 0,
"no-underscore-dangle": 1,
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
"no-param-reassign": 0,
"operator-linebreak": 0,
"global-require": 0
},
"globals": {
"fetch": false
}
}

14
templates/app/react-native/.gitignore

@ -0,0 +1,14 @@
node_modules/**/*
.expo/*
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
web-report/
# macOS
.DS_Store

8
templates/app/react-native/.prettierrc

@ -0,0 +1,8 @@
{
"trailingComma": "all",
"singleQuote": true,
"jsxSingleQuote": false,
"printWidth": 100,
"semi": true,
"jsxBracketSameLine": true
}

3
templates/app/react-native/.vscode/extensions.json

@ -0,0 +1,3 @@
{
"recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
}

25
templates/app/react-native/App.js

@ -0,0 +1,25 @@
import { StyleProvider } from 'native-base';
import React from 'react';
import { enableScreens } from 'react-native-screens';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import AppContainer from './src/components/AppContainer/AppContainer';
import { store, persistor } from './src/store';
import getTheme from './src/theme/components';
import { activeTheme } from './src/theme/variables';
import { initAPIInterceptor } from './src/interceptors/APIInterceptor';
enableScreens();
initAPIInterceptor(store);
export default function App() {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<StyleProvider style={getTheme(activeTheme)}>
<AppContainer />
</StyleProvider>
</PersistGate>
</Provider>
);
}

31
templates/app/react-native/Environment.js

@ -0,0 +1,31 @@
const ENV = {
dev: {
apiUrl: 'http://localhost:44305',
oAuthConfig: {
issuer: 'http://localhost:44305',
clientId: 'MyProjectName_App',
clientSecret: '1q2w3e*',
scope: 'MyProjectName',
},
localization: {
defaultResourceName: 'MyProjectName',
},
},
prod: {
apiUrl: 'http://localhost:44305',
oAuthConfig: {
issuer: 'http://localhost:44305',
clientId: 'MyProjectName_App',
clientSecret: '1q2w3e*',
scope: 'MyProjectName',
},
localization: {
defaultResourceName: 'MyProjectName',
},
},
};
export const getEnvVars = () => {
// eslint-disable-next-line no-undef
return __DEV__ ? ENV.dev : ENV.prod;
};

30
templates/app/react-native/app.json

@ -0,0 +1,30 @@
{
"expo": {
"name": "MyProjectName",
"slug": "MyProjectName",
"privacy": "public",
"sdkVersion": "36.0.0",
"platforms": ["ios", "android", "web"],
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#38003c"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.MyCompanyName.MyProjectName",
"buildNumber": "1.0.0"
},
"android": {
"package": "com.MyCompanyName.MyProjectName",
"versionCode": 1
}
}
}

BIN
templates/app/react-native/assets/avatar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

BIN
templates/app/react-native/assets/icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
templates/app/react-native/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
templates/app/react-native/assets/splash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

6
templates/app/react-native/babel.config.js

@ -0,0 +1,6 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
};
};

62
templates/app/react-native/package.json

@ -0,0 +1,62 @@
{
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
"lint": "eslint *.js **/*.js",
"lint:fix": "yarn lint --fix"
},
"dependencies": {
"@expo/vector-icons": "^10.0.6",
"@react-native-community/masked-view": "0.1.5",
"@react-navigation/drawer": "^5.1.1",
"@react-navigation/native": "^5.0.9",
"@react-navigation/stack": "^5.1.1",
"@reduxjs/toolkit": "^1.2.3",
"axios": "^0.19.2",
"color": "^3.1.2",
"expo": "~36.0.0",
"expo-constants": "~8.0.0",
"expo-font": "~8.0.0",
"formik": "^2.1.2",
"i18n-js": "^3.5.1",
"lodash": "^4.17.15",
"native-base": "^2.13.8",
"prop-types": "^15.7.2",
"react": "~16.9.0",
"react-dom": "~16.9.0",
"react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz",
"react-native-gesture-handler": "~1.5.0",
"react-native-reanimated": "~1.4.0",
"react-native-safe-area-context": "0.6.0",
"react-native-safe-area-view": "^1.0.0",
"react-native-screens": "2.0.0-alpha.12",
"react-native-web": "~0.11.7",
"react-redux": "^7.1.3",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"reselect": "^4.0.0",
"yup": "^0.28.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@types/i18n-js": "^3.0.1",
"@types/react": "~16.9.0",
"@types/react-native": "~0.60.23",
"@types/react-redux": "^7.1.7",
"@types/yup": "^0.26.29",
"babel-eslint": "^10.0.3",
"babel-preset-expo": "~8.0.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.18.3",
"prettier": "^1.19.1"
},
"private": true
}

10
templates/app/react-native/src/api/API.js

@ -0,0 +1,10 @@
import axios from 'axios';
import { getEnvVars } from '../../Environment';
const { apiUrl } = getEnvVars();
const axiosInstance = axios.create({
baseURL: apiUrl,
});
export default axiosInstance;

41
templates/app/react-native/src/api/AccountAPI.js

@ -0,0 +1,41 @@
import api from './API';
import { getEnvVars } from '../../Environment';
const { oAuthConfig } = getEnvVars();
export const login = ({ username, password }) => {
// eslint-disable-next-line no-undef
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
formData.append('grant_type', 'password');
formData.append('scope', `${oAuthConfig.scope} offline_access`);
formData.append('client_id', oAuthConfig.clientId);
formData.append('client_secret', oAuthConfig.clientSecret);
return api({
method: 'POST',
url: '/connect/token',
headers: { 'Content-Type': 'multipart/form-data' },
data: formData,
baseURL: oAuthConfig.issuer,
}).then(({ data }) => data);
};
export const logout = () =>
api({
method: 'GET',
url: '/api/account/logout',
}).then(({ data }) => data);
export const getTenant = tenantName =>
api({
method: 'GET',
url: `/api/abp/multi-tenancy/tenants/by-name/${tenantName}`,
}).then(({ data }) => data);
export const getTenantById = tenantId =>
api({
method: 'GET',
url: `/api/abp/multi-tenancy/tenants/by-id/${tenantId}`,
}).then(({ data }) => data);

30
templates/app/react-native/src/api/ApplicationConfigurationAPI.js

@ -0,0 +1,30 @@
import i18n from 'i18n-js';
import api from './API';
export const getApplicationConfiguration = () =>
api
.get('/api/abp/application-configuration')
.then(({ data }) => data)
.then(async config => {
const { cultureName } = config.localization.currentCulture;
i18n.locale = cultureName;
Object.keys(config.localization.values).forEach(key => {
const resource = config.localization.values[key];
if (typeof resource !== 'object') return;
Object.keys(resource).forEach(key2 => {
if (/'{|{/g.test(resource[key2])) {
resource[key2] = resource[key2].replace(/'{|{/g, '{{').replace(/}'|}/g, '}}');
}
});
});
i18n.translations[cultureName] = {
...config.localization.values,
...(i18n.translations[cultureName] || {}),
};
return config;
});

26
templates/app/react-native/src/api/IdentityAPI.js

@ -0,0 +1,26 @@
import api from './API';
export const getProfileDetail = () => api.get('/api/identity/my-profile').then(({ data }) => data);
export const getAllRoles = () => api.get('/api/identity/roles/all').then(({ data }) => data.items);
export const getUserRoles = id =>
api.get(`/api/identity/users/${id}/roles`).then(({ data }) => data.items);
export const getUsers = (params = { maxResultCount: 10, skipCount: 0 }) =>
api.get('/api/identity/users', { params }).then(({ data }) => data);
export const getUserById = id => api.get(`/api/identity/users/${id}`).then(({ data }) => data);
export const createUser = body => api.post('/api/identity/users', body).then(({ data }) => data);
export const updateUser = (body, id) =>
api.put(`/api/identity/users/${id}`, body).then(({ data }) => data);
export const removeUser = id => api.delete(`/api/identity/users/${id}`);
export const updateProfileDetail = body =>
api.put('/api/identity/my-profile', body).then(({ data }) => data);
export const changePassword = body =>
api.post('/api/identity/my-profile/change-password', body).then(({ data }) => data);

21
templates/app/react-native/src/api/TenantManagementAPI.js

@ -0,0 +1,21 @@
import api from './API';
export function getTenants(params = {}) {
return api.get('/api/multi-tenancy/tenants', { params }).then(({ data }) => data);
}
export function createTenant(body) {
return api.post('/api/multi-tenancy/tenants', body).then(({ data }) => data);
}
export function getTenantById(id) {
return api.get(`/api/multi-tenancy/tenants/${id}`).then(({ data }) => data);
}
export function updateTenant(body, id) {
return api.put(`/api/multi-tenancy/tenants/${id}`, body).then(({ data }) => data);
}
export function removeTenant(id) {
return api.delete(`/api/multi-tenancy/tenants/${id}`).then(({ data }) => data);
}

97
templates/app/react-native/src/components/AppContainer/AppContainer.js

@ -0,0 +1,97 @@
import { Ionicons } from '@expo/vector-icons';
import * as Font from 'expo-font';
import i18n from 'i18n-js';
import PropTypes from 'prop-types';
import React, { useEffect, useState, useMemo } from 'react';
import { Platform, StatusBar } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { Root } from 'native-base';
import Loading from '../Loading/Loading';
import { connectToRedux } from '../../utils/ReduxConnect';
import { createLanguageSelector } from '../../store/selectors/AppSelectors';
import { createTokenSelector } from '../../store/selectors/PersistentStorageSelectors';
import AppActions from '../../store/actions/AppActions';
import PersistentStorageActions from '../../store/actions/PersistentStorageActions';
import { LocalizationContext } from '../../contexts/LocalizationContext';
import { isTokenValid } from '../../utils/TokenUtils';
import DrawerNavigator from '../../navigators/DrawerNavigator';
import AuthNavigator from '../../navigators/AuthNavigator';
import { getEnvVars } from '../../../Environment';
const { localization } = getEnvVars();
i18n.defaultSeparator = '::';
const cloneT = i18n.t;
i18n.t = (key, ...args) => {
if (key.slice(0, 2) === '::') {
key = localization.defaultResourceName + key;
}
return cloneT(key, ...args);
};
function AppContainer({ language, fetchAppConfig, token, setToken }) {
const platform = Platform.OS;
const [isReady, setIsReady] = useState(false);
const localizationContext = useMemo(
() => ({
t: i18n.t,
locale: (language || {}).cultureName,
}),
[language],
);
const isValid = useMemo(() => isTokenValid(token), [token]);
useEffect(() => {
if (!isValid && token && token.access_token) {
setToken({});
}
}, [isValid]);
useEffect(() => {
fetchAppConfig();
Font.loadAsync({
Roboto: require('native-base/Fonts/Roboto.ttf'),
Roboto_medium: require('native-base/Fonts/Roboto_medium.ttf'),
...Ionicons.font,
}).then(() => setIsReady(true));
}, []);
return (
<>
<StatusBar barStyle={platform === 'ios' ? 'dark-content' : 'light-content'} />
<Root>
{isReady && language ? (
<LocalizationContext.Provider value={localizationContext}>
<NavigationContainer>
{isValid ? <DrawerNavigator /> : <AuthNavigator />}
</NavigationContainer>
</LocalizationContext.Provider>
) : null}
</Root>
<Loading />
</>
);
}
AppContainer.propTypes = {
language: PropTypes.object,
token: PropTypes.object.isRequired,
fetchAppConfig: PropTypes.func.isRequired,
setToken: PropTypes.func.isRequired,
};
export default connectToRedux({
component: AppContainer,
stateProps: state => ({
language: createLanguageSelector()(state),
token: createTokenSelector()(state),
}),
dispatchProps: {
fetchAppConfig: AppActions.fetchAppConfigAsync,
setToken: PersistentStorageActions.setToken,
},
});

139
templates/app/react-native/src/components/DataList/DataList.js

@ -0,0 +1,139 @@
import { useFocusEffect } from '@react-navigation/native';
import i18n from 'i18n-js';
import { connectStyle, Icon, Input, InputGroup, Item, List, Spinner, Text } from 'native-base';
import PropTypes from 'prop-types';
import React, { forwardRef, useCallback, useEffect, useState } from 'react';
import { RefreshControl, StyleSheet, View } from 'react-native';
import LoadingActions from '../../store/actions/LoadingActions';
import { activeTheme } from '../../theme/variables';
import { debounce } from '../../utils/Debounce';
import { connectToRedux } from '../../utils/ReduxConnect';
import LoadingButton from '../LoadingButton/LoadingButton';
function DataList({
style,
navigation,
fetchFn,
render,
maxResultCount = 15,
debounceTime = 350,
...props
}) {
const [records, setRecords] = useState([]);
const [totalCount, setTotalCount] = useState(0);
const [loading, setLoading] = useState(false);
const [searchLoading, setSearchLoading] = useState(false);
const [buttonLoading, setButtonLoading] = useState(false);
const [skipCount, setSkipCount] = useState(0);
const [filter, setFilter] = useState('');
const fetch = (skip = 0, isRefreshingActive = true) => {
if (isRefreshingActive) setLoading(true);
return fetchFn({ filter, maxResultCount, skipCount: skip })
.then(({ items, totalCount: total }) => {
setTotalCount(total);
setRecords(skip ? [...records, ...items] : items);
setSkipCount(skip);
})
.finally(() => {
if (isRefreshingActive) setLoading(false);
});
};
const fetchPartial = () => {
if (loading || records.length === totalCount) return;
setButtonLoading(true);
fetch(skipCount + maxResultCount, false).finally(() => setButtonLoading(false));
};
useFocusEffect(
useCallback(() => {
setSkipCount(0);
fetch(0, false);
}, []),
);
useEffect(() => {
function searchFetch() {
setSearchLoading(true);
return fetch(0, false).finally(() => setTimeout(() => setSearchLoading(false), 150));
}
debounce(searchFetch, debounceTime)();
}, [filter]);
return (
<>
<Item placeholderLabel style={{ backgroundColor: '#fff' }}>
<InputGroup style={{ marginLeft: 10 }}>
<Input
placeholder={i18n.t('AbpUi::PagerSearch')}
style={{ padding: 0, margin: 0 }}
returnKeyType="done"
value={filter}
onChangeText={setFilter}
/>
{searchLoading ? (
<View>
<Spinner style={style.spinner} color={style.spinner.color} />
</View>
) : (
<Icon style={{ fontSize: 20, marginRight: 15 }} name="ios-search" />
)}
</InputGroup>
</Item>
<View style={style.container}>
<List
showsVerticalScrollIndicator
scrollEnabled
refreshControl={<RefreshControl refreshing={loading} onRefresh={fetch} />}
dataArray={records}
renderRow={(data, sectionID, rowId, ...args) => (
<>
{render(data, sectionID, rowId, ...args)}
{rowId + 1 === skipCount + maxResultCount && totalCount > records.length ? (
<View style={{ justifyContent: 'center', alignItems: 'center' }}>
<LoadingButton loading={buttonLoading} onPress={() => fetchPartial()}>
<Text>{i18n.t('AbpUi::LoadMore')}</Text>
</LoadingButton>
</View>
) : null}
</>
)}
{...props}
/>
</View>
</>
);
}
DataList.propTypes = {
...List.propTypes,
style: PropTypes.any.isRequired,
fetchFn: PropTypes.func.isRequired,
render: PropTypes.func.isRequired,
maxResultCount: PropTypes.number,
debounceTime: PropTypes.number,
};
const styles = StyleSheet.create({
container: { flex: 1 },
list: {},
spinner: {
transform: [{ scale: 0.5 }],
position: 'absolute',
right: 8,
top: -40,
color: activeTheme.brandInfo,
},
});
const Forwarded = forwardRef((props, ref) => <DataList {...props} forwardedRef={ref} />);
export default connectToRedux({
component: connectStyle('ABP.DataList', styles)(Forwarded),
dispatchProps: {
startLoading: LoadingActions.start,
stopLoading: LoadingActions.stop,
},
});

121
templates/app/react-native/src/components/DrawerContent/DrawerContent.js

@ -0,0 +1,121 @@
import { Text, View, List, ListItem, Left, Icon, Body } from 'native-base';
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import SafeAreaView from 'react-native-safe-area-view';
import i18n from 'i18n-js';
import PropTypes from 'prop-types';
import Constants from 'expo-constants';
import { withPermission } from '../../hocs/PermissionHOC';
const screens = {
Home: { label: '::Menu:Home', iconName: 'home' },
Users: {
label: 'AbpIdentity::Users',
iconName: 'contacts',
requiredPolicy: 'AbpIdentity.Users',
},
Tenants: {
label: 'AbpTenantManagement::Tenants',
iconName: 'people',
requiredPolicy: 'AbpTenantManagement.Tenants',
},
Settings: { label: 'AbpSettingManagement::Settings', iconName: 'cog' },
};
const ListItemWithPermission = withPermission(ListItem);
function DrawerContent({ navigation, state: { routeNames, index: currentScreenIndex } }) {
const navigate = screen => {
navigation.navigate(screen);
navigation.closeDrawer();
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.container} forceInset={{ top: 'always', horizontal: 'never' }}>
<View style={styles.headerView}>
<Image style={styles.logo} source={require('../../../assets/logo.png')} />
</View>
<List
dataArray={routeNames}
keyExtractor={item => item}
renderRow={name => (
<ListItemWithPermission
icon
key={name}
policyKey={screens[name].requiredPolicy}
selected={name === routeNames[currentScreenIndex]}
onPress={() => navigate(name)}
style={{
...styles.navItem,
backgroundColor: name === routeNames[currentScreenIndex] ? '#38003c' : '#f2f2f2',
}}>
<Left>
<Icon
dark={name !== routeNames[currentScreenIndex]}
light={name === routeNames[currentScreenIndex]}
name={screens[name].iconName}
/>
</Left>
<Body style={{ borderBottomWidth: 0 }}>
<Text
style={{
color: name === routeNames[currentScreenIndex] ? '#fff' : '#000',
}}>
{i18n.t(screens[name].label)}
</Text>
</Body>
</ListItemWithPermission>
)}
/>
</SafeAreaView>
<View style={styles.footer}>
<Text note style={styles.copyRight}>
© MyProjectName
</Text>
<Text note style={styles.version}>
v{Constants.manifest.version}
</Text>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flexGrow: 1,
},
logo: {
marginTop: 20,
marginBottom: 15,
},
headerView: {
borderBottomWidth: 1,
borderColor: '#eee',
alignItems: 'center',
},
navItem: {
marginLeft: 0,
marginBottom: 3,
paddingLeft: 10,
width: '100%',
backgroundColor: '#f2f2f2',
},
footer: {
backgroundColor: '#eee',
flexDirection: 'row',
justifyContent: 'space-between',
},
copyRight: {
margin: 15,
},
version: {
margin: 15,
},
});
DrawerContent.propTypes = {
state: PropTypes.object.isRequired,
};
export default DrawerContent;

82
templates/app/react-native/src/components/FormButtons/FormButtons.js

@ -0,0 +1,82 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { Button, Text, connectStyle } from 'native-base';
import { View, StyleSheet, Alert } from 'react-native';
import i18n from 'i18n-js';
function FormButtons({
style,
submit,
remove,
removeMessage,
isRemoveDisabled,
isSubmitDisabled,
isShowRemove = false,
isShowSubmit = true,
}) {
const confirmation = () => {
Alert.alert(
i18n.t('AbpUi::AreYouSure'),
removeMessage,
[
{
text: i18n.t('AbpUi::Cancel'),
style: 'cancel',
},
{ text: i18n.t('AbpUi::Yes'), onPress: () => remove() },
],
{ cancelable: true },
);
};
return (
<View style={style.container}>
{isShowRemove ? (
<Button
abpButton
danger
style={{ flex: 1, borderRadius: 0 }}
onPress={() => confirmation()}
disabled={isRemoveDisabled}>
<Text>{i18n.t('AbpIdentity::Delete')}</Text>
</Button>
) : null}
{isShowSubmit ? (
<Button
abpButton
primary
style={{ flex: 1, borderRadius: 0 }}
onPress={submit}
disabled={isSubmitDisabled}>
<Text>{i18n.t('AbpIdentity::Save')}</Text>
</Button>
) : null}
</View>
);
}
FormButtons.propTypes = {
submit: PropTypes.func.isRequired,
remove: PropTypes.func,
removeMessage: PropTypes.string,
style: PropTypes.any,
isRemoveDisabled: PropTypes.bool,
isSubmitDisabled: PropTypes.bool,
isShowRemove: PropTypes.bool,
isShowSubmit: PropTypes.bool,
};
const styles = StyleSheet.create({
container: {
width: '100%',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
bottom: 0,
flexDirection: 'row',
},
});
const Forwarded = forwardRef((props, ref) => <FormButtons {...props} forwardedRef={ref} />);
export default connectStyle('ABP.FormButtons', styles)(Forwarded);

63
templates/app/react-native/src/components/Loading/Loading.js

@ -0,0 +1,63 @@
import React, { forwardRef } from 'react';
import { Spinner, View, connectStyle } from 'native-base';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { activeTheme } from '../../theme/variables';
import { connectToRedux } from '../../utils/ReduxConnect';
import {
createLoadingSelector,
createOpacitySelector,
} from '../../store/selectors/LoadingSelectors';
function Loading({ style, loading, opacity }) {
return loading ? (
<View style={style.container}>
<View
style={{
...style.backdrop,
opacity: opacity || 0.6,
}}
/>
<Spinner style={style.spinner} color={style.spinner.color} />
</View>
) : null;
}
const Forwarded = forwardRef((props, ref) => <Loading {...props} forwardedRef={ref} />);
const backdropStyle = {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: '#fff',
};
export const styles = StyleSheet.create({
container: {
...backdropStyle,
backgroundColor: 'transparent',
zIndex: activeTheme.zIndex.indicator,
alignItems: 'center',
justifyContent: 'center',
},
backdrop: backdropStyle,
spinner: {
color: activeTheme.brandPrimary,
fontSize: 100,
},
});
Loading.propTypes = {
style: PropTypes.objectOf(PropTypes.any),
loading: PropTypes.bool,
opacity: PropTypes.number,
};
export default connectToRedux({
component: connectStyle('ABP.Loading', styles)(Forwarded),
stateProps: state => ({
loading: createLoadingSelector()(state),
opacity: createOpacitySelector()(state),
}),
});

31
templates/app/react-native/src/components/LoadingButton/LoadingButton.js

@ -0,0 +1,31 @@
import { Button, connectStyle, Spinner } from 'native-base';
import PropTypes from 'prop-types';
import React, { forwardRef } from 'react';
import { StyleSheet } from 'react-native';
function LoadingButton({ loading = false, style, children, ...props }) {
return (
<Button style={style.button} {...props}>
{children}
{loading ? <Spinner style={style.spinner} color={style.spinner.color || 'white'} /> : null}
</Button>
);
}
LoadingButton.propTypes = {
...Button.propTypes,
loading: PropTypes.bool.isRequired,
};
const styles = StyleSheet.create({
button: { marginTop: 20, marginBottom: 30, height: 30 },
spinner: {
transform: [{ scale: 0.5 }],
color: 'white',
marginRight: 5,
},
});
const Forwarded = forwardRef((props, ref) => <LoadingButton {...props} forwardedRef={ref} />);
export default connectStyle('ABP.LoadingButton', styles)(Forwarded);

19
templates/app/react-native/src/components/MenuIcon/MenuIcon.js

@ -0,0 +1,19 @@
import React from 'react';
import { TouchableOpacity } from 'react-native';
import { Icon } from 'native-base';
import PropTypes from 'prop-types';
function MenuIcon({ onPress, iconName = 'menu' }) {
return (
<TouchableOpacity onPress={onPress}>
<Icon navElement name={iconName} />
</TouchableOpacity>
);
}
MenuIcon.propTypes = {
onPress: PropTypes.func.isRequired,
iconName: PropTypes.string,
};
export default MenuIcon;

128
templates/app/react-native/src/components/TenantBox/TenantBox.js

@ -0,0 +1,128 @@
import i18n from 'i18n-js';
import {
Button,
connectStyle,
Content,
Input,
InputGroup,
Label,
Segment,
Text,
} from 'native-base';
import PropTypes from 'prop-types';
import React, { forwardRef, useState } from 'react';
import { StyleSheet, View, Alert } from 'react-native';
import { getTenant } from '../../api/AccountAPI';
import PersistentStorageActions from '../../store/actions/PersistentStorageActions';
import { connectToRedux } from '../../utils/ReduxConnect';
import { createTenantSelector } from '../../store/selectors/PersistentStorageSelectors';
function TenantBox({ style, tenant = {}, setTenant, showTenantSelection, toggleTenantSelection }) {
const [tenantName, setTenantName] = useState(tenant.name);
const findTenant = () => {
if (!tenantName) {
setTenant({});
toggleTenantSelection();
return;
}
getTenant(tenantName).then(({ success, ...data }) => {
if (!success) {
Alert.alert(
i18n.t('AbpUi::Error'),
i18n.t('AbpUiMultiTenancy::GivenTenantIsNotAvailable', {
0: tenantName,
}),
[{ text: i18n.t('AbpUi::Ok') }],
);
return;
}
setTenant(data);
toggleTenantSelection();
});
};
return (
<>
<Segment style={style.container}>
<View>
<Text style={style.title}>{i18n.t('AbpUiMultiTenancy::Tenant')}</Text>
<Text style={style.tenant}>
{tenant.name ? tenant.name : i18n.t('AbpUiMultiTenancy::NotSelected')}
</Text>
</View>
<Button
style={{ ...style.switchButton, display: !showTenantSelection ? 'flex' : 'none' }}
onPress={() => toggleTenantSelection()}>
<Text style={{ color: '#fff' }}>{i18n.t('AbpUiMultiTenancy::Switch')}</Text>
</Button>
</Segment>
{showTenantSelection ? (
<Content px20 style={{ flex: 1 }}>
<InputGroup abpInputGroup>
<Label abpLabel>{i18n.t('AbpUiMultiTenancy::Name')}</Label>
<Input abpInput value={tenantName} onChangeText={setTenantName} />
</InputGroup>
<Text style={style.hint}>{i18n.t('AbpUiMultiTenancy::SwitchTenantHint')}</Text>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Button abpButton light style={style.button} onPress={() => toggleTenantSelection()}>
<Text>{i18n.t('AbpAccount::Cancel')}</Text>
</Button>
<Button abpButton style={style.button} onPress={() => findTenant()}>
<Text>{i18n.t('AbpAccount::Save')}</Text>
</Button>
</View>
</Content>
) : null}
</>
);
}
TenantBox.propTypes = {
style: PropTypes.any.isRequired,
setTenant: PropTypes.func.isRequired,
showTenantSelection: PropTypes.bool.isRequired,
toggleTenantSelection: PropTypes.func.isRequired,
tenant: PropTypes.object.isRequired,
};
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
alignItems: 'center',
justifyContent: 'space-between',
height: 70,
},
button: { marginTop: 20, width: '49%' },
switchButton: {
borderTopWidth: 0,
borderRightWidth: 0,
borderBottomWidth: 0,
borderLeftWidth: 0,
borderRadius: 10,
backgroundColor: '#38003c',
height: 35,
},
tenant: { color: '#777' },
title: {
marginRight: 10,
color: '#777',
fontSize: 13,
fontWeight: '600',
textTransform: 'uppercase',
},
hint: { color: '#bbb', textAlign: 'left' },
});
const Forwarded = forwardRef((props, ref) => <TenantBox {...props} forwardedRef={ref} />);
export default connectToRedux({
component: connectStyle('ABP.TenantBox', styles)(Forwarded),
dispatchProps: {
setTenant: PersistentStorageActions.setTenant,
},
stateProps: state => ({
tenant: createTenantSelector()(state),
}),
});

18
templates/app/react-native/src/components/ValidationMessage/ValidationMessage.js

@ -0,0 +1,18 @@
import i18n from 'i18n-js';
import { connectStyle } from 'native-base';
import React, { forwardRef } from 'react';
import { Text } from 'react-native';
const ValidationMessage = ({ children, ...props }) =>
children ? <Text {...props}>{i18n.t(children)}</Text> : null;
const styles = {
fontSize: 12,
marginHorizontal: 10,
marginTop: -5,
color: '#ed2f2f',
};
const Forwarded = forwardRef((props, ref) => <ValidationMessage {...props} forwardedRef={ref} />);
export default connectStyle('ABP.ValidationMessage', styles)(Forwarded);

3
templates/app/react-native/src/contexts/LocalizationContext.js

@ -0,0 +1,3 @@
import React from 'react';
export const LocalizationContext = React.createContext();

17
templates/app/react-native/src/hocs/PermissionHOC.js

@ -0,0 +1,17 @@
import React, { forwardRef } from 'react';
import PropTypes from 'prop-types';
import { usePermission } from '../hooks/UsePermission';
export function withPermission(Component, policyKey) {
const Forwarded = forwardRef((props, ref) => {
const isGranted =
policyKey || props.policyKey ? usePermission(policyKey || props.policyKey) : true;
return isGranted ? <Component ref={ref} {...props} /> : null;
});
Forwarded.propTypes = {
policyKey: PropTypes.string,
};
return Forwarded;
}

16
templates/app/react-native/src/hooks/UsePermission.js

@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
import { store } from '../store';
import { createGrantedPolicySelector } from '../store/selectors/AppSelectors';
export function usePermission(key) {
const [permission, setPermission] = useState(false);
const state = store.getState();
const policy = createGrantedPolicySelector(key)(state);
useEffect(() => {
setPermission(policy);
}, [policy]);
return permission;
}

102
templates/app/react-native/src/interceptors/APIInterceptor.js

@ -0,0 +1,102 @@
import { Toast } from 'native-base';
import i18n from 'i18n-js';
import api from '../api/API';
import PersistentStorageActions from '../store/actions/PersistentStorageActions';
import LoadingActions from '../store/actions/LoadingActions';
export function initAPIInterceptor(store) {
api.interceptors.request.use(
async request => {
const {
persistentStorage: { token, language, tenant },
} = store.getState();
if (!request.headers.Authorization && token && token.access_token) {
request.headers.Authorization = `${token.token_type} ${token.access_token}`;
}
if (!request.headers['Content-Type']) {
request.headers['Content-Type'] = 'application/json';
}
if (!request.headers['Accept-Language'] && language) {
request.headers['Accept-Language'] = language;
}
if (!request.headers.__tenant && tenant && tenant.tenantId) {
request.headers.__tenant = tenant.tenantId;
}
return request;
},
error => console.error(error),
);
api.interceptors.response.use(
response => response,
error => {
store.dispatch(LoadingActions.clear());
const errorRes = error.response;
if (errorRes) {
if (errorRes.headers._abperrorformat && errorRes.status === 401) {
store.dispatch(PersistentStorageActions.setToken({}));
}
showError({ error: errorRes.data.error || {}, status: errorRes.status });
} else {
Toast.show({
text: 'An unexpected error has occurred',
buttonText: 'x',
duration: 10000,
type: 'danger',
textStyle: { textAlign: 'center' },
});
}
return Promise.reject(error);
},
);
}
function showError({ error = {}, status }) {
let message = '';
let title = i18n.t('AbpAccount::DefaultErrorMessage');
if (typeof error === 'string') {
message = error;
} else if (error.details) {
message = error.details;
title = error.message;
} else if (error.message) {
message = error.message;
} else {
switch (status) {
case 401:
title = i18n.t('AbpAccount::DefaultErrorMessage401');
message = i18n.t('AbpAccount::DefaultErrorMessage401Detail');
break;
case 403:
title = i18n.t('AbpAccount::DefaultErrorMessage403');
message = i18n.t('AbpAccount::DefaultErrorMessage403Detail');
break;
case 404:
title = i18n.t('AbpAccount::DefaultErrorMessage404');
message = i18n.t('AbpAccount::DefaultErrorMessage404Detail');
break;
case 500:
title = i18n.t('AbpAccount::500Message');
message = i18n.t('AbpAccount::InternalServerErrorMessage');
break;
default:
break;
}
}
Toast.show({
text: `${title}\n${message}`,
buttonText: 'x',
duration: 10000,
type: 'danger',
textStyle: { textAlign: 'center' },
});
}

22
templates/app/react-native/src/navigators/AuthNavigator.js

@ -0,0 +1,22 @@
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { LocalizationContext } from '../contexts/LocalizationContext';
import LoginScreen from '../screens/Login/LoginScreen';
const Stack = createStackNavigator();
export default function AuthStackNavigator() {
const { t } = React.useContext(LocalizationContext);
return (
<Stack.Navigator initialRouteName="Login">
<Stack.Screen
name="Login"
component={LoginScreen}
options={() => ({
title: t('AbpAccount::Login'),
})}
/>
</Stack.Navigator>
);
}

20
templates/app/react-native/src/navigators/DrawerNavigator.js

@ -0,0 +1,20 @@
import React from 'react';
import { createDrawerNavigator } from '@react-navigation/drawer';
import HomeStackNavigator from './HomeNavigator';
import SettingsStackNavigator from './SettingsNavigator';
import UsersStackNavigator from './UsersNavigator';
import TenantsStackNavigator from './TenantsNavigator';
import DrawerContent from '../components/DrawerContent/DrawerContent';
const Drawer = createDrawerNavigator();
export default function DrawerNavigator() {
return (
<Drawer.Navigator initialRouteName="Home" drawerContent={DrawerContent}>
<Drawer.Screen name="Home" component={HomeStackNavigator} />
<Drawer.Screen name="Users" component={UsersStackNavigator} />
<Drawer.Screen name="Tenants" component={TenantsStackNavigator} />
<Drawer.Screen name="Settings" component={SettingsStackNavigator} />
</Drawer.Navigator>
);
}

24
templates/app/react-native/src/navigators/HomeNavigator.js

@ -0,0 +1,24 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/Home/HomeScreen';
import MenuIcon from '../components/MenuIcon/MenuIcon';
import { LocalizationContext } from '../contexts/LocalizationContext';
const Stack = createStackNavigator();
export default function HomeStackNavigator() {
const { t } = React.useContext(LocalizationContext);
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={({ navigation }) => ({
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />,
title: t('::Menu:Home'),
})}
/>
</Stack.Navigator>
);
}

41
templates/app/react-native/src/navigators/SettingsNavigator.js

@ -0,0 +1,41 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import i18n from 'i18n-js';
import SettingsScreen from '../screens/Settings/SettingsScreen';
import ChangePasswordScreen from '../screens/ChangePassword/ChangePasswordScreen';
import ManageProfileScreen from '../screens/ManageProfile/ManageProfileScreen';
import MenuIcon from '../components/MenuIcon/MenuIcon';
import { LocalizationContext } from '../contexts/LocalizationContext';
const Stack = createStackNavigator();
export default function SettingsStackNavigator() {
const { t } = React.useContext(LocalizationContext);
return (
<Stack.Navigator initialRouteName="Settings">
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={({ navigation }) => ({
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />,
title: t('AbpSettingManagement::Settings'),
})}
/>
<Stack.Screen
name="ChangePassword"
component={ChangePasswordScreen}
options={{
title: i18n.t('AbpUi::ChangePassword'),
}}
/>
<Stack.Screen
name="ManageProfile"
component={ManageProfileScreen}
options={{
title: i18n.t('AbpAccount::ManageYourProfile'),
}}
/>
</Stack.Navigator>
);
}

34
templates/app/react-native/src/navigators/TenantsNavigator.js

@ -0,0 +1,34 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import TenantsScreen from '../screens/Tenants/TenantsScreen';
import CreateUpdateTenantScreen from '../screens/CreateUpdateTenant/CreateUpdateTenantScreen';
import MenuIcon from '../components/MenuIcon/MenuIcon';
import { LocalizationContext } from '../contexts/LocalizationContext';
const Stack = createStackNavigator();
export default function TenantsStackNavigator() {
const { t } = React.useContext(LocalizationContext);
return (
<Stack.Navigator initialRouteName="Tenants">
<Stack.Screen
name="Tenants"
component={TenantsScreen}
options={({ navigation }) => ({
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />,
title: t('AbpTenantManagement::Tenants'),
})}
/>
<Stack.Screen
name="CreateUpdateTenant"
component={CreateUpdateTenantScreen}
options={({ route }) => ({
title: t(
route.params?.tenantId ? 'AbpTenantManagement::Edit' : 'AbpTenantManagement::NewTenant',
),
})}
/>
</Stack.Navigator>
);
}

32
templates/app/react-native/src/navigators/UsersNavigator.js

@ -0,0 +1,32 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import UsersScreen from '../screens/Users/UsersScreen';
import CreateUpdateUserScreen from '../screens/CreateUpdateUser/CreateUpdateUserScreen';
import MenuIcon from '../components/MenuIcon/MenuIcon';
import { LocalizationContext } from '../contexts/LocalizationContext';
const Stack = createStackNavigator();
export default function UsersStackNavigator() {
const { t } = React.useContext(LocalizationContext);
return (
<Stack.Navigator initialRouteName="Users">
<Stack.Screen
name="Users"
component={UsersScreen}
options={({ navigation }) => ({
headerLeft: () => <MenuIcon onPress={() => navigation.openDrawer()} />,
title: t('AbpIdentity::Users'),
})}
/>
<Stack.Screen
name="CreateUpdateUser"
component={CreateUpdateUserScreen}
options={({ route }) => ({
title: t(route.params?.userId ? 'AbpIdentity::Edit' : 'AbpIdentity::NewUser'),
})}
/>
</Stack.Navigator>
);
}

99
templates/app/react-native/src/screens/ChangePassword/ChangePasswordForm.js

@ -0,0 +1,99 @@
import { Formik } from 'formik';
import i18n from 'i18n-js';
import { Container, Content, Form, Input, InputGroup, Item, Icon, Label } from 'native-base';
import PropTypes from 'prop-types';
import React, { useRef, useState } from 'react';
import * as Yup from 'yup';
import FormButtons from '../../components/FormButtons/FormButtons';
import ValidationMessage from '../../components/ValidationMessage/ValidationMessage';
const ValidationSchema = Yup.object().shape({
currentPassword: Yup.string().required('AbpAccount::ThisFieldIsRequired.'),
newPassword: Yup.string().required('AbpAccount::ThisFieldIsRequired.'),
});
function ChangePasswordForm({ submit, cancel }) {
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const currentPasswordRef = useRef();
const newPasswordRef = useRef();
const onSubmit = values => {
submit({
...values,
newPasswordConfirm: values.newPassword,
});
};
return (
<Formik
enableReinitialize
validationSchema={ValidationSchema}
initialValues={{
currentPassword: '',
newPassword: '',
}}
onSubmit={values => onSubmit(values)}>
{({ handleChange, handleBlur, handleSubmit, values, errors, isValid }) => (
<>
<Container>
<Content px20>
<Form>
<InputGroup abpInputGroup>
<Label abpLabel>{i18n.t('AbpIdentity::DisplayName:CurrentPassword')}</Label>
<Item abpInput>
<Input
ref={currentPasswordRef}
onSubmitEditing={() => newPasswordRef.current._root.focus()}
returnKeyType="next"
onChangeText={handleChange('currentPassword')}
onBlur={handleBlur('currentPassword')}
value={values.currentPassword}
textContentType="password"
secureTextEntry={!showCurrentPassword}
/>
<Icon
active
name={showCurrentPassword ? 'eye-off' : 'eye'}
onPress={() => setShowCurrentPassword(!showCurrentPassword)}
/>
</Item>
</InputGroup>
<ValidationMessage>{errors.currentPassword}</ValidationMessage>
<InputGroup abpInputGroup>
<Label abpLabel>{i18n.t('AbpIdentity::DisplayName:NewPassword')}</Label>
<Item abpInput>
<Input
ref={newPasswordRef}
returnKeyType="done"
onSubmitEditing={handleSubmit}
onChangeText={handleChange('newPassword')}
onBlur={handleBlur('newPassword')}
value={values.newPassword}
textContentType="newPassword"
secureTextEntry={!showNewPassword}
/>
<Icon
name={showNewPassword ? 'eye-off' : 'eye'}
onPress={() => setShowNewPassword(!showNewPassword)}
/>
</Item>
</InputGroup>
<ValidationMessage>{errors.newPassword}</ValidationMessage>
</Form>
</Content>
</Container>
<FormButtons submit={handleSubmit} cancel={cancel} isSubmitDisabled={!isValid} />
</>
)}
</Formik>
);
}
ChangePasswordForm.propTypes = {
submit: PropTypes.func.isRequired,
cancel: PropTypes.func.isRequired,
};
export default ChangePasswordForm;

33
templates/app/react-native/src/screens/ChangePassword/ChangePasswordScreen.js

@ -0,0 +1,33 @@
import React from 'react';
import PropTypes from 'prop-types';
import { changePassword } from '../../api/IdentityAPI';
import LoadingActions from '../../store/actions/LoadingActions';
import { connectToRedux } from '../../utils/ReduxConnect';
import ChangePasswordForm from './ChangePasswordForm';
function ChangePasswordScreen({ navigation, startLoading, stopLoading }) {
const submit = data => {
startLoading({ key: 'changePassword' });
changePassword(data)
.then(() => {
navigation.goBack();
})
.finally(() => stopLoading({ key: 'changePassword' }));
};
return <ChangePasswordForm submit={submit} cancel={() => navigation.goBack()} />;
}
ChangePasswordScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
stopLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: ChangePasswordScreen,
dispatchProps: {
startLoading: LoadingActions.start,
stopLoading: LoadingActions.stop,
},
});

84
templates/app/react-native/src/screens/CreateUpdateTenant/CreateUpdateTenantForm.js

@ -0,0 +1,84 @@
import { Formik } from 'formik';
import i18n from 'i18n-js';
import { Container, Content, Input, InputGroup, Label } from 'native-base';
import PropTypes from 'prop-types';
import React, { useRef } from 'react';
import { StyleSheet } from 'react-native';
import * as Yup from 'yup';
import FormButtons from '../../components/FormButtons/FormButtons';
import ValidationMessage from '../../components/ValidationMessage/ValidationMessage';
import { usePermission } from '../../hooks/UsePermission';
const validations = {
name: Yup.string().required('AbpAccount::ThisFieldIsRequired.'),
};
function CreateUpdateTenantForm({ editingTenant = {}, submit, remove }) {
const tenantNameRef = useRef();
const hasRemovePermission = usePermission('AbpTenantManagement.Tenants.Delete');
const onSubmit = values => {
submit({
...editingTenant,
...values,
});
};
return (
<Formik
enableReinitialize
validationSchema={Yup.object().shape({
...validations,
})}
initialValues={{
lockoutEnabled: false,
twoFactorEnabled: false,
...editingTenant,
}}
onSubmit={values => onSubmit(values)}>
{({ handleChange, handleBlur, handleSubmit, values, errors, isValid }) => (
<>
<Container style={styles.container}>
<Content px20>
<InputGroup abpInputGroup>
<Label abpLabel>{i18n.t('AbpTenantManagement::TenantName')}</Label>
<Input
abpInput
ref={tenantNameRef}
onChangeText={handleChange('name')}
onBlur={handleBlur('name')}
value={values.name}
/>
</InputGroup>
<ValidationMessage>{errors.name}</ValidationMessage>
</Content>
</Container>
<FormButtons
submit={handleSubmit}
remove={remove}
removeMessage={i18n.t('AbpTenantManagement::TenantDeletionConfirmationMessage', {
0: editingTenant.name,
})}
isSubmitDisabled={!isValid}
isShowRemove={!!editingTenant.id && hasRemovePermission}
/>
</>
)}
</Formik>
);
}
CreateUpdateTenantForm.propTypes = {
editingTenant: PropTypes.object,
submit: PropTypes.func.isRequired,
remove: PropTypes.func.isRequired,
};
const styles = StyleSheet.create({
container: {
marginBottom: 50,
},
});
export default CreateUpdateTenantForm;

77
templates/app/react-native/src/screens/CreateUpdateTenant/CreateUpdateTenantScreen.js

@ -0,0 +1,77 @@
import PropTypes from 'prop-types';
import React, { useState, useCallback } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import {
createTenant,
getTenantById,
removeTenant,
updateTenant,
} from '../../api/TenantManagementAPI';
import LoadingActions from '../../store/actions/LoadingActions';
import { createLoadingSelector } from '../../store/selectors/LoadingSelectors';
import { connectToRedux } from '../../utils/ReduxConnect';
import CreateUpdateTenantForm from './CreateUpdateTenantForm';
function CreateUpdateTenantScreen({ navigation, route, startLoading, stopLoading }) {
const [tenant, setTenant] = useState();
const tenantId = route.params?.tenantId;
const remove = () => {
startLoading({ key: 'removeTenant' });
removeTenant(tenantId)
.then(() => navigation.goBack())
.finally(() => stopLoading({ key: 'removeTenant' }));
};
useFocusEffect(
useCallback(() => {
if (tenantId) {
getTenantById(tenantId).then((data = {}) => setTenant(data));
}
}, []),
);
const submit = data => {
startLoading({ key: 'saveTenant' });
let request;
if (data.id) {
request = updateTenant(data, tenantId);
} else {
request = createTenant(data);
}
request
.then(() => {
navigation.goBack();
})
.finally(() => stopLoading({ key: 'saveTenant' }));
};
const renderForm = () => (
<CreateUpdateTenantForm editingTenant={tenant} submit={submit} remove={remove} />
);
if (tenantId && tenant) {
return renderForm();
}
if (!tenantId) {
return renderForm();
}
return null;
}
CreateUpdateTenantScreen.propTypes = {
startLoading: PropTypes.func.isRequired,
stopLoading: PropTypes.func.isRequired,
};
export default connectToRedux({
component: CreateUpdateTenantScreen,
stateProps: state => ({ loading: createLoadingSelector()(state) }),
dispatchProps: {
startLoading: LoadingActions.start,
stopLoading: LoadingActions.stop,
},
});

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save