Merge branch 'dev' into auto-merge/rel-7-1/1796

pull/15961/head
Alper Ebiçoğlu 3 years ago committed by GitHub
commit cf8bdb5d52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -421,7 +421,7 @@
"ContentCacheSlidingExpirationByDay": "Content Cache Sliding Expiration By Day",
"MaxDaysForCaching": "Max Days For Caching",
"Enabled": "Enabled",
"Menu:NugetPackagesContentCache": "NuGet Packages Content Cache",
"Menu:NugetPackagesContentCache": "NuGet Cache",
"NugetPackagesContentCache": "NuGet Content Cache",
"SlidingExpritionByDayInfo": "Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. This will not extend the entry lifetime beyond the absolute expiration.",
"MaxDaysForCachingInfo": "Gets or sets an absolute expiration time, relative to now.",

@ -27,6 +27,7 @@
"Volo.AbpIo.Domain:030010": "To purchase the trial license, you first need to activate your trial license!",
"Volo.AbpIo.Domain:030011": "You cannot delete a trial license when it is purchased!",
"Volo.AbpIo.Domain:030012": "A user is entitled to have only 1 free trial period. You already used your trial license.",
"Volo.AbpIo.Domain:030013": "A user with an active license cannot start a trial license.",
"Volo.AbpIo.Domain:070000": "The organization name can only contain latin letters, numbers, dots and hyphens!",
"Volo.AbpIo.Domain:070001": "The company name can only contain latin letters, numbers, dots, space and hyphens!",
"WantToLearn?": "Want to learn?",
@ -205,6 +206,8 @@
"Icons": "Icons",
"Url": "Url",
"Icon": "Icon",
"RecentActivities": "Recent Activities"
"RecentActivities": "Recent Activities",
"SpringCampaign": "Welcome <br>Spring Sale!",
"SpringCampaign2": "<span>Limited <br> Time Offer!</span>"
}
}

@ -196,7 +196,7 @@
"ChangingDevelopers": "Can I change the registered developers of my organization in the future?",
"ChangingDevelopersExplanation": "In addition to adding new developers to your license, you can also change the existing developers (you can remove a developer and add a new one to the same seat) without any additional cost.",
"WhatHappensWhenLicenseEnds": "What happens when my license period ends?",
"WhatHappensWhenLicenseEndsExplanation1": "The ABP Commercial license is a <a href=\"{0}\" target=\"_blank\">perpetual license</a>. After your license expires, you can continue developing your project. And you are not obliged to renew your license. Your license comes with a one-year update and support plan out of the box. In order to continue to get new features, performance enhancements, bug fixes, support and continue using ABP Suite, you need to renew your license. When your license expires, you will not get the following benefits:",
"WhatHappensWhenLicenseEndsExplanation1": "The ABP Commercial license is a <a href=\"{0}\" target=\"_blank\">perpetual license</a>. After your license expires, you can continue developing your project. And you are not obliged to renew your license. Your license comes with a one-year update and support plan out of the box. In order to continue to get new features, performance enhancements, bug fixes, support and continue using ABP Suite, you need to renew your license. When your license expires;",
"WhatHappensWhenLicenseEndsExplanation2": "You can not create new solutions using the ABP Commercial, but you can continue developing your existing applications forever.",
"WhatHappensWhenLicenseEndsExplanation3": "You will be able to get updates for the modules and themes within your MINOR version (except RC or Preview versions). For example: if you are using v3.2.0 of a module, you can still get updates for v3.2.x (v3.2.1, v3.2.5... etc.) of that module. But you cannot get updates for the next major or minor version (like v3.3.0, v3.3.3, 4.x.x.. etc.). For example, when your license expired, the latest release was v4.4.3, and later, it published both 4.4.4 version and 4.5.0 version, you would be able to access the v4.4.X but you wouldn't be access the v4.5.X.",
"WhatHappensWhenLicenseEndsExplanation4": "You can not install new modules and themes added to the ABP Commercial platform after your license ends.",
@ -208,7 +208,7 @@
"WhenShouldIRenewMyLicense": "When should I renew my license?",
"WhenShouldIRenewMyLicenseExplanation": "If you renew your license within <strong>1 month</strong> after your license expires, the following discounts will be applied: Team License {0}; Business License {1}; Enterprise License {2}. However, if you renew your license after <strong>1 month</strong> since the expiry date of your license, the renewal price will be the same as the license purchase price and there will be no discount on your renewal.",
"TrialPlan": "Do you have a trial plan?",
"TrialPlanExplanation": "It has a 14 days trial period for the ABP Commercial team license. For more information visit <a href={0} target='_blank'>here</a>. Furthermore, for the Team licenses we provide a 30 days money-back guarantee. You can just request a refund in the first 30 days. For the Business and Enterprise licenses, we provide 60% refund in 30 days. This is because Business and Enterprise licenses include the full source code of all the modules and the themes.",
"TrialPlanExplanation": "No, there is no trial version for ABP Commercial. You can check the community edition to understand the code quality and approaches. We also offer a 30-day money-back guarantee for the Team license, no questions asked! You can request a refund within the first 30 days. We provide a 60% refund within 30 days for Business and Enterprise licenses. This is because the Business and Enterprise licenses contain the full source-code of all the modules and themes.",
"DoYouAcceptBankWireTransfer": "Do you accept bank wire transfers?",
"DoYouAcceptBankWireTransferExplanation": "Yes, we accept bank wire transfers.<br/>After sending the license fee via bank transfer, send your receipt and requested license type to accounting@volosoft.com.<br/>Our international bank account information:",
"HowToUpgrade": "How to upgrade existing applications when a new version is available?",
@ -818,6 +818,7 @@
"DeletingMemberWarningMessage": "\"{0}\" will be removed from the developer list. If you want, you can assign this empty seat to another developer later.",
"AdditionalInfo": "If the developer seats are above your requirements, you can reduce them. You can email at <a href=\"mailto:info@abp.io\">info@abp.io</a> to remove some of your developer seats. Clearing unused developer seats will reduce the license renewal cost. If you want, you can re-purchase additional developer seats within your active license period. Note that, since there are {0} developers in this license package, you cannot reduce this number.",
"LinkExpiredErrorMessage": "The link you are trying to access is expired.",
"ExpirationDate": "Expiration Date"
"ExpirationDate": "Expiration Date",
"SpringCampaignDiscount": "Spring Campaign Discount"
}
}

@ -184,6 +184,8 @@
"Layout_MetaDescription": "ABP Community is an environment where people can share posts about ABP framework and follows the projects.",
"Index_Page_CommunityIntroduction": "This is a hub for ABP Framework, .NET and software development. You can read the articles, watch the video tutorials, get informed about ABPs development progress and ABP-related events, help other developers and share your expertise with the ABP community.",
"TagsInArticle": "Tags in article",
"IConsentToMedium": "I consent to the publication of this post at https://medium.com/volosoft."
"IConsentToMedium": "I consent to the publication of this post at https://medium.com/volosoft.",
"SearchResultsFor": "Search results for <span class=\"fw-bold\">\"{0}\"</span>",
"SeeMoreVideos": "See more videos"
}
}

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>7.1.0-rc.3</Version>
<Version>7.2.0</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

@ -75,6 +75,42 @@ This job simply uses `IEmailSender` to send emails (see [email sending document]
A background job should not hide exceptions. If it throws an exception, the background job is automatically re-tried after a calculated waiting time. Hide exceptions only if you don't want to re-run the background job for the current argument.
#### Cancelling Background Jobs
If your background task is cancellable, then you can use the standard [Cancellation Token](Cancellation-Token-Provider.md) system to obtain a `CancellationToken` to cancel your job when requested. See the following example that uses the `ICancellationTokenProvider` to obtain the cancellation token:
```csharp
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Threading;
namespace MyProject
{
public class LongRunningJob : AsyncBackgroundJob<LongRunningJobArgs>, ITransientDependency
{
private readonly ICancellationTokenProvider _cancellationTokenProvider;
public LongRunningJob(ICancellationTokenProvider cancellationTokenProvider)
{
_cancellationTokenProvider = cancellationTokenProvider;
}
public override async Task ExecuteAsync(LongRunningJobArgs args)
{
foreach (var id in args.Ids)
{
_cancellationTokenProvider.Token.ThrowIfCancellationRequested();
await ProcessAsync(id); // code omitted for brevity
}
}
}
}
```
> A cancellation operation might be needed if the application is shutting down and we don't want to block the application in the background job. This example throws an exception if the cancellation is requested. So, the job will be retried the next time the application starts. If you don't want that, just return from the `ExecuteAsync` method without throwing any exception (you can simply check the `_cancellationTokenProvider.Token.IsCancellationRequested` property).
#### Job Name
Each background job has a name. Job names are used in several places. For example, RabbitMQ provider uses job names to determine the RabbitMQ Queue names.

@ -0,0 +1,44 @@
## Streamline Localization in Your ABP Project
Making localization changes to an ABP project can be a daunting task, especially if you're dealing with multiple languages and translations. During development, it's easy to overlook some changes and that leads to inconsistencies across different languages. Fortunately, I have developed a tool that can help streamline the localization process and ensure consistency across different languages.
The tool is a console application that uses JSON files to manage localization keys and their translations. It addresses three common scenarios that can arise during localization:
1. When the argument count of a key changes, it can be difficult to update the translations for all languages. My tool solves this problem by scanning all JSON files in the project folder and identifying any keys that have mismatched argument counts. It then offers two options to the user: delete the mismatched translations or export them as a JSON file for manual editing.
2. When a new key is added to the project, forgetting to add its translations to all the other languages is easy. My tool helps to avoid this issue by scanning the default language's JSON file and identifying any keys that don't have translations in other languages. It then exports these keys as a JSON file that can be used to add missing translations.
3. When a key's name is changed, it's important to update its translations in all the other languages. My tool makes this task simple by scanning all the JSON files in the project folder and updating any translations of the old key name with the new one.
The tool also includes an export feature that allows users to modify translations outside of the application and import them back into the JSON files.
## How it Helps
With my Localization Key Synchronizer tool, you can perform complex localization changes more quickly and easily than by manually sifting through files and making changes one-by-one. This can save you significant time and effort, especially if you're working with a large number of languages or translations.
## How it Works
When you run the Localization Key Synchronizer tool, it presents you with three options:
1. Find Asynchronous Keys
2. Apply Changes in the Exported File
3. Replace Keys
If you select "Find Asynchronous Keys," the tool prompts you to enter the default language path. Once you've entered the path, the tool displays all of the JSON files in the same folder as a multi-select list. After selecting one or more files, you are asked whether you want to find keys that do not match the number of arguments, missing keys, or both. If you select "Missing Keys," the tool prompts you to enter the absolute path to export the missing keys. After you've entered the path, the export process starts, and the tool closes.
![](./images/Part1.gif)
If you select "Apply Changes in the Exported File" at the main menu, the tool prompts you to enter the path to the exported file. After you've entered the path, the import process starts, and the tool closes.
![](./images/Part2.gif)
If you select "Replace Keys," the tool prompts you to enter the localization folder path, the old key, the new key, and the JSON files to apply the changes to. Once you've entered all the required information and made your selections, the tool performs the replacements and closes.
![](./images/Part3.gif)
## Conclusion
If you're struggling to manage localization changes in an ABP project, give my Localization Key Synchronizer tool a try. It can help streamline your workflow and make the process much more manageable. You can find the tool on [GitHub](https://github.com/abpframework/abp/tree/dev/tools/localization-key-synchronizer).
To use the tool, simply run the console application and follow the prompts. It's a user-friendly solution that helps to ensure localization consistency in your ABP project. Give it a try and let me know what you think!

@ -440,16 +440,16 @@ Use `ICachedServiceProvider` (instead of `ITransientCachedServiceProvider`) unle
## Advanced Features
### IServiceCollection.OnRegistred Event
### IServiceCollection.OnRegistered Event
You may want to perform an action for every service registered to the dependency injection. In the `PreConfigureServices` method of your module, register a callback using the `OnRegistred` method as shown below:
You may want to perform an action for every service registered to the dependency injection. In the `PreConfigureServices` method of your module, register a callback using the `OnRegistered` method as shown below:
````csharp
public class AppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(ctx =>
context.Services.OnRegistered(ctx =>
{
var type = ctx.ImplementationType;
//...
@ -465,7 +465,7 @@ public class AppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(ctx =>
context.Services.OnRegistered(ctx =>
{
if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true))
{
@ -478,7 +478,7 @@ public class AppModule : AbpModule
This example simply checks if the service class has `MyLogAttribute` attribute and adds `MyLogInterceptor` to the interceptor list if so.
> Notice that `OnRegistred` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](Dynamic-Proxying-Interceptors.md) of dynamic proxying / interceptors.
> Notice that `OnRegistered` callback might be called multiple times for the same service class if it exposes more than one service/interface. So, it's safe to use `Interceptors.TryAdd` method instead of `Interceptors.Add` method. See [the documentation](Dynamic-Proxying-Interceptors.md) of dynamic proxying / interceptors.
## 3rd-Party Providers

@ -32,25 +32,6 @@ Configure<AbpMultiTenancyOptions>(options =>
> Multi-Tenancy is disabled in the ABP Framework by default. However, it is **enabled by default** when you create a new solution using the [startup template](Startup-Templates/Application.md). `MultiTenancyConsts` class in the solution has a constant to control it in a single place.
### AbpMultiTenancyOptions: Handle inactive and non-existent tenants.
The `MultiTenancyMiddlewareErrorPageBuilder` of `AbpMultiTenancyOptions` is used to handle inactive and non-existent tenants.
It will respond to an error page by default, you can change it if you want, eg: only output the error log and continue ASP NET Core's request pipeline.
```csharp
Configure<AbpMultiTenancyOptions>(options =>
{
options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) =>
{
// Handle the exception.
// Return true to stop the pipeline, false to continue.
return true;
};
});
```
### Database Architecture
ABP Framework supports all the following approaches to store the tenant data in the database;
@ -273,6 +254,23 @@ class SomeComponent {
> However, we don't suggest to change this value since some clients may assume the the `__tenant` as the parameter name and they might need to manually configure then.
The `MultiTenancyMiddlewareErrorPageBuilder` is used to handle inactive and non-existent tenants.
It will respond to an error page by default, you can change it if you want, eg: only output the error log and continue ASP NET Core's request pipeline.
```csharp
Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{
options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) =>
{
// Handle the exception.
// Return true to stop the pipeline, false to continue.
return true;
};
});
```
##### Domain/Subdomain Tenant Resolver
In a real application, most of times you will want to determine current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure the `AbpTenantResolveOptions` to add the domain tenant resolver.

@ -361,7 +361,7 @@ public class BookAppService :
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task<BookDto> GetAsync(Guid id)

@ -0,0 +1,52 @@
# Card Component
The ABP Card Component is a wrapper component for the Bootstrap card class.
## Usage
ABP Card Component is a part of the `ThemeSharedModule` module. If you've imported that module into your module, you don't need to import it again. If not, first import it as shown below:
```ts
// my-feature.module.ts
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { CardDemoComponent } from './chart-demo.component';
@NgModule({
imports: [
ThemeSharedModule ,
// ...
],
declarations: [CardDemoComponent],
// ...
})
export class MyFeatureModule {}
```
Then, the `abp-card` component can be used. See the example below:
```ts
// card-demo.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-card-demo',
template: `
<abp-card [cardStyle]="{width: '18rem'}">
<abp-card-body>
<abp-card-title>Lorem Ipsum</abp-card-title>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla commodo condimentum ligula, sed varius nibh eleifend sit amet. Maecenas facilisis vel arcu nec maximus.
</abp-card-body>
</abp-card>
`,
})
export class CardDemoComponent { }
```
See the result below:
![abp-card-component](./images/abp-card-component.png)
As you can see in the example above, you can customize your card component's style with the `cardStyle` input. You can also add your custom classes with the `cardClass` input.

@ -179,7 +179,7 @@ This command will download and start a simple static server, a browser window at
Of course, you need your application to run on an optimized web server and become available to everyone. This is quite straight-forward:
1. Create a new static web server instance. You can use a service like [Azure App Service](https://azure.microsoft.com/tr-tr/services/app-service/web/), [Firebase](https://firebase.google.com/docs/hosting), [Netlify](https://www.netlify.com/), [Vercel](https://vercel.com/), or even [GitHub Pages](https://angular.io/guide/deployment#deploy-to-github-pages). Another option is maintaining own web server with [NGINX](https://www.nginx.com/), [IIS](https://www.iis.net/), [Apache HTTP Server](https://httpd.apache.org/), or equivalent.
1. Create a new static web server instance. You can use a service like [Azure App Service](https://azure.microsoft.com/en-us/services/app-service/web/), [Firebase](https://firebase.google.com/docs/hosting), [Netlify](https://www.netlify.com/), [Vercel](https://vercel.com/), or even [GitHub Pages](https://angular.io/guide/deployment#deploy-to-github-pages). Another option is maintaining own web server with [NGINX](https://www.nginx.com/), [IIS](https://www.iis.net/), [Apache HTTP Server](https://httpd.apache.org/), or equivalent.
2. Copy the files from `dist/MyProjectName` <sup id="a-dist-folder-name">[1](#f-dist-folder-name)</sup> to a publicly served destination on the server via CLI of the service provider, SSH, or FTP (whichever is available). This step would be defined as a job if you have a CI/CD flow.
3. [Configure the server](https://angular.io/guide/deployment#server-configuration) to redirect all requests to the _index.html_ file. Some services do that automatically. Others require you [to add a file to the bundle via assets](https://angular.io/guide/workspace-config#assets-configuration) which describes the server how to do the redirections. Occasionally, you may need to do manual configuration.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@ -104,6 +104,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications.
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute.
* `cssClass` (`string`): Additional string classes for the menu item.
* `groupName` (`string`): Can be used to group menu items.
### Authorization
@ -179,6 +180,58 @@ userMenu.Icon = "fa fa-users";
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors.
### Menu Groups
You can define groups and associate menu items with a group.
Example:
```csharp
using System.Threading.Tasks;
using MyProject.Localization;
using Volo.Abp.UI.Navigation;
namespace MyProject.Web.Menus
{
public class MyProjectMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var l = context.GetLocalizer<MyProjectResource>();
context.Menu.AddGroup(
new ApplicationMenuGroup(
name: "Main",
displayName: l["Main"]
)
)
context.Menu.AddItem(
new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main")
.AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Customers",
displayName: l["Menu:Customers"],
url: "/crm/customers")
).AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Orders",
displayName: l["Menu:Orders"],
url: "/crm/orders")
)
);
}
}
}
```
> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group.
## Standard Menus
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus:
@ -233,4 +286,3 @@ namespace MyProject.Web.Pages
}
}
```

@ -10,7 +10,7 @@ See the [form elements demo page](https://bootstrap-taghelpers.abp.io/Components
## abp-input
`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`.
`abp-input` tag creates a Bootstrap form input for a given c# property. It uses [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-7.0#the-input-tag-helper) in background, so every data annotation attribute of `input` tag helper of Asp.Net Core is also valid for `abp-input`.
Usage:
@ -88,7 +88,7 @@ You can set some of the attributes on your c# property, or directly on html tag.
* `label`: Sets the label for input.
* `display-required-symbol`: Adds the required symbol (*) to label if input is required. Default `True`.
`asp-format`, `name` and `value` attributes of [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-input-tag-helper) are also valid for `abp-input` tag helper.
`asp-format`, `name` and `value` attributes of [Asp.Net Core Input Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-7.0#the-input-tag-helper) are also valid for `abp-input` tag helper.
### Label & Localization
@ -100,7 +100,7 @@ You can set label of your input in different ways:
## abp-select
`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [Asp.Net Core Select Tag Helper](https://docs.microsoft.com/tr-tr/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-3.1#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of Asp.Net Core is also valid for `abp-select`.
`abp-select` tag creates a Bootstrap form select for a given c# property. It uses [Asp.Net Core Select Tag Helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms?view=aspnetcore-7.0#the-select-tag-helper) in background, so every data annotation attribute of `select` tag helper of Asp.Net Core is also valid for `abp-select`.
`abp-select` tag needs a list of `Microsoft.AspNetCore.Mvc.Rendering.SelectListItem ` to work. It can be provided by `asp-items` attriube on the tag or `[SelectItems()]` attribute on c# property. (if you are using [abp-dynamic-form](Dynamic-forms.md), c# attribute is the only way.)

@ -85,6 +85,27 @@ You can simply override the styles in the Global Styles file of your application
See the [Customization / Overriding Components](Customization-Overriding-Components.md) to learn how you can replace components, customize and extend the user interface.
### Overriding the Menu Item
Basic theme supports overriding a single menu item with a custom component. You can create a custom component and call `UseComponent` extension method of Basic Theme in the **MenuContributor**.
```csharp
using Volo.Abp.AspNetCore.Components.Web.BasicTheme.Navigation;
//...
context.Menu.Items.Add(
new ApplicationMenuItem("Custom.1", "My Custom Menu", "#")
.UseComponent(typeof(MyMenuItemComponent)));
```
```html
<li class="nav-item">
<a href="#" class="nav-link">
My Custom Menu
</a>
</li>
```
### Copy & Customize
You can run the following [ABP CLI](../../CLI.md) command in **Blazor{{if UI == "Blazor"}}WebAssembly{{else}} Server{{end}}** project directory to copy the source code to your solution:

@ -104,6 +104,7 @@ There are more options of a menu item (the constructor of the `ApplicationMenuIt
* `target` (`string`): Target of the menu item. Can be `null` (default), "\_*blank*", "\_*self*", "\_*parent*", "\_*top*" or a frame name for web applications.
* `elementId` (`string`): Can be used to render the element with a specific HTML `id` attribute.
* `cssClass` (`string`): Additional string classes for the menu item.
* `groupName` (`string`): Can be used to group menu items.
### Authorization
@ -160,6 +161,58 @@ userMenu.Icon = "fa fa-users";
> `context.Menu` gives you ability to access to all the menu items those have been added by the previous menu contributors.
### Menu Groups
You can define groups and associate menu items with a group.
Example:
```csharp
using System.Threading.Tasks;
using MyProject.Localization;
using Volo.Abp.UI.Navigation;
namespace MyProject.Web.Menus
{
public class MyProjectMenuContributor : IMenuContributor
{
public async Task ConfigureMenuAsync(MenuConfigurationContext context)
{
if (context.Menu.Name == StandardMenus.Main)
{
await ConfigureMainMenuAsync(context);
}
}
private async Task ConfigureMainMenuAsync(MenuConfigurationContext context)
{
var l = context.GetLocalizer<MyProjectResource>();
context.Menu.AddGroup(
new ApplicationMenuGroup(
name: "Main",
displayName: l["Main"]
)
)
context.Menu.AddItem(
new ApplicationMenuItem("MyProject.Crm", l["Menu:CRM"], groupName: "Main")
.AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Customers",
displayName: l["Menu:Customers"],
url: "/crm/customers")
).AddItem(new ApplicationMenuItem(
name: "MyProject.Crm.Orders",
displayName: l["Menu:Orders"],
url: "/crm/orders")
)
);
}
}
}
```
> The UI theme will decide whether to render the groups or not, and if it decides to render, the way it's rendered is up to the theme. Only the LeptonX theme implements the menu group.
## Standard Menus
A menu is a **named** component. An application may contain more than one menus with different, unique names. There are two pre-defined standard menus:

@ -88,7 +88,7 @@ There are a set of standard libraries that comes pre-installed and supported by
* [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework.
* [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree.
* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library.
* [Flag Icon](https://github.com/lipis/flag-icon-css) as a library to show flags of countries.
* [Flag Icon](https://github.com/lipis/flag-icons) as a library to show flags of countries.
These libraries are selected as the base libraries and available to the applications and modules.

@ -48,7 +48,7 @@ All the themes must depend on the [Volo.Abp.AspNetCore.Components.Server.Theming
* [Twitter Bootstrap](https://getbootstrap.com/) as the fundamental HTML/CSS framework.
* [Blazorise](https://github.com/stsrki/Blazorise) as a component library that supports the Bootstrap and adds extra components like Data Grid and Tree.
* [FontAwesome](https://fontawesome.com/) as the fundamental CSS font library.
* [Flag Icon](https://github.com/lipis/flag-icon-css) as a library to show flags of countries.
* [Flag Icon](https://github.com/lipis/flag-icons) as a library to show flags of countries.
These libraries are selected as the base libraries and available to the applications and modules.

@ -270,16 +270,16 @@ using (var scope = _serviceProvider.CreateScope())
## 高级特性
### IServiceCollection.OnRegistred 事件
### IServiceCollection.OnRegistered 事件
你可能想在注册到依赖注入的每个服务上执行一个操作, 在你的模块的 `PreConfigureServices` 方法中, 使用 `OnRegistred` 方法注册一个回调(callback) , 如下所示:
你可能想在注册到依赖注入的每个服务上执行一个操作, 在你的模块的 `PreConfigureServices` 方法中, 使用 `OnRegistered` 方法注册一个回调(callback) , 如下所示:
````csharp
public class AppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(ctx =>
context.Services.OnRegistered(ctx =>
{
var type = ctx.ImplementationType;
//...
@ -295,7 +295,7 @@ public class AppModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(ctx =>
context.Services.OnRegistered(ctx =>
{
if (ctx.ImplementationType.IsDefined(typeof(MyLogAttribute), true))
{
@ -308,7 +308,7 @@ public class AppModule : AbpModule
这个示例判断一个服务类是否具有 `MyLogAttribute` 特性, 如果有的话就添加一个 `MyLogInterceptor` 到拦截器集合中.
> 注意, 如果服务类公开了多于一个服务或接口, `OnRegistred` 回调(callback)可能被同一服务类多次调用. 因此, 较安全的方法是使用 `Interceptors.TryAdd` 方法而不是 `Interceptors.Add` 方法. 请参阅动态代理(dynamic proxying)/拦截器 [文档](Dynamic-Proxying-Interceptors.md).
> 注意, 如果服务类公开了多于一个服务或接口, `OnRegistered` 回调(callback)可能被同一服务类多次调用. 因此, 较安全的方法是使用 `Interceptors.TryAdd` 方法而不是 `Interceptors.Add` 方法. 请参阅动态代理(dynamic proxying)/拦截器 [文档](Dynamic-Proxying-Interceptors.md).
## 第三方提供程序

@ -367,7 +367,7 @@ namespace Acme.BookStore.Books
GetListPolicyName = BookStorePermissions.Books.Default;
CreatePolicyName = BookStorePermissions.Books.Create;
UpdatePolicyName = BookStorePermissions.Books.Edit;
DeletePolicyName = BookStorePermissions.Books.Create;
DeletePolicyName = BookStorePermissions.Books.Delete;
}
public override async Task<BookDto> GetAsync(Guid id)

@ -0,0 +1,56 @@
using System;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Authentication.Cookies;
public static class CookieAuthenticationOptionsExtensions
{
/// <summary>
/// Introspect access token on validating the principal.
/// </summary>
/// <param name="options"></param>
/// <param name="oidcAuthenticationScheme"></param>
/// <returns></returns>
public static CookieAuthenticationOptions IntrospectAccessToken(this CookieAuthenticationOptions options, string oidcAuthenticationScheme = "oidc")
{
var originalHandler = options.Events.OnValidatePrincipal;
options.Events.OnValidatePrincipal = async principalContext =>
{
originalHandler?.Invoke(principalContext);
if (principalContext.Principal != null && principalContext.Principal.Identity != null && principalContext.Principal.Identity.IsAuthenticated)
{
var accessToken = principalContext.Properties.GetTokenValue("access_token");
if (!accessToken.IsNullOrWhiteSpace())
{
var openIdConnectOptions = principalContext.HttpContext.RequestServices.GetRequiredService<IOptionsMonitor<OpenIdConnectOptions>>().Get(oidcAuthenticationScheme);
if (openIdConnectOptions.Configuration == null && openIdConnectOptions.ConfigurationManager != null)
{
openIdConnectOptions.Configuration = await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(principalContext.HttpContext.RequestAborted);
}
var response = await openIdConnectOptions.Backchannel.IntrospectTokenAsync(new TokenIntrospectionRequest
{
Address = openIdConnectOptions.Configuration?.IntrospectionEndpoint ?? openIdConnectOptions.Authority.EnsureEndsWith('/') + "connect/introspect",
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret,
Token = accessToken
});
if (response.IsActive)
{
return;
}
}
principalContext.RejectPrincipal();
await principalContext.HttpContext.SignOutAsync(principalContext.Scheme.Name);
}
};
return options;
}
}

@ -12,11 +12,13 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.AspNetCore.Components.Web\Volo.Abp.AspNetCore.Components.Web.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.EventBus\Volo.Abp.EventBus.csproj" />
<ProjectReference Include="..\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Components.Web\Volo.Abp.AspNetCore.Components.Web.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.Mvc.Contracts\Volo.Abp.AspNetCore.Mvc.Contracts.csproj" />
<ProjectReference Include="..\Volo.Abp.EventBus\Volo.Abp.EventBus.csproj" />
<ProjectReference Include="..\Volo.Abp.Http.Client\Volo.Abp.Http.Client.csproj" />
<ProjectReference Include="..\Volo.Abp.AspNetCore.SignalR\Volo.Abp.AspNetCore.SignalR.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="IdentityModel" Version="6.0.0" />
</ItemGroup>
</Project>

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Configuration;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Components.WebAssembly.Configuration;
[Dependency(ReplaceServices = true)]
public class BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService :
ICurrentApplicationConfigurationCacheResetService,
ITransientDependency
{
private readonly WebAssemblyCachedApplicationConfigurationClient _webAssemblyCachedApplicationConfigurationClient;
public BlazorWebAssemblyCurrentApplicationConfigurationCacheResetService(WebAssemblyCachedApplicationConfigurationClient webAssemblyCachedApplicationConfigurationClient)
{
_webAssemblyCachedApplicationConfigurationClient = webAssemblyCachedApplicationConfigurationClient;
}
public async Task ResetAsync()
{
await _webAssemblyCachedApplicationConfigurationClient.InitializeAsync();
}
}

@ -7,6 +7,13 @@ public class FlagIconCssStyleContributor : BundleContributor
{
public override void ConfigureBundle(BundleConfigurationContext context)
{
context.Files.AddIfNotContains("/libs/flag-icon-css/css/flag-icons.min.css");
if (context.FileProvider.GetFileInfo("/libs/flag-icons/css/flag-icons.min.css").Exists)
{
context.Files.AddIfNotContains("/libs/flag-icons/css/flag-icons.min.css");
}
else if (context.FileProvider.GetFileInfo("/libs/flag-icon-css/css/flag-icons.min.css").Exists)
{
context.Files.AddIfNotContains("/libs/flag-icon-css/css/flag-icons.min.css");
}
}
}

@ -1,16 +1,28 @@
using System.Collections.Generic;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Packages.Core;
using Volo.Abp.Modularity;
using Volo.Abp.Localization;
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Select2;
[DependsOn(typeof(CoreScriptContributor))]
public class Select2ScriptContributor : BundleContributor
{
public const string PackageName = "select2";
public override void ConfigureBundle(BundleConfigurationContext context)
{
//TODO: Add select2.full.min.js or localize!
//TODO: Add select2.full.min.js
context.Files.AddIfNotContains("/libs/select2/js/select2.min.js");
}
public override void ConfigureDynamicResources(BundleConfigurationContext context)
{
var fileName = context.LazyServiceProvider.LazyGetRequiredService<IOptions<AbpLocalizationOptions>>().Value.GetCurrentUICultureLanguageFilesMap(PackageName);
var filePath = $"/libs/select2/js/i18n/{fileName}.js";
if (context.FileProvider.GetFileInfo(filePath).Exists)
{
context.Files.AddIfNotContains(filePath);
}
}
}

@ -105,6 +105,7 @@
$select.select2({
ajax: {
url: url,
delay: 250,
dataType: "json",
data: function (params) {
let query = {};
@ -132,6 +133,7 @@
width: '100%',
dropdownParent: parentSelector ? $(parentSelector) : $('body'),
allowClear: allowClear,
language: abp.localization.currentCulture.cultureName,
placeholder: {
id: '-1',
text: placeholder

@ -39,7 +39,7 @@ public class AbpAspNetCoreMvcUiWidgetsModule : AbpModule
{
var widgetTypes = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (WidgetAttribute.IsWidget(context.ImplementationType))
{

@ -43,6 +43,7 @@ using Volo.Abp.Localization;
using Volo.Abp.Modularity;
using Volo.Abp.UI;
using Volo.Abp.UI.Navigation;
using Volo.Abp.Validation.Localization;
namespace Volo.Abp.AspNetCore.Mvc;
@ -174,10 +175,17 @@ public class AbpAspNetCoreMvcModule : AbpModule
context.Services.Replace(ServiceDescriptor.Singleton<IValidationAttributeAdapterProvider, AbpValidationAttributeAdapterProvider>());
context.Services.AddSingleton<ValidationAttributeAdapterProvider>();
Configure<MvcOptions>(mvcOptions =>
{
mvcOptions.AddAbp(context.Services);
});
context.Services.AddOptions<MvcOptions>()
.Configure<IServiceProvider>((mvcOptions, serviceProvider) =>
{
mvcOptions.AddAbp(context.Services);
// serviceProvider is root service provider.
var stringLocalizer = serviceProvider.GetRequiredService<IStringLocalizer<AbpValidationResource>>();
mvcOptions.ModelBindingMessageProvider.SetValueIsInvalidAccessor(_ => stringLocalizer["The value '{0}' is invalid."]);
mvcOptions.ModelBindingMessageProvider.SetNonPropertyValueMustBeANumberAccessor(() => stringLocalizer["The field must be a number."]);
mvcOptions.ModelBindingMessageProvider.SetValueMustBeANumberAccessor(value => stringLocalizer["The field {0} must be a number.", value]);
});
Configure<AbpEndpointRouterOptions>(options =>
{

@ -91,7 +91,7 @@ public class AbpAspNetCoreSignalRModule : AbpModule
{
var hubTypes = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (IsHubClass(context) && !IsDisabledForAutoMap(context))
{

@ -24,7 +24,7 @@ public class AbpAuditingModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(AuditingInterceptorRegistrar.RegisterIfNeeded);
context.Services.OnRegistered(AuditingInterceptorRegistrar.RegisterIfNeeded);
}
public override void ConfigureServices(ServiceConfigurationContext context)

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.Auditing;
@ -76,7 +77,8 @@ public class AbpAuditingOptions
IgnoredTypes = new List<Type>
{
typeof(Stream),
typeof(Expression)
typeof(Expression),
typeof(CancellationToken)
};
EntityHistorySelectors = new EntityHistorySelectorList();

@ -22,7 +22,7 @@ public class AbpAuthorizationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(AuthorizationInterceptorRegistrar.RegisterIfNeeded);
context.Services.OnRegistered(AuthorizationInterceptorRegistrar.RegisterIfNeeded);
AutoAddDefinitionProviders(context.Services);
}
@ -64,7 +64,7 @@ public class AbpAuthorizationModule : AbpModule
{
var definitionProviders = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(IPermissionDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{

@ -15,7 +15,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="Autofac" Version="7.0.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="6.0.1" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="$(MicrosoftPackageVersion)" />

@ -21,7 +21,7 @@ public class AbpBackgroundJobsAbstractionsModule : AbpModule
{
var jobTypes = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(IBackgroundJob<>)) ||
ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(IAsyncBackgroundJob<>)))

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -6,7 +7,7 @@ namespace Volo.Abp.BackgroundJobs;
public abstract class AsyncBackgroundJob<TArgs> : IAsyncBackgroundJob<TArgs>
{
//TODO: Add UOW, Localization and other useful properties..?
//TODO: Add UOW, Localization, CancellationTokenProvider and other useful properties..?
public ILogger<AsyncBackgroundJob<TArgs>> Logger { get; set; }

@ -1,3 +1,4 @@
using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@ -5,7 +6,7 @@ namespace Volo.Abp.BackgroundJobs;
public abstract class BackgroundJob<TArgs> : IBackgroundJob<TArgs>
{
//TODO: Add UOW, Localization and other useful properties..?
//TODO: Add UOW, Localization, CancellationTokenProvider and other useful properties..?
public ILogger<BackgroundJob<TArgs>> Logger { get; set; }

@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
namespace Volo.Abp.BackgroundJobs;
@ -46,13 +47,19 @@ public class BackgroundJobExecuter : IBackgroundJobExecuter, ITransientDependenc
{
using(CurrentTenant.Change(GetJobArgsTenantId(context.JobArgs)))
{
if (jobExecuteMethod.Name == nameof(IAsyncBackgroundJob<object>.ExecuteAsync))
{
await ((Task)jobExecuteMethod.Invoke(job, new[] { context.JobArgs }));
}
else
var cancellationTokenProvider =
context.ServiceProvider.GetRequiredService<ICancellationTokenProvider>();
using (cancellationTokenProvider.Use(context.CancellationToken))
{
jobExecuteMethod.Invoke(job, new[] { context.JobArgs });
if (jobExecuteMethod.Name == nameof(IAsyncBackgroundJob<object>.ExecuteAsync))
{
await ((Task)jobExecuteMethod.Invoke(job, new[] { context.JobArgs }));
}
else
{
jobExecuteMethod.Invoke(job, new[] { context.JobArgs });
}
}
}

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundJobs;

@ -1,4 +1,6 @@
namespace Volo.Abp.BackgroundJobs;
using System.Threading;
namespace Volo.Abp.BackgroundJobs;
/// <summary>
/// Defines interface of a background job.

@ -1,4 +1,5 @@
using System;
using System.Threading;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.BackgroundJobs;
@ -11,10 +12,17 @@ public class JobExecutionContext : IServiceProviderAccessor
public object JobArgs { get; }
public JobExecutionContext(IServiceProvider serviceProvider, Type jobType, object jobArgs)
public CancellationToken CancellationToken { get; }
public JobExecutionContext(
IServiceProvider serviceProvider,
Type jobType,
object jobArgs,
CancellationToken cancellationToken = default)
{
ServiceProvider = serviceProvider;
JobType = jobType;
JobArgs = jobArgs;
CancellationToken = cancellationToken;
}
}

@ -12,22 +12,22 @@ namespace Volo.Abp.BackgroundJobs.Hangfire;
public class HangfireBackgroundJobManager : IBackgroundJobManager, ITransientDependency
{
protected AbpBackgroundJobOptions Options { get; }
public HangfireBackgroundJobManager(IOptions<AbpBackgroundJobOptions> options)
{
Options = options.Value;
}
public virtual Task<string> EnqueueAsync<TArgs>(TArgs args, BackgroundJobPriority priority = BackgroundJobPriority.Normal,
TimeSpan? delay = null)
{
return Task.FromResult(delay.HasValue
? BackgroundJob.Schedule<HangfireJobExecutionAdapter<TArgs>>(
adapter => adapter.ExecuteAsync(GetQueueName(typeof(TArgs)),args),
adapter => adapter.ExecuteAsync(GetQueueName(typeof(TArgs)), args, default),
delay.Value
)
: BackgroundJob.Enqueue<HangfireJobExecutionAdapter<TArgs>>(
adapter => adapter.ExecuteAsync(GetQueueName(typeof(TArgs)) ,args)
adapter => adapter.ExecuteAsync(GetQueueName(typeof(TArgs)), args, default)
));
}

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -21,8 +22,8 @@ public class HangfireJobExecutionAdapter<TArgs>
Options = options.Value;
}
[Queue("{0}")]
public async Task ExecuteAsync(string queue, TArgs args)
[Queue("{0}")]
public async Task ExecuteAsync(string queue, TArgs args, CancellationToken cancellationToken = default)
{
if (!Options.IsJobExecutionEnabled)
{
@ -38,7 +39,7 @@ public class HangfireJobExecutionAdapter<TArgs>
using (var scope = ServiceScopeFactory.CreateScope())
{
var jobType = Options.GetJob(typeof(TArgs)).JobType;
var context = new JobExecutionContext(scope.ServiceProvider, jobType, args);
var context = new JobExecutionContext(scope.ServiceProvider, jobType, args, cancellationToken: cancellationToken);
await JobExecuter.ExecuteAsync(context);
}
}

@ -40,7 +40,7 @@ public class QuartzJobExecutionAdapter<TArgs> : IJob
{
var args = JsonSerializer.Deserialize<TArgs>(context.JobDetail.JobDataMap.GetString(nameof(TArgs)));
var jobType = Options.GetJob(typeof(TArgs)).JobType;
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobType, args);
var jobContext = new JobExecutionContext(scope.ServiceProvider, jobType, args, cancellationToken: context.CancellationToken);
try
{
await JobExecuter.ExecuteAsync(jobContext);

@ -68,7 +68,8 @@ public class BackgroundJobWorker : AsyncPeriodicBackgroundWorkerBase, IBackgroun
var context = new JobExecutionContext(
workerContext.ServiceProvider,
jobConfiguration.JobType,
jobArgs);
jobArgs,
workerContext.CancellationToken);
try
{

@ -58,7 +58,7 @@ public class AuthService : IAuthService, ITransientDependency
{
if (!response.IsSuccessStatusCode)
{
Logger.LogError("Remote server returns '{response.StatusCode}'");
Logger.LogError($"Remote server returns '{response.StatusCode}'");
return null;
}
@ -127,6 +127,26 @@ public class AuthService : IAuthService, ITransientDependency
}
}
public async Task<bool> CheckMultipleOrganizationsAsync(string username)
{
var url = $"{CliUrls.WwwAbpIo}api/license/check-multiple-organizations?username={username}";
var client = CliHttpClientFactory.CreateClient();
using (var response = await client.GetHttpResponseMessageWithRetryAsync(url, CancellationTokenProvider.Token, Logger))
{
if (!response.IsSuccessStatusCode)
{
throw new Exception($"ERROR: Remote server returns '{response.StatusCode}'");
}
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response);
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<bool>(responseContent);
}
}
private async Task LogoutAsync(string accessToken)
{
try

@ -9,4 +9,6 @@ public interface IAuthService
Task LoginAsync(string userName, string password, string organizationName = null);
Task LogoutAsync();
Task<bool> CheckMultipleOrganizationsAsync(string username);
}

@ -1,8 +1,11 @@

using System;
namespace Volo.Abp.Cli.Auth;
public class LoginInfo
{
public Guid? Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }

@ -2,13 +2,11 @@
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Auth;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
@ -26,17 +24,13 @@ public class LoginCommand : IConsoleCommand, ITransientDependency
public ICancellationTokenProvider CancellationTokenProvider { get; }
public IRemoteServiceExceptionHandler RemoteServiceExceptionHandler { get; }
private readonly CliHttpClientFactory _cliHttpClientFactory;
public LoginCommand(AuthService authService,
ICancellationTokenProvider cancellationTokenProvider,
IRemoteServiceExceptionHandler remoteServiceExceptionHandler,
CliHttpClientFactory cliHttpClientFactory)
IRemoteServiceExceptionHandler remoteServiceExceptionHandler)
{
AuthService = authService;
CancellationTokenProvider = cancellationTokenProvider;
RemoteServiceExceptionHandler = remoteServiceExceptionHandler;
_cliHttpClientFactory = cliHttpClientFactory;
Logger = NullLogger<LoginCommand>.Instance;
}
@ -111,7 +105,7 @@ public class LoginCommand : IConsoleCommand, ITransientDependency
private async Task<bool> HasMultipleOrganizationAndThisNotSpecified(CommandLineArgs commandLineArgs, string organization)
{
if (string.IsNullOrWhiteSpace(organization) &&
await CheckMultipleOrganizationsAsync(commandLineArgs.Target))
await AuthService.CheckMultipleOrganizationsAsync(commandLineArgs.Target))
{
Logger.LogError($"You have multiple organizations, please specify your organization with `--organization` parameter.");
return true;
@ -168,26 +162,6 @@ public class LoginCommand : IConsoleCommand, ITransientDependency
return false;
}
private async Task<bool> CheckMultipleOrganizationsAsync(string username)
{
var url = $"{CliUrls.WwwAbpIo}api/license/check-multiple-organizations?username={username}";
var client = _cliHttpClientFactory.CreateClient();
using (var response = await client.GetHttpResponseMessageWithRetryAsync(url, CancellationTokenProvider.Token, Logger))
{
if (!response.IsSuccessStatusCode)
{
throw new Exception($"ERROR: Remote server returns '{response.StatusCode}'");
}
await RemoteServiceExceptionHandler.EnsureSuccessfulHttpResponseAsync(response);
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<bool>(responseContent);
}
}
public string GetUsageInfo()
{
var sb = new StringBuilder();

@ -1,5 +1,4 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text;
using System.Threading.Tasks;
@ -41,7 +40,7 @@ public class LoginInfoCommand : IConsoleCommand, ITransientDependency
var sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine($"Login info:");
sb.AppendLine("Login info:");
sb.AppendLine($"Name: {loginInfo.Name}");
sb.AppendLine($"Surname: {loginInfo.Surname}");
sb.AppendLine($"Username: {loginInfo.Username}");

@ -0,0 +1,94 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Cli.Commands.Services;
public class SuiteAppSettingsService : ITransientDependency
{
private const int DefaultPort = 3000;
public CmdHelper CmdHelper { get; }
public SuiteAppSettingsService(CmdHelper cmdHelper)
{
CmdHelper = cmdHelper;
}
public async Task<int> GetSuitePortAsync()
{
return await GetSuitePortAsync(GetCurrentSuiteVersion());
}
public async Task<int> GetSuitePortAsync(string version)
{
var filePath = GetFilePathOrNull(version);
if (filePath == null)
{
return DefaultPort;
}
var content = File.ReadAllText(filePath);
var contentAsJson = JObject.Parse(content);
var url = contentAsJson["AbpSuite"]?["ApplicationUrl"]?.ToString();
if (url == null)
{
return DefaultPort;
}
return Convert.ToInt32(url.Split(":").Last());
}
private string GetFilePathOrNull(string version)
{
if (version == null)
{
return null;
}
var path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".dotnet",
"tools",
".store",
"volo.abp.suite",
version,
"volo.abp.suite",
version,
"tools",
"net7.0",
"any",
"appsettings.json"
);
if (!File.Exists(path))
{
return null;
}
return path;
}
private string GetCurrentSuiteVersion()
{
var dotnetToolList = CmdHelper.RunCmdAndGetOutput("dotnet tool list -g", out int exitCode);
var suiteLine = dotnetToolList.Split(Environment.NewLine)
.FirstOrDefault(l => l.ToLower().StartsWith("volo.abp.suite "));
if (string.IsNullOrEmpty(suiteLine))
{
return null;
}
return suiteLine.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1];
}
}

@ -35,23 +35,26 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
private readonly PackageVersionCheckerService _packageVersionCheckerService;
private readonly AuthService _authService;
private readonly CliHttpClientFactory _cliHttpClientFactory;
private readonly SuiteAppSettingsService _suiteAppSettingsService;
private const string SuitePackageName = "Volo.Abp.Suite";
public ILogger<SuiteCommand> Logger { get; set; }
private const string AbpSuiteHost = "http://localhost:3000";
private int _abpSuitePort = 3000;
public SuiteCommand(
AbpNuGetIndexUrlService nuGetIndexUrlService,
PackageVersionCheckerService packageVersionCheckerService,
ICmdHelper cmdHelper,
AuthService authService,
CliHttpClientFactory cliHttpClientFactory)
CliHttpClientFactory cliHttpClientFactory,
SuiteAppSettingsService suiteAppSettingsService)
{
CmdHelper = cmdHelper;
_nuGetIndexUrlService = nuGetIndexUrlService;
_packageVersionCheckerService = packageVersionCheckerService;
_authService = authService;
_cliHttpClientFactory = cliHttpClientFactory;
_suiteAppSettingsService = suiteAppSettingsService;
Logger = NullLogger<SuiteCommand>.Instance;
}
@ -72,17 +75,20 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
commandLineArgs.Options.ContainsKey(Options.Preview.Long);
var version = commandLineArgs.Options.GetOrNull(Options.Version.Short, Options.Version.Long);
var currentSuiteVersionAsString = GetCurrentSuiteVersion();
switch (operationType)
{
case "":
case null:
await InstallSuiteIfNotInstalledAsync();
await InstallSuiteIfNotInstalledAsync(currentSuiteVersionAsString);
_abpSuitePort = await _suiteAppSettingsService.GetSuitePortAsync(currentSuiteVersionAsString);
RunSuite();
break;
case "generate":
await InstallSuiteIfNotInstalledAsync();
await InstallSuiteIfNotInstalledAsync(currentSuiteVersionAsString);
_abpSuitePort = await _suiteAppSettingsService.GetSuitePortAsync(currentSuiteVersionAsString);
var suiteProcess = StartSuite();
System.Threading.Thread.Sleep(500); //wait for initialization of the app
await GenerateCrudPageAsync(commandLineArgs);
@ -130,7 +136,7 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
var IsSolutionBuiltResponse = await client.GetAsync(
$"{AbpSuiteHost}/api/abpSuite/solutions/{solutionId.ToString()}/is-built"
$"http://localhost:{_abpSuitePort}/api/abpSuite/solutions/{solutionId.ToString()}/is-built"
);
var IsSolutionBuilt = Convert.ToBoolean(await IsSolutionBuiltResponse.Content.ReadAsStringAsync());
@ -148,7 +154,7 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
);
var responseMessage = await client.PostAsync(
$"{AbpSuiteHost}/api/abpSuite/crudPageGenerator/{solutionId.ToString()}/save-and-generate-entity",
$"http://localhost:{_abpSuitePort}/api/abpSuite/crudPageGenerator/{solutionId.ToString()}/save-and-generate-entity",
entityContent
);
@ -178,7 +184,7 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
var responseMessage = await client.GetHttpResponseMessageWithRetryAsync(
"http://localhost:3000/api/abpSuite/solutions",
$"http://localhost:{_abpSuitePort}/api/abpSuite/solutions",
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)),
Logger,
timeIntervals.ToArray());
@ -216,7 +222,7 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
);
var responseMessage = await client.PostAsync(
"http://localhost:3000/api/abpSuite/addSolution",
$"http://localhost:{_abpSuitePort}/api/abpSuite/addSolution",
entityContent,
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10))
);
@ -234,11 +240,9 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
}
private async Task InstallSuiteIfNotInstalledAsync()
private async Task InstallSuiteIfNotInstalledAsync(string currentSuiteVersion)
{
var currentSuiteVersionAsString = GetCurrentSuiteVersion();
if (string.IsNullOrEmpty(currentSuiteVersionAsString))
if (string.IsNullOrEmpty(currentSuiteVersion))
{
await InstallSuiteAsync();
}
@ -430,6 +434,19 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
Logger.LogWarning("Couldn't check ABP Suite installed status: " + ex.Message);
}
if (IsSuiteAlreadyRunning())
{
Logger.LogInformation("Opening suite...");
CmdHelper.Open($"http://localhost:{_abpSuitePort}");
return;
}
if (IsPortAlreadyInUse())
{
Logger.LogError($"Port \"{_abpSuitePort}\" is already in use.");
return;
}
CmdHelper.RunCmd("abp-suite");
}
@ -453,23 +470,32 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
return null;
}
if (IsPortAlreadyInUse())
{
Logger.LogError($"Port \"{_abpSuitePort}\" is already in use.");
return null;
}
return CmdHelper.RunCmdAndGetProcess("abp-suite --no-browser");
}
private bool IsSuiteAlreadyRunning()
{
return GetProcessesRelatedWithSuite().Any();
}
private bool IsPortAlreadyInUse()
{
var ipGP = IPGlobalProperties.GetIPGlobalProperties();
var endpoints = ipGP.GetActiveTcpListeners();
return endpoints.Any(e => e.Port == 3000);
return endpoints.Any(e => e.Port == _abpSuitePort);
}
private void KillSuite()
{
try
{
var suiteProcesses = (from p in Process.GetProcesses()
where p.ProcessName.ToLower().Contains("abp-suite")
select p);
var suiteProcesses = GetProcessesRelatedWithSuite();
foreach (var suiteProcess in suiteProcesses)
{
@ -483,6 +509,13 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
}
private IEnumerable<Process> GetProcessesRelatedWithSuite()
{
return (from p in Process.GetProcesses()
where p.ProcessName.ToLower().Contains("abp-suite")
select p);
}
public string GetUsageInfo()
{
var sb = new StringBuilder();

@ -5,9 +5,9 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionRegistrationActionExtensions
{
// OnRegistred
// OnRegistered
public static void OnRegistred(this IServiceCollection services, Action<IOnServiceRegistredContext> registrationAction)
public static void OnRegistered(this IServiceCollection services, Action<IOnServiceRegistredContext> registrationAction)
{
GetOrCreateRegistrationActionList(services).Add(registrationAction);
}

@ -209,7 +209,22 @@ public static class AbpStringExtensions
return str;
}
return str.Substring(0, pos) + replace + str.Substring(pos + search.Length);
var searchLength = search.Length;
var replaceLength = replace.Length;
var newLength = str.Length - searchLength + replaceLength;
Span<char> buffer = newLength <= 1024 ? stackalloc char[newLength] : new char[newLength];
// Copy the part of the original string before the search term
str.AsSpan(0, pos).CopyTo(buffer);
// Copy the replacement text
replace.AsSpan().CopyTo(buffer.Slice(pos));
// Copy the remainder of the original string
str.AsSpan(pos + searchLength).CopyTo(buffer.Slice(pos + replaceLength));
return buffer.ToString();
}
/// <summary>

@ -42,7 +42,7 @@ public class AbpDataModule : AbpModule
{
var contributors = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(IDataSeedContributor).IsAssignableFrom(context.ImplementationType))
{

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.BackgroundJobs;
using Volo.Abp.DependencyInjection;

@ -47,7 +47,7 @@ public class AbpEventBusModule : AbpModule
var localHandlers = new List<Type>();
var distributedHandlers = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (ReflectionHelper.IsAssignableToGenericType(context.ImplementationType, typeof(ILocalEventHandler<>)))
{

@ -22,7 +22,7 @@ public class AbpFeaturesModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(FeatureInterceptorRegistrar.RegisterIfNeeded);
context.Services.OnRegistered(FeatureInterceptorRegistrar.RegisterIfNeeded);
AutoAddDefinitionProviders(context.Services);
}
@ -57,7 +57,7 @@ public class AbpFeaturesModule : AbpModule
{
var definitionProviders = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(IFeatureDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{

@ -124,7 +124,7 @@ public class FeatureDefinition : ICanCreateChildFeature
}
/// <summary>
/// Sets a property in the <see cref="Properties"/> dictionary.
/// Adds one or more providers to the <see cref="AllowedProviders"/> list.
/// This is a shortcut for nested calls on this object.
/// </summary>
public virtual FeatureDefinition WithProviders(params string[] providers)

@ -17,7 +17,7 @@ public class AbpGlobalFeaturesModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(GlobalFeatureInterceptorRegistrar.RegisterIfNeeded);
context.Services.OnRegistered(GlobalFeatureInterceptorRegistrar.RegisterIfNeeded);
}
public override void ConfigureServices(ServiceConfigurationContext context)

@ -6,6 +6,7 @@ using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.Application.Services;
@ -13,6 +14,7 @@ using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.ClientProxying;
using Volo.Abp.Http.Client.StaticProxying;
using Volo.Abp.Http.Modeling;
using Volo.Abp.Reflection;
@ -24,14 +26,17 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention
protected readonly IClientProxyApiDescriptionFinder ClientProxyApiDescriptionFinder;
protected readonly List<ControllerModel> ControllerWithAttributeRoute;
protected readonly List<ActionModel> ActionWithAttributeRoute;
protected readonly AbpHttpClientStaticProxyingOptions StaticProxyingOptions;
public AbpHttpClientProxyServiceConvention(
IOptions<AbpAspNetCoreMvcOptions> options,
IConventionalRouteBuilder conventionalRouteBuilder,
IClientProxyApiDescriptionFinder clientProxyApiDescriptionFinder)
IClientProxyApiDescriptionFinder clientProxyApiDescriptionFinder,
IOptions<AbpHttpClientStaticProxyingOptions> staticProxyingOptions)
: base(options, conventionalRouteBuilder)
{
ClientProxyApiDescriptionFinder = clientProxyApiDescriptionFinder;
StaticProxyingOptions = staticProxyingOptions.Value;
ControllerWithAttributeRoute = new List<ControllerModel>();
ActionWithAttributeRoute = new List<ActionModel>();
}
@ -73,6 +78,27 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention
}
}
protected override void ConfigureParameters(ControllerModel controller)
{
foreach (var action in controller.Actions)
{
foreach (var prm in action.Parameters)
{
if (prm.BindingInfo != null)
{
continue;
}
if (StaticProxyingOptions.BindingFromQueryTypes.Contains(prm.ParameterInfo.ParameterType))
{
prm.BindingInfo = BindingInfo.GetBindingInfo(new[] { new FromQueryAttribute() });
}
}
}
base.ConfigureParameters(controller);
}
protected virtual bool ShouldBeRemove(ApplicationModel application, ControllerModel controllerModel)
{
return application.Controllers

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Http.Client.StaticProxying;
public class AbpHttpClientStaticProxyingOptions
{
public List<Type> BindingFromQueryTypes { get; }
public AbpHttpClientStaticProxyingOptions()
{
BindingFromQueryTypes = new List<Type>();
}
}

@ -1,20 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Volo.Abp.MultiLingualObject</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Localization\Volo.Abp.Localization.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\configureawait.props" />
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Volo.Abp.MultiLingualObject</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.Localization\Volo.Abp.Localization.csproj" />
</ItemGroup>
</Project>

@ -5,16 +5,30 @@ namespace Volo.Abp.MultiLingualObjects;
public interface IMultiLingualObjectManager
{
Task<TTranslation> GetTranslationAsync<TMultiLingual, TTranslation>(
Task<TTranslation?> GetTranslationAsync<TMultiLingual, TTranslation>(
TMultiLingual multiLingual,
string culture = null,
string? culture = null,
bool fallbackToParentCultures = true)
where TMultiLingual : IMultiLingualObject<TTranslation>
where TTranslation : class, IObjectTranslation;
Task<TTranslation> GetTranslationAsync<TTranslation>(
ICollection<TTranslation> translations,
string culture = null,
Task<TTranslation?> GetTranslationAsync<TTranslation>(
IEnumerable<TTranslation> translations,
string? culture = null,
bool fallbackToParentCultures = true)
where TTranslation : class, IObjectTranslation;
Task<List<TTranslation?>> GetBulkTranslationsAsync<TTranslation>(
IEnumerable<IEnumerable<TTranslation>> translationsCombined,
string? culture = null,
bool fallbackToParentCultures = true)
where TTranslation : class, IObjectTranslation;
Task<List<(TMultiLingual entity, TTranslation? translation)>> GetBulkTranslationsAsync<TMultiLingual, TTranslation>(
IEnumerable<TMultiLingual> multiLinguals,
string? culture = null,
bool fallbackToParentCultures = true)
where TMultiLingual : IMultiLingualObject<TTranslation>
where TTranslation : class, IObjectTranslation;
}

@ -19,16 +19,16 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD
{
SettingProvider = settingProvider;
}
public virtual async Task<TTranslation> GetTranslationAsync<TTranslation>(
ICollection<TTranslation> translations,
string culture,
public virtual async Task<TTranslation?> GetTranslationAsync<TTranslation>(
IEnumerable<TTranslation> translations,
string? culture,
bool fallbackToParentCultures)
where TTranslation : class, IObjectTranslation
{
culture ??= CultureInfo.CurrentUICulture.Name;
if (translations.IsNullOrEmpty())
if (translations == null || !translations.Any())
{
return null;
}
@ -65,9 +65,9 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD
return translation;
}
public virtual Task<TTranslation> GetTranslationAsync<TMultiLingual, TTranslation>(
public virtual Task<TTranslation?> GetTranslationAsync<TMultiLingual, TTranslation>(
TMultiLingual multiLingual,
string culture = null,
string? culture = null,
bool fallbackToParentCultures = true)
where TMultiLingual : IMultiLingualObject<TTranslation>
where TTranslation : class, IObjectTranslation
@ -75,13 +75,13 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD
return GetTranslationAsync(multiLingual.Translations, culture: culture, fallbackToParentCultures: fallbackToParentCultures);
}
protected virtual TTranslation GetTranslationBasedOnCulturalRecursive<TTranslation>(
CultureInfo culture, ICollection<TTranslation> translations, int currentDepth)
protected virtual TTranslation? GetTranslationBasedOnCulturalRecursive<TTranslation>(
CultureInfo culture, IEnumerable<TTranslation> translations, int currentDepth)
where TTranslation : class, IObjectTranslation
{
if (culture == null ||
culture.Name.IsNullOrWhiteSpace() ||
translations.IsNullOrEmpty() ||
translations == null || !translations.Any() ||
currentDepth > MaxCultureFallbackDepth)
{
return null;
@ -89,5 +89,108 @@ public class MultiLingualObjectManager : IMultiLingualObjectManager, ITransientD
var translation = translations.FirstOrDefault(pt => pt.Language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase));
return translation ?? GetTranslationBasedOnCulturalRecursive(culture.Parent, translations, currentDepth + 1);
}
}
public virtual async Task<List<TTranslation?>> GetBulkTranslationsAsync<TTranslation>(IEnumerable<IEnumerable<TTranslation>> translationsCombined, string? culture, bool fallbackToParentCultures)
where TTranslation : class, IObjectTranslation
{
culture ??= CultureInfo.CurrentUICulture.Name;
if (translationsCombined == null || !translationsCombined.Any())
{
return new();
}
var someHaveNoTranslations = false;
var res = new List<TTranslation?>();
foreach (var translations in translationsCombined)
{
if (!translations.Any())
{
//if the src has no translations, don't try to find a translation
res.Add(null);
continue;
}
var translation = translations.FirstOrDefault(pt => pt.Language == culture);
if (translation != null)
{
res.Add(translation);
}
else
{
if (fallbackToParentCultures)
{
translation = GetTranslationBasedOnCulturalRecursive(
CultureInfo.CurrentUICulture.Parent,
translations,
0
);
if (translation != null)
{
res.Add(translation);
}
else
{
res.Add(null);
someHaveNoTranslations = true;
}
}
else
{
res.Add(null);
someHaveNoTranslations = true;
}
}
}
if (someHaveNoTranslations)
{
var defaultLanguage = await SettingProvider.GetOrNullAsync(LocalizationSettingNames.DefaultLanguage);
var index = 0;
foreach (var translations in translationsCombined)
{
if (!translations.Any())
{
//don't try to find a translation
}
else
{
var translation = res[index];
if (translation != null)
{
continue;
}
translation = translations.FirstOrDefault(pt => pt.Language == defaultLanguage);
if (translation != null)
{
res[index] = translation;
}
else
{
res[index] = translations.FirstOrDefault();
}
}
index++;
}
}
return res;
}
public virtual async Task<List<(TMultiLingual entity, TTranslation? translation)>> GetBulkTranslationsAsync<TMultiLingual, TTranslation>(IEnumerable<TMultiLingual> multiLinguals, string? culture, bool fallbackToParentCultures)
where TMultiLingual : IMultiLingualObject<TTranslation>
where TTranslation : class, IObjectTranslation
{
var resInitial = await GetBulkTranslationsAsync(multiLinguals.Select(x => x.Translations), culture, fallbackToParentCultures);
var index = 0;
var res = new List<(TMultiLingual entity, TTranslation? translation)>();
foreach (var item in multiLinguals)
{
var t = resInitial[index++];
res.Add((item, t));
}
return res;
}
}

@ -63,7 +63,7 @@ public class AbpSecurityModule : AbpModule
{
var contributorTypes = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(IAbpClaimsPrincipalContributor).IsAssignableFrom(context.ImplementationType))
{

@ -36,7 +36,7 @@ public class AbpSettingsModule : AbpModule
{
var definitionProviders = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(ISettingDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{

@ -23,7 +23,7 @@ public class AbpTextTemplatingCoreModule : AbpModule
var definitionProviders = new List<Type>();
var contentContributors = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(ITemplateDefinitionProvider).IsAssignableFrom(context.ImplementationType))
{

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Volo.Abp.Data;
@ -5,7 +6,7 @@ using Volo.Abp.UI.Navigation;
namespace Volo.Abp.UI.Navigation;
public class ApplicationMenu : IHasMenuItems
public class ApplicationMenu : IHasMenuItems, IHasMenuGroups
{
/// <summary>
/// Unique name of the menu in the application.
@ -31,6 +32,10 @@ public class ApplicationMenu : IHasMenuItems
[NotNull]
public ApplicationMenuItemList Items { get; }
/// <inheritdoc cref="IHasMenuGroups.Groups"/>
[NotNull]
public ApplicationMenuGroupList Groups { get; }
/// <summary>
/// Can be used to store a custom object related to this menu.
/// </summary>
@ -47,6 +52,7 @@ public class ApplicationMenu : IHasMenuItems
DisplayName = displayName ?? Name;
Items = new ApplicationMenuItemList();
Groups = new ApplicationMenuGroupList();
}
/// <summary>
@ -60,6 +66,17 @@ public class ApplicationMenu : IHasMenuItems
return this;
}
/// <summary>
/// Adds a <see cref="ApplicationMenuGroup"/> to <see cref="Groups"/>.
/// </summary>
/// <param name="group"><see cref="ApplicationMenuGroup"/> to be added</param>
/// <returns>This <see cref="ApplicationMenu"/> object</returns>
public ApplicationMenu AddGroup([NotNull] ApplicationMenuGroup group)
{
Groups.Add(group);
return this;
}
/// <summary>
/// Adds a custom data item to <see cref="CustomData"/> with given key &amp; value.
/// </summary>

@ -64,4 +64,54 @@ public static class ApplicationMenuExtensions
return menuWithItems;
}
[NotNull]
public static ApplicationMenuGroup GetMenuGroup(
[NotNull] this IHasMenuGroups menuWithGroups,
string groupName)
{
var menuGroup = menuWithGroups.GetMenuGroupOrNull(groupName);
if (menuGroup == null)
{
throw new AbpException($"Could not find a group item with given name: {groupName}");
}
return menuGroup;
}
[CanBeNull]
public static ApplicationMenuGroup GetMenuGroupOrNull(
[NotNull] this IHasMenuGroups menuWithGroups,
string menuGroupName)
{
Check.NotNull(menuWithGroups, nameof(menuWithGroups));
return menuWithGroups.Groups.FirstOrDefault(group => group.Name == menuGroupName);
}
public static bool TryRemoveMenuGroup(
[NotNull] this IHasMenuGroups menuWithGroups,
string menuGroupName)
{
Check.NotNull(menuWithGroups, nameof(menuWithGroups));
return menuWithGroups.Groups.RemoveAll(group => group.Name == menuGroupName) > 0;
}
[NotNull]
public static IHasMenuGroups SetMenuGroupOrder(
[NotNull] this IHasMenuGroups menuWithGroups,
string menuGroupName,
int order)
{
Check.NotNull(menuWithGroups, nameof(menuWithGroups));
var menuGroup = menuWithGroups.GetMenuGroupOrNull(menuGroupName);
if (menuGroup != null)
{
menuGroup.Order = order;
}
return menuWithGroups;
}
}

@ -0,0 +1,67 @@
using JetBrains.Annotations;
namespace Volo.Abp.UI.Navigation;
public class ApplicationMenuGroup
{
private string _displayName;
/// <summary>
/// Default <see cref="Order"/> value of a group item.
/// </summary>
public const int DefaultOrder = 1000;
/// <summary>
/// Unique name of the group in the application.
/// </summary>
[NotNull]
public string Name { get; }
/// <summary>
/// Display name of the group.
/// </summary>
[NotNull]
public string DisplayName {
get { return _displayName; }
set {
Check.NotNullOrWhiteSpace(value, nameof(value));
_displayName = value;
}
}
/// <summary>
/// Can be used to render the element with a specific Id for DOM selections.
/// </summary>
public string ElementId { get; set; }
/// <summary>
/// The Display order of the group.
/// Default value: 1000.
/// </summary>
public int Order { get; set; }
public ApplicationMenuGroup(
[NotNull] string name,
[NotNull] string displayName,
string elementId = null,
int order = DefaultOrder)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
Check.NotNullOrWhiteSpace(displayName, nameof(displayName));
Name = name;
DisplayName = displayName;
ElementId = elementId;
Order = order;
}
private string GetDefaultElementId()
{
return "MenuGroup_" + Name;
}
public override string ToString()
{
return $"[ApplicationMenuGroup] Name = {Name}";
}
}

@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
namespace Volo.Abp.UI.Navigation;
public class ApplicationMenuGroupList: List<ApplicationMenuGroup>
{
public ApplicationMenuGroupList()
{
}
public ApplicationMenuGroupList(int capacity)
: base(capacity)
{
}
public ApplicationMenuGroupList(IEnumerable<ApplicationMenuGroup> collection)
: base(collection)
{
}
public void Normalize()
{
Order();
}
private void Order()
{
var orderedItems = this.OrderBy(item => item.Order).ToArray();
Clear();
AddRange(orderedItems);
}
}

@ -92,6 +92,11 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers<Applic
/// </summary>
public string CssClass { get; set; }
/// <summary>
/// Can be used to group menu items.
/// </summary>
public string GroupName { get; set; }
public ApplicationMenuItem(
[NotNull] string name,
[NotNull] string displayName,
@ -101,6 +106,7 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers<Applic
string target = null,
string elementId = null,
string cssClass = null,
string groupName = null,
string requiredPermissionName = null)
{
Check.NotNullOrWhiteSpace(name, nameof(name));
@ -114,6 +120,7 @@ public class ApplicationMenuItem : IHasMenuItems, IHasSimpleStateCheckers<Applic
Target = target;
ElementId = elementId ?? GetDefaultElementId();
CssClass = cssClass;
GroupName = groupName;
RequiredPermissionName = requiredPermissionName;
StateCheckers = new List<ISimpleStateChecker<ApplicationMenuItem>>();
Items = new ApplicationMenuItemList();

@ -0,0 +1,9 @@
namespace Volo.Abp.UI.Navigation;
public interface IHasMenuGroups
{
/// <summary>
/// Menu groups.
/// </summary>
ApplicationMenuGroupList Groups { get; }
}

@ -96,6 +96,7 @@ public class MenuManager : IMenuManager, ITransientDependency
}
NormalizeMenu(menu);
NormalizeMenuGroup(menu);
return menu;
}
@ -159,4 +160,23 @@ public class MenuManager : IMenuManager, ITransientDependency
menuWithItems.Items.Normalize();
}
protected virtual void NormalizeMenuGroup(ApplicationMenu applicationMenu)
{
foreach (var menuGroup in applicationMenu.Items.Where(x => !x.GroupName.IsNullOrWhiteSpace()).GroupBy(x => x.GroupName))
{
var group = applicationMenu.GetMenuGroupOrNull(menuGroup.First().GroupName);
if (group != null)
{
continue;
}
foreach (var menuItem in menuGroup)
{
menuItem.GroupName = null;
}
}
applicationMenu.Groups.Normalize();
}
}

@ -7,6 +7,6 @@ public class AbpUnitOfWorkModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded);
context.Services.OnRegistered(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded);
}
}

@ -193,7 +193,7 @@ public class UnitOfWork : IUnitOfWork, ITransientDependency
if (_databaseApis.ContainsKey(key))
{
throw new AbpException("There is already a database API in this unit of work with given key: " + key);
throw new AbpException("There is already a database API in this unit of work with given key.");
}
_databaseApis.Add(key, api);
@ -221,7 +221,7 @@ public class UnitOfWork : IUnitOfWork, ITransientDependency
if (_transactionApis.ContainsKey(key))
{
throw new AbpException("There is already a transaction API in this unit of work with given key: " + key);
throw new AbpException("There is already a transaction API in this unit of work with given key.");
}
_transactionApis.Add(key, api);

@ -16,7 +16,7 @@ public class AbpValidationModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.OnRegistred(ValidationInterceptorRegistrar.RegisterIfNeeded);
context.Services.OnRegistered(ValidationInterceptorRegistrar.RegisterIfNeeded);
AutoAddObjectValidationContributors(context.Services);
}
@ -39,7 +39,7 @@ public class AbpValidationModule : AbpModule
{
var contributorTypes = new List<Type>();
services.OnRegistred(context =>
services.OnRegistered(context =>
{
if (typeof(IObjectValidationContributor).IsAssignableFrom(context.ImplementationType))
{

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "يجب أن يكون الحقل {0} سلسلة أحرف طولها {1} كحد أدنى و {2} كحد أقصى.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "الحقل {0} ليس عنوانا URL صالحًا مؤهلاً بالكامل سواء كان عنوان http أو https أو ftp",
"The field {0} is invalid.": "الحقل {0} غير صالح.",
"The value '{0}' is invalid.": "القيمة '{0}' غير صالحة.",
"The field {0} must be a number.": "يجب أن يكون الحقل {0} رقمًا.",
"The field must be a number.": "يجب أن يكون الحقل رقمًا.",
"ThisFieldIsNotAValidCreditCardNumber.": "هذا الحقل لا يمثل رقم بطاقة ائتمان صالح.",
"ThisFieldIsNotValid.": "هذا الحقل غير صالح.",
"ThisFieldIsNotAValidEmailAddress.": "هذا الحقل لا يمثل عنوان بريد إلكتروني صالح.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Pole {0} musí být řetězec o minimální délce {2} a maximální délce {1} znaků.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Pole {0} není platná plně kvalifikovaná adresa http, https, nebo ftp URL.",
"The field {0} is invalid.": "Pole {0} je neplatné.",
"The value '{0}' is invalid.": "Hodnota '{0}' je neplatná.",
"The field {0} must be a number.": "Pole {0} musí být číslo.",
"The field must be a number.": "Pole musí být číslo.",
"ThisFieldIsNotAValidCreditCardNumber.": "V poli {0} není platné číslo kreditní karty.",
"ThisFieldIsNotValid.": "{0} není platný.",
"ThisFieldIsNotAValidEmailAddress.": "V poli {0} není platný email.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Das Feld {0} muss eine Zeichenfolge mit einer Mindestlänge von {2} und einer Maximallänge von {1} sein.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Das Feld {0} ist keine gültige, vollqualifizierte http-, https- oder ftp-URL.",
"The field {0} is invalid.": "Das Feld {0} ist ungültig.",
"The value '{0}' is invalid.": "Der Wert '{0}' ist ungültig.",
"The field {0} must be a number.": "Das Feld {0} muss eine Zahl sein.",
"The field must be a number.": "Das Feld muss eine Zahl sein.",
"ThisFieldIsNotAValidCreditCardNumber.": "Dieses Feld ist keine gültige Kreditkartennummer.",
"ThisFieldIsNotValid.": "Dieses Feld ist ungültig.",
"ThisFieldIsNotAValidEmailAddress.": "Dieses Feld ist keine gültige E-Mail-Adresse.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Το πεδίο {0} πρέπει να είναι μια συμβολοσειρά με ελάχιστο μήκος {2} και μέγιστο μήκος {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Το πεδίο {0} δεν είναι έγκυρη πλήρως πιστοποιημένη διεύθυνση URL http, https ή ftp.",
"The field {0} is invalid.": "Το πεδίο {0} δεν είναι έγκυρο.",
"The value '{0}' is invalid.": "Η τιμή '{0}' δεν είναι έγκυρη.",
"The field {0} must be a number.": "Το πεδίο {0} πρέπει να είναι αριθμός.",
"The field must be a number.": "Το πεδίο πρέπει να είναι αριθμός.",
"ThisFieldIsNotAValidCreditCardNumber.": "Αυτό το πεδίο δεν περιέχει έγκυρο αριθμό πιστωτικής κάρτας.",
"ThisFieldIsNotValid.": "Αυτό το πεδίο δεν είναι έγκυρο.",
"ThisFieldIsNotAValidEmailAddress.": "Αυτό το πεδίο δεν περιέχει έγκυρη διεύθυνση ηλεκτρονικού ταχυδρομείου.",

@ -17,6 +17,9 @@
"The field {0} is invalid.": "The field {0} is invalid.",
"ThisFieldIsNotAValidCreditCardNumber.": "This field is not a valid credit card number.",
"ThisFieldIsNotValid.": "This field is not valid.",
"The value '{0}' is invalid.": "The value '{0}' is invalid.",
"The field {0} must be a number.": "The field {0} must be a number.",
"The field must be a number.": "The field must be a number.",
"ThisFieldIsNotAValidEmailAddress.": "This field is not a valid e-mail address.",
"ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "This field only accepts files with the following extensions: {0}",
"ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}": "This field must be a string or array type with a maximum length of '{0}'.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "The {0} field is not a valid fully-qualified http, https, or ftp URL.",
"The field {0} is invalid.": "The field {0} is invalid.",
"The value '{0}' is invalid.": "The value '{0}' is invalid.",
"The field {0} must be a number.": "The field {0} must be a number.",
"The field must be a number.": "The field must be a number.",
"ThisFieldIsNotAValidCreditCardNumber.": "This field is not a valid credit card number.",
"ThisFieldIsNotValid.": "This field is not valid.",
"ThisFieldIsNotAValidEmailAddress.": "This field is not a valid e-mail address.",
@ -33,4 +36,4 @@
"ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "This field is not a valid fully-qualified http, https, or ftp URL.",
"ThisFieldIsInvalid.": "This field is invalid."
}
}
}

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "El campo {0} debe ser una cadena con una longitud mínima de {2} y máxima de {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "El campo {0} no es una URL (http, https o ftp) valida.",
"The field {0} is invalid.": "El campo {0} no es valido.",
"The value '{0}' is invalid.": "El valor '{0}' no es válido.",
"The field {0} must be a number.": "El campo {0} debe ser un número.",
"The field must be a number.": "El campo debe ser un número.",
"ThisFieldIsNotAValidCreditCardNumber.": "El campo no es un número de tarjeta de crédito valido.",
"ThisFieldIsNotValid.": "Este campo no es valido.",
"ThisFieldIsNotAValidEmailAddress.": "Este campo no es una dirección de e-mail valida.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "فیلد {0} باید رشته ای با حداقل طول {2} و حداکثر باشد طول {1}. ",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "فیلد {0} یک نشانی اینترنتی http ، https ، یا ftp کاملاً واجد شرایط نیست.",
"The field {0} is invalid.": "فیلد {0} نامعتبر است.",
"The value '{0}' is invalid.": "مقدار '{0}' نامعتبر است.",
"The field {0} must be a number.": "فیلد {0} باید یک عدد باشد.",
"The field must be a number.": "فیلد باید یک عدد باشد.",
"ThisFieldIsNotAValidCreditCardNumber.": "این قسمت شماره کارت اعتباری معتبری نیست.",
"ThisFieldIsNotValid.": "این قسمت معتبر نیست.",
"ThisFieldIsNotAValidEmailAddress.": "این قسمت آدرس ایمیل معتبری نیست.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Kentän {0} on oltava merkkijono, jonka vähimmäispituus on {2} ja enimmäispituus {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "{0} -kenttä ei ole kelvollinen täysin hyväksytty http, https tai ftp URL.",
"The field {0} is invalid.": "Kenttä {0} on virheellinen.",
"The value '{0}' is invalid.": "Arvo '{0}' on virheellinen.",
"The field {0} must be a number.": "Kentän {0} on oltava numero.",
"The field must be a number.": "Kentän on oltava numero.",
"ThisFieldIsNotAValidCreditCardNumber.": "Tämä kenttä ei ole kelvollinen luottokortin numero.",
"ThisFieldIsNotValid.": "Tämä kenttä ei kelpaa.",
"ThisFieldIsNotAValidEmailAddress.": "Tämä kenttä ei ole kelvollinen sähköpostiosoite.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Le champ {0} doit être une chaîne d'une longueur minimale de {2} et d'une longueur maximale de {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Le champ {0} n'est pas une URL http, https ou ftp complète valide.",
"The field {0} is invalid.": "Le champ {0} n'est pas valide.",
"The value '{0}' is invalid.": "La valeur '{0}' n'est pas valide.",
"The field {0} must be a number.": "Le champ {0} doit être un nombre.",
"The field must be a number.": "Le champ doit être un nombre.",
"ThisFieldIsNotAValidCreditCardNumber.": "Ce champ n'est pas un numéro de carte de crédit valide.",
"ThisFieldIsNotValid.": "Ce champ n'est pas valide.",
"ThisFieldIsNotAValidEmailAddress.": "Ce champ n'est pas une adresse e-mail valide.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "फ़ील्ड {0} की लंबाई न्यूनतम {2} और अधिकतम लंबाई {1} होनी चाहिए।",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "{0} फ़ील्ड मान्य पूर्णत: योग्य http, https, या ftp URL नहीं है।",
"The field {0} is invalid.": "फ़ील्ड {0} अमान्य है।",
"The value '{0}' is invalid.": "मान '{0}' अमान्य है।",
"The field {0} must be a number.": "फ़ील्ड {0} एक संख्या होनी चाहिए।",
"The field must be a number.": "फ़ील्ड एक संख्या होनी चाहिए।",
"ThisFieldIsNotAValidCreditCardNumber.": "यह फ़ील्ड मान्य क्रेडिट कार्ड नंबर नहीं है।",
"ThisFieldIsNotValid.": "यह फ़ील्ड मान्य नहीं है।",
"ThisFieldIsNotAValidEmailAddress.": "यह फ़ील्ड मान्य ई-मेल पता नहीं है।",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Polje {0} mora biti text sa minimalnom du<64>inom od {2} i maksimalnom dužinom od {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "{0} nije valjan potpuno kvalificirani http, https, ili ftp URL.",
"The field {0} is invalid.": "Polje {0} nije važeće.",
"The value '{0}' is invalid.": "Vrijednost '{0}' nije važeća.",
"The field {0} must be a number.": "Polje {0} mora biti broj.",
"The field must be a number.": "Polje mora biti broj.",
"ThisFieldIsNotAValidCreditCardNumber.": "Ovo polje nije važeći broj kreditne kartice.",
"ThisFieldIsNotValid.": "Ovo polje nije valjano.",
"ThisFieldIsNotAValidEmailAddress.": "Ovo polje nije valjana e-mail adresa.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "A {0} mezőnek szöveget kell tartalmaznia minimum {2} és maximum {1} hosszan.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "A {0} mező nem érvényes fully-qualified http, https, vagy ftp URL cím.",
"The field {0} is invalid.": "A {0} mező nem érvényes.",
"The value '{0}' is invalid.": "A(z) '{0}' érték érvénytelen.",
"The field {0} must be a number.": "A(z) {0} mezőnek számnak kell lennie.",
"The field must be a number.": "A mezőnek számnak kell lennie.",
"ThisFieldIsNotAValidCreditCardNumber.": "A mező nem érvényes bankkártya számot tartalmaz.",
"ThisFieldIsNotValid.": "A mező nem érvényes.",
"ThisFieldIsNotAValidEmailAddress.": "A mező nem érvényes email cím.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Reiturinn {0} verður að vera strengur með lágmarkslengd {2} og hámarkslengd {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Reiturinn {0} er ekki fullgild http, https, eða ftp slóð.",
"The field {0} is invalid.": "Reiturinn {0} er ekki rétt útfylltur.",
"The value '{0}' is invalid.": "Gildið '{0}' er ógilt.",
"The field {0} must be a number.": "Reiturinn {0} verður að vera númer.",
"The field must be a number.": "Reiturinn verður að vera númer.",
"ThisFieldIsNotAValidCreditCardNumber.": "Þessi reitur hefur ekki gilt kreditkortanúmer.",
"ThisFieldIsNotValid.": "Reiturinn er ekki rétt útfylltur.",
"ThisFieldIsNotAValidEmailAddress.": "Þessi reitur hefur ekki gilt netfang.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Il campo {0} deve essere una stringa con una lunghezza minima di {2} e una lunghezza massima di {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Il campo {0} non è un URL http, https o ftp completo e valido.",
"The field {0} is invalid.": "Il campo {0} non è valido.",
"The value '{0}' is invalid.": "Il valore '{0}' non è valido.",
"The field {0} must be a number.": "Il campo {0} deve essere un numero.",
"The field must be a number.": "Il campo deve essere un numero.",
"ThisFieldIsNotAValidCreditCardNumber.": "Questo campo non è un numero di carta di credito valido.",
"ThisFieldIsNotValid.": "Questo campo non è valido.",
"ThisFieldIsNotAValidEmailAddress.": "Questo campo non è un indirizzo e-mail valido.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Het veld {0} moet een tekenreeks zijn met een minimale lengte van {2} en een maximale lengte van {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Het veld {0} is geen geldige, volledig gekwalificeerde http-, https- of ftp-URL.",
"The field {0} is invalid.": "Het veld {0} is ongeldig.",
"The value '{0}' is invalid.": "De waarde '{0}' is ongeldig.",
"The field {0} must be a number.": "Het veld {0} moet een getal zijn.",
"The field must be a number.": "Het veld moet een getal zijn.",
"ThisFieldIsNotAValidCreditCardNumber.": "Dit veld is geen geldig krediet kaartnummer.",
"ThisFieldIsNotValid.": "Dir veld is ongeldig.",
"ThisFieldIsNotAValidEmailAddress.": "Dit veld is geen geldig e-mail adres.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Pole {0} musi być łańcuchem znaków o minimalnej długości {2} i maksymalnej długości {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Pole {0} nie jest prawidłowym, w pełni kwalifikowanym adresem URL http, https lub ftp.",
"The field {0} is invalid.": "Pole {0} jest niepoprawne.",
"The value '{0}' is invalid.": "Wartość '{0}' jest nieprawidłowa.",
"The field {0} must be a number.": "Pole {0} musi być liczbą.",
"The field must be a number.": "Pole musi być liczbą.",
"ThisFieldIsNotAValidCreditCardNumber.": "To pole nie jest prawidłowym numerem karty kredytowej.",
"ThisFieldIsNotValid.": "To pole jest nieprawidłowe.",
"ThisFieldIsNotAValidEmailAddress.": "To pole nie jest prawidłowym adresem e-mail.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "O campo {0} deve ser uma palavra com o tamanho mínimo de {2} e tamanho máximo de {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "O campo {0} não é um http, https, ou FTP válido.",
"The field {0} is invalid.": "O campo {0} é inválido.",
"The value '{0}' is invalid.": "O valor '{0}' é inválido.",
"The field {0} must be a number.": "O campo {0} deve ser um número.",
"The field must be a number.": "O campo deve ser um número.",
"ThisFieldIsNotAValidCreditCardNumber.": "Não é um cartão de crédito válido.",
"ThisFieldIsNotValid.": "Campo inválido.",
"ThisFieldIsNotAValidEmailAddress.": "E-mail inválido.",

@ -16,6 +16,9 @@
"The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "Câmpul {0} trebuie să fie un string cu lungimea minimă de {2} şi lungimea maximă de {1}.",
"The {0} field is not a valid fully-qualified http, https, or ftp URL.": "Câmpul {0} nu este o adresă validă complet http, https sau ftp.",
"The field {0} is invalid.": "Câmpul {0} este invalid.",
"The value '{0}' is invalid.": "Valoarea '{0}' este nevalidă.",
"The field {0} must be a number.": "Câmpul {0} trebuie să fie un număr.",
"The field must be a number.": "Câmpul trebuie să fie un număr.",
"ThisFieldIsNotAValidCreditCardNumber.": "Acest câmp nu este un număr de card de credit valid.",
"ThisFieldIsNotValid.": "Acest câmp nu este valid.",
"ThisFieldIsNotAValidEmailAddress.": "Acest câmp nu este o adresă de e-mail validă.",

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

Loading…
Cancel
Save