diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index b14d1ffa70..92d7450319 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -601,7 +601,7 @@ "PaymentSucceed_ViewOrganization": "Click here to view organization", "Purchase_TotalAnnualPrice": "TOTAL (annual fee)", "Purchase_TrainingPrice": "Training Price", - "Purchase_OnboardingTraining": "ABP Onboarding & Web Application Development Live Training", + "Purchase_OnboardingTraining": "Onboarding & Web Application Development Live Training", "TotalDeveloperPrice": "Total Developer Price", "Purchase_PricePerDeveloper": "{0} {1} per developer", "Purchase_IncludedDeveloperInfo": "{0} {1} included.", @@ -978,10 +978,10 @@ "Purchase_DevelopersAlreadyIncluded": "{0} developers already included", "1Year": "1 year", "{0}Years": "{0} years", - "1YearLicense": "1 year license", - "{0}YearsLicense": "{0} years license", - "1AdditionalDeveloper": "1 additional developer", - "{0}AdditionalDevelopers": "{0} additional developers", + "1YearLicense": "1 Year License", + "{0}YearsLicense": "{0} Years License", + "1AdditionalDeveloper": "1 Additional Developer", + "{0}AdditionalDevelopers": "{0} Additional Developers", "Discount": "Discount ({0}%)", "Summary": "Summary", "TrainingPack": "Training pack", @@ -1081,6 +1081,10 @@ "CampaignInfo": "Buy a new license or renew your existing license and get an additional 2 months at no additional cost! This offer is valid for all license plans. Ensure you take advantage of this limited-time promotion to expand your access to premium features and upgrades.", "HurryUpLastDay": "Hurry Up! Last Day: {0}", "CreatingCRUDPagesWithABPSuite": "Creating CRUD pages with ABP Suite", - "Testimonials": "Testimonials" + "Testimonials": "Testimonials", + "MultipleYearDiscount": "Multiple Year Discount", + "CampaignDiscountText": "Black Friday Discount", + "CampaignDiscountName": "Black Friday", + "CampaignName:BlackFriday": "Black Friday" } } diff --git a/docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md b/docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md index a8f1aeadfa..73d2e6bad5 100644 --- a/docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md +++ b/docs/en/Community-Articles/2023-04-15-Converting-Create-Edit-Modal-To-Page/POST.md @@ -315,7 +315,7 @@ import { CreateBookComponent } from './create-book/create-book.component'; import { EditBookComponent } from './edit-book/edit-book.component'; const routes: Routes = [ - { path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] }, + { path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] }, { path: 'create', component: CreateBookComponent }, { path: 'edit/:id', component: EditBookComponent }, ]; diff --git a/docs/en/Community-Articles/2023-10-30-Enhancements-to-JSON-column-mapping/POST.md b/docs/en/Community-Articles/2023-10-30-Enhancements-to-JSON-column-mapping/POST.md new file mode 100644 index 0000000000..db8f72ca17 --- /dev/null +++ b/docs/en/Community-Articles/2023-10-30-Enhancements-to-JSON-column-mapping/POST.md @@ -0,0 +1,124 @@ +# EF Core 8 - Enhancements to JSON column mapping + +In this article, we will examine the enhancements introduced in EF Core 8 for the JSON column feature, building upon the foundation laid by [JSON columns in Entity Framework Core 7](https://community.abp.io/posts/json-columns-in-entity-framework-core-7-cjaom76j). + +## The entity classes we will be using in the article + +```csharp +public class Person +{ + public int Id { get; set; } + [Required] + public string Name { get; set; } + [Required] + public ContactDetails ContactDetails { get; set; } +} + +public class ContactDetails +{ + public List
Addresses { get; set; } = new(); + public string? Phone { get; set; } +} + +public class Address +{ + public Address(string street, string city, string postcode, string country) + { + Street = street; + City = city; + Postcode = postcode; + Country = country; + } + + public string Street { get; set; } + public string City { get; set; } + public string Postcode { get; set; } + public string Country { get; set; } + public bool IsMainAddress { get; set; } +} +``` + +## The DbContext class we will be using in the article + +```csharp +public class AppDbContext : DbContext +{ + public DbSet Persons { get; set; } = null!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { +#if SQLSERVER + optionsBuilder.UseSqlServer("Server=localhost;Database=EfCore8Json;Trusted_Connection=True;TrustServerCertificate=True"); +#elif SQLITE + optionsBuilder.UseSqlite("Data Source=EfCore8Json.db"); +#endif + base.OnConfiguring(optionsBuilder); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.ToTable("Persons"); + b.HasKey(x => x.Id); + b.Property(x => x.Name).IsRequired(); + b.OwnsOne(x => x.ContactDetails, cb => + { + cb.ToJson(); + cb.Property(x => x.Phone); + cb.OwnsMany(x => x.Addresses); + }); + }); + + base.OnModelCreating(modelBuilder); + } +} +``` + +## Translate element access into JSON arrays + +EF Core 8 supports indexing in JSON arrays when executing queries. For example, the following query returns individuals whose first address is the main address in the database: + +```csharp +var query = dbContext.Persons + .Select(x => x.ContactDetails.Addresses[0]) + .Where(x => x.IsMainAddress == true) + .ToListAsync(); +``` + +The generated SQL query is as follows when using SQL Server: + +```sql +SELECT JSON_QUERY([p].[ContactDetails], '$.Addresses[0]'), [p].[Id] +FROM [Persons] AS [p] +WHERE CAST(JSON_VALUE([p].[ContactDetails], '$.Addresses[0].IsMainAddress') AS bit) = CAST(1 AS bit) +``` + +> Note: If you attempt to access an index that is outside of the array, it will return null. + +## JSON Columns for SQLite + +In EF Core 7, JSON column mapping was supported for Azure SQL/SQL Server. In EF Core 8, this support has been extended to include SQLite as well. + +### Queries into JSON columns + +The following query returns individuals whose first address is the main address in the database: + +```csharp +var query = dbContext.Persons + .Select(x => x.ContactDetails.Addresses[0]) + .Where(x => x.IsMainAddress == true) + .ToListAsync(); +``` + +The generated SQL query is as follows when using SQLite: + +```sql +SELECT "p"."ContactDetails" ->> '$.Addresses[0]', "p"."Id" +FROM "Persons" AS "p" +WHERE "p"."ContactDetails" ->> '$.Addresses[0].IsMainAddress' = 0 +``` + +## References + +- [EF Core 8 - Enhancements to JSON column mapping](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#enhancements-to-json-column-mapping) \ No newline at end of file diff --git a/docs/en/Community-Articles/2023-10-31-NET-8-ASP-NET-Core-Minimal-APIs/POST.md b/docs/en/Community-Articles/2023-10-31-NET-8-ASP-NET-Core-Minimal-APIs/POST.md new file mode 100644 index 0000000000..03ca906884 --- /dev/null +++ b/docs/en/Community-Articles/2023-10-31-NET-8-ASP-NET-Core-Minimal-APIs/POST.md @@ -0,0 +1,156 @@ +# New Minimal APIs features in ASP.NET Core 8.0 + +In this article, we will see the new features of Minimal APIs in ASP.NET Core 8.0. + +## Binding to forms + +We can bind to forms using the [FromForm] attribute. Let's see an example: + +```csharp +app.MapPost("/books", async ([FromForm] string name, + [FromForm] BookType bookType, IFormFile? cover, BookDb db) => +{ + var book = new Book + { + Name = name, + BookType = bookType + }; + + if (cover is not null) + { + var coverName = Path.GetRandomFileName(); + + using var stream = File.Create(Path.Combine("wwwroot", coverName)); + await cover.CopyToAsync(stream); + book.Cover = coverName; + } + + db.Books.Add(book); + await db.SaveChangesAsync(); + + return Results.Ok(); +}); +``` + +Another way is using the [AsParameters] attribute, the following code binds from form values to properties of the `NewBookRequest` record struct: + +```csharp +public record NewBookRequest([FromForm] string Name, [FromForm] BookType BookType, IFormFile? Cover); + +app.MapPost("/books", async ([AsParameters] NewBookRequest request, BookDb db) => +{ + var book = new Book + { + Name = request.Name, + BookType = request.BookType + }; + + if (request.Cover is not null) + { + var coverName = Path.GetRandomFileName(); + + using var stream = File.Create(Path.Combine("wwwroot", coverName)); + await request.Cover.CopyToAsync(stream); + book.Cover = coverName; + } + + db.Books.Add(book); + await db.SaveChangesAsync(); + + return Results.Ok(); +}); +``` + +## Antiforgery + +ASP.NET Core 8.0 adds support for antiforgery tokens. We can call the `AddAntiforgery` method to register the antiforgery services and `WebApplicationBuilder` will automatically add the antiforgery middleware to the pipeline: + +```csharp +var builder = WebApplication.CreateBuilder(); + +builder.Services.AddAntiforgery(); + +var app = builder.Build(); + +// Implicitly added by WebApplicationBuilder if AddAntiforgery is called. +// app.UseAntiforgery(); + +app.MapGet("/", () => "Hello World!"); + +app.Run(); +``` + +Example of using antiforgery tokens: + +```csharp + +// Use the antiforgery service to generate tokens. +app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) => +{ + var token = antiforgery.GetAndStoreTokens(context); + return Results.Content(...., "text/html"); +}); + +// It will automatically validate the token. +app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo)); + +// Disable antiforgery validation for this endpoint. +app.MapPost("/todo2", ([FromForm] Todo todo) => Results.Ok(todo)) + .DisableAntiforgery(); +``` + + +## Native AOT + +ASP.NET Core 8.0 adds support for Native AOT. + +You can add `PublishAot` to the project file to enable Native AOT: + +```xml + + true + +``` + +### Web API (native AOT) template + +You can use the `dotnet new webapiaot` command to create a new Minimal APIs project with Native AOT enabled. + +```diff ++using System.Text.Json.Serialization; + +-var builder = WebApplication.CreateBuilder(); ++var builder = WebApplication.CreateSlimBuilder(args); + ++builder.Services.ConfigureHttpJsonOptions(options => ++{ ++ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); ++}); + +var app = builder.Build(); + +var sampleTodos = TodoGenerator.GenerateTodos().ToArray(); + +var todosApi = app.MapGroup("/todos"); +todosApi.MapGet("/", () => sampleTodos); +todosApi.MapGet("/{id}", (int id) => + sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo + ? Results.Ok(todo) + : Results.NotFound()); + +app.Run(); + ++[JsonSerializable(typeof(Todo[]))] ++internal partial class AppJsonSerializerContext : JsonSerializerContext ++{ ++ ++} +``` + +* Reflection isn't supported in native AOT, you must use the `JsonSerializable` attribute to specify the types that you want to serialize/deserialize. + +## References + +* https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#minimal-apis +* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#explicit-binding-from-form-values +* https://learn.microsoft.com/en-us/aspnet/core/fundamentals/native-aot?view=aspnetcore-8.0 diff --git a/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/POST.md b/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/POST.md new file mode 100644 index 0000000000..22c621aa1b --- /dev/null +++ b/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/POST.md @@ -0,0 +1,138 @@ +# ASP.NET 8: What's New About Authentication and Authorization + +In ASP.NET 8, the concept of authentication and authorization is undergoing a transformation. Specifically, ASP.NET Core Identity is transitioning from a focus on traditional web pages to a more API-driven approach. We will see what the Identity API endpoints are, why we need them, and how to use them in detail. So, let's dive in. + +## What is ASP.NET Core Identity? + +ASP.NET Core Identity is like an extra toolkit that comes with ASP.NET Core. It offers a bunch of services to help you deal with user accounts in your ASP.NET Core application. These services include both basic concepts and ready-made solutions for each task. + +With ASP.NET Core Identity, you can save user accounts in your app, handle user information, add extra security with two-factor authentication, and even connect other log in options, like social media logins, to user accounts. In simple terms, it's all about keeping user accounts in your app and making it easy for users to log in. + +![](./identity_endpoints_scaffold.png) + +Prior to ASP.NET 8, there were limitations on using ASP.NET Identity in Single Page Applications (SPAs). Because the ASP.NET Core Identity framework is primarily tailored for traditional server-rendered web applications, like ASP.NET Core MVC or Razor Pages apps. However, the Identity API endpoints that come with ASP.NET 8 aim to solve token-based authentication and authorization in SPA without external dependencies. + +## Why do we need Identity APIs? + +The introduction of Identity endpoints in ASP.NET 8 aims to streamline the process of integrating ASP.NET Core Identity into both API server applications and front-end SPAs like those built with JavaScript or Blazor. To understand the importance of these endpoints, let's first examine the disadvantages of using ASP.NET Core Identity before ASP.NET 8. + +Let's delve into a common scenario: + +Imagine you have a straightforward application composed of an ASP.NET Core backend that exposes APIs. On the client side, you have a SPA application that communicates with these APIs. Now, you want to incorporate user accounts, complete with authentication and authorization, into your application. + +Before the advent of the identity endpoints, you could add ASP.NET Core Identity and the default Razor Pages UI to your app by adding a few packages, updating your database schema, and registering some services. However, this approach had some disadvantages. From a user experience perspective, the full-page refreshes of Razor Pages can be a major drawback especially compared to the fluidity of SPAs. On the other hand, from a developer's perspective, if you want the default pages to be compatible with the rest of your application, you may need to update more than 30 pages. Moreover, the default Razor Pages UI uses traditional cookie-based authentication, not bearer tokens. + +In ASP.NET 8, identity endpoints were introduced to simplify the process of adding user accounts to ASP.NET Core API apps used with SPAs or mobile. These endpoints streamline the user management process by providing an alternative to the traditional Razor Page Identity UI pages. Additionally, a new endpoint is included for retrieving bearer tokens, which can be used for authentication. These changes directly address the concerns mentioned earlier, making it easier to create a user-friendly and integrated experience within your app, with no full-page refreshes or styling conflicts. + +## How to add Identity APIs? + +In this part, I'll create a demo application, add all of the required packages, and lastly map the Identity APIs. + +Create a new ASP.NET Core app with the following command and change the directory to the created application folder: + +```bash +dotnet new webapi --name NewIdentityEndpoints -o IdentityDemo +cd IdentityDemo +``` + +After that, open the application in your favorite IDE and add the required packages: + +```csharp + + +``` + +**Note:** I used the `.NET 8.0.100-rc.2-**` SDK for everything in this post. + +Then replace `Program.cs` as below: + +```csharp +using System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme); +builder.Services.AddAuthorizationBuilder(); + +builder.Services.AddDbContext(options => options.UseInMemoryDatabase("AppDb")); + +builder.Services.AddIdentityCore() +.AddEntityFrameworkStores() +.AddApiEndpoints(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapGroup("/my-identity-api").MapIdentityApi(); + +app.MapGet("/", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.Run(); + +class MyCustomUser : IdentityUser { } + +class ApplicationDbContext : IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) { } +} + +``` + +When you run the application, you will see a result as the following: + +![](./new-identity-endpoints.png) + +## Custom Authorization Policies + +Prior to ASP.NET 8, adding a parameterized authorization policy to an endpoint required writing a lot of code. + +- Implementing an `AuthorizeAttribute` for each policy +- Implementing an `AuthorizationPolicyProvider` to process a custom policy from a string-based contract +- Implementing an `AuthorizationRequirement` for the policy +- Implementing an `AuthorizationHandler` for each requirement + +However, implementing the `IAuthorizationRequirementData` interface that comes with ASP.NET 8, your application code should look like this: + +```csharp +class MinimumAgeAuthorizeAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData +{ + public MinimumAgeAuthorizeAttribute(int age) => Age =age; + public int Age { get; } + + public IEnumerable GetRequirements() + { + yield return this; + } +} + +class MinimumAgeAuthorizationHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeAuthorizeAttribute requirement) + { + ... + } +} +``` + +See [here](https://gist.github.com/captainsafia/7c54e92d12df695ff0908e989fb8531f) for the complete code. + +## Conclusion + +In this article, I've shown you the Identity APIs introduced with ASP.NET 8, why we need them, and how we can add them to our application, and finally I explained how to define a custom authorization policy with fewer lines of code. + +## References +- https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#authentication-and-authorization +- https://auth0.com/blog/whats-new-dotnet8-authentication-authorization/ +- https://andrewlock.net/exploring-the-dotnet-8-preview-introducing-the-identity-api-endpoints/ +- https://andrewlock.net/should-you-use-the-dotnet-8-identity-api-endpoints/ diff --git a/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png b/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png new file mode 100644 index 0000000000..951f6f1da6 Binary files /dev/null and b/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/identity_endpoints_scaffold.png differ diff --git a/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png b/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png new file mode 100644 index 0000000000..b8ac5021ee Binary files /dev/null and b/docs/en/Community-Articles/2023-11-03-ASPNET8-authentication-and-authorization/new-identity-endpoints.png differ diff --git a/docs/en/Community-Articles/2023-11-06- SerializationDeserialization-Improvements/POST.md b/docs/en/Community-Articles/2023-11-06- SerializationDeserialization-Improvements/POST.md new file mode 100644 index 0000000000..74022bd572 --- /dev/null +++ b/docs/en/Community-Articles/2023-11-06- SerializationDeserialization-Improvements/POST.md @@ -0,0 +1,100 @@ +# .NET 8: Serialization Improvements + +The [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) namespace provides functionality for serializing to and deserializing from JSON. In .NET 8, there have been many improvements to the System.Text.Json serialization and deserialization functionality. + +## Source generator + +.NET 8 includes enhancements of the System.Text.Json source generator that are aimed at making the Native AOT experience on par with the reflection-based serializer. This is important because now you can select the source generator for System.Text.Json. To see a comparison of Reflection versus source generation in System.Text.Json, check out the comparison [documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/reflection-vs-source-generation?pivots=dotnet-8-0#reflection). + +## Interface hierarchies + +.NET 8 adds support for serializing properties from interface hierarchies. Here is a code sample that shows this feature. + +```csharp +ICar car = new MyCar { Color = "Red", NumberOfWheels = 4 }; +JsonSerializer.Serialize(car); // {"Color":"Red","NumberOfWheels":4} + +public interface IVehicle +{ + public string Color { get; set; } +} + +public interface ICar : IVehicle +{ + public int NumberOfWheels { get; set; } +} + +public class MyCar : ICar +{ + public string Color { get; set; } + public int NumberOfWheels { get; set; } +} +``` + +## Naming policies + +Two new naming policies `snake_case` and `kebab-case` have been added. You can use them as shown below: + +```csharp +var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; +JsonSerializer.Serialize(new { CreationTime = new DateTime(2023,11,6) }, options); // {"creation_time":"2023-11-06T00:00:00"} +``` + +## Read-only properties + +With .NET 8, you can deserialize onto read-only fields or properties. This feature can be globally enabled by setting `PreferredObjectCreationHandling` to `JsonObjectCreationHandling.Populate`. You can also enable this feature for a class or one of its members by adding the `[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]` attribute. Here is a sample: + +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; + +var book = JsonSerializer.Deserialize("""{"Contributors":["John Doe"],"Author":{"Name":"Sample Author"}}""")!; +Console.WriteLine(JsonSerializer.Serialize(book)); + +class Author +{ + public required string Name { get; set; } +} + +[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)] +class Book +{ + // Both of these properties are read-only. + public List Contributors { get; } = new(); + public Author Author { get; } = new() {Name = "Undefined"}; +} +``` + +Before .NET 8, the output was like this: + +```json +{"Contributors":[],"Author":{"Name":"Undefined"}} +``` + +With .NET 8, the output now looks like this: + +```json +{"Contributors":["John Doe"],"Author":{"Name":"Sample Author"}} +``` + +## Disable reflection-based default + +One of the nice features about Serialization is, now you can disable using the reflection-based serializer by default. To disable default reflection-based serialization, set the `JsonSerializerIsReflectionEnabledByDefault` MSBuild property to `false` in your project file. + +## Streaming deserialization APIs + +.NET 8 includes new `IAsyncEnumerable` streaming deserialization extension methods. The new extension methods invoke streaming APIs and return `IAsyncEnumerable`. Here is a sample code that uses this new feature. + +```csharp +const string RequestUri = "https://yourwebsite.com/api/saas/tenants?skipCount=0&maxResultCount=10"; +using var client = new HttpClient(); +IAsyncEnumerable tenants = client.GetFromJsonAsAsyncEnumerable(RequestUri); + +await foreach (Tenant tenant in tenants) +{ + Console.WriteLine($"* '{tenant.name}' uses '{tenant.editionName +}' edition"); +} +``` + +I have mentioned some of the items for Serialization Improvements in .NET 8. If you want to check the full list, you can read it in the .NET 8's [What's new document](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#serialization). diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/Post.md b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/Post.md new file mode 100644 index 0000000000..a346b6f084 --- /dev/null +++ b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/Post.md @@ -0,0 +1,89 @@ +# Blazor's History and Full-stack Web UI + +Blazor is a web framework that allows developers to build interactive web applications using .NET instead of JavaScript. The first version of Blazor was **released on May 14, 2020**. Since its initial release, Blazor has evolved with the new versions. Until now, six different versions have been declared. Sometimes, it can be not very clear to see the differences between these approaches. First, let's try to understand these. + +* **Blazor-Server**: >> *Loads fast at first* >> In this version, heavy things are done in the server. Browsers are thin clients and download a small page for the first load. The page updates are done via SignalR connection. This was released with .NET Core 3. +* **Blazor WebAssembly (WASM):** >> *Loads slow at first* >> In this version, some binary files are being downloaded to the browser. This approach takes longer initialization time than the "Server" approach. The hard work is done on the browser. +* **Blazor Hybrid:** It's a combination of Blazor and Xamarin.Forms. It allows you to run your app on iOS, Android, macOS, and Windows. Blazor Hybrid uses a WebView component to host the Blazor-based user interface within the mobile app. +* **Blazor Native**: It runs natively on the devices and uses a common UI abstraction to render native controls for that device. This is very similar to how frameworks like Xamarin Forms or React Native work today. It has also been considered but has not reached [the planning stage](https://devblogs.microsoft.com/dotnet/blazor-server-in-net-core-3-0-scenarios-and-performance/). + +* **Blazor United**: Recently, Microsoft updated this name to "Fullstack web UI". Blazor-Server and Blazor WASM both have some disadvantages and advantages. So, Microsoft decided to combine these two approaches and find an optimum solution for the entire Blazor version. We can call it *Best of Blazor* 😀 + +## Why is "Blazor Fullstack Web UI" the best? + +These apps are a combination of both Blazor Server and Blazor WASM. It provides the advantages of Blazor Server and WASM. Developers would be able to more fine-tune the rendering mode. **This approach overcomes the large binary downloads of Blazor WASM, and it resolves the Blazor Server's problem, which always needs to be connected to the server via SignalR.** + +> Blazor Fullstack Web UI comes with the .NET 8, and it will be published on November 14, 2023. + +I took the following photo from Steven Sanderson's talk at NDC Porto 2023. You can read my impressions of this conf at https://volosoft.com/blog/ndc-port-2023-impressions, but after reading this one. + +![image-20231106163046763](image-20231106163046763-1699282281622-2.png) + + + +There are two basic page styles: + +* **HTML**; simple and loads fast! Eg: MVC, Razor Pages +* **Single Page Apps (SPA):** high interaction with the client and loads slower! Eg: Blazor WASM, Blazor Server... + +## HTML + SPA => Blazor Fullstack + +Blazor Fullstack (formerly United) is a technology that turns Blazor Server into a SPA style. + +.NET 8 will combine Blazor Server's server-side rendering and WebAssembly's client-side interaction. + +You can switch between two rendering modes and even mix them on the same page. With .NET 8 there also comes amazing features like; + +* [Streaming rendering](https://github.com/dotnet/aspnetcore/issues/46352): With this feature, most of the page will be rendered, and long async operations on the server will still be in progress. + +* [Progressive enhancement of form submission & navigation](https://github.com/dotnet/aspnetcore/issues/46399): With this feature, it doesn't fully reload the page after submitting the form. This gives the user a better and smoother experience. + + + + ## How it works? + +### Rendering on Server + +You can add `WebComponentRenderMode.Server` to your Blazor components so that these components will run interactively. In the below example, the list editor will make AJAX requests to the server like single-page applications. + +![image-20231106172420148](image-20231106172420148.png) + + + +And sure you can add `WebComponentRenderMode.Server` to your page level, and the complete page will be rendered as a server component. All inputs on this page can work as an interactive server component like SPA mode. + +![image-20231106172638604](image-20231106172638604.png) + + + +### Rendering on client + +You can switch to WebAssembly mode by writing `WebComponentRenderMode.WebAssembly` attribute to your page. By doing so, the whole page should run interactively using WebAssembly. This time there's no server connection anymore because it loads the binaries (WebAssembly runtimes) at the page load. + +![image-20231106173021958](image-20231106173021958.png) + +## How it works? + +To enable Blazor Full-stack Web UI, you need to write `net8.0;net7.0-browser` into the `TargetFrameworks` area of your `csproj` file. These two keywords change your app like this; `net8.0` framework renders on the server, and `net7.0-browser` framework renders on the browser. + +![image-20231106173411309](image-20231106173411309.png) + +## Let the System decide on WebAssembly or Server approach + +You can let the system decide whether it uses `WebAssembly` or `Server`. This can be done with the `Auto` mode of the `WebComponentRenderMode`. In this case, it will not load binary files (WebAssembly files) for the initial page that has `WebComponentRenderMode.Server` attribute, but whenever the user navigates to a page that has `WebComponentRenderMode.WebAssembly`, it will download the runtimes. This will allow us to load the initial page very fast, and when we need interactivity, we can switch to `WebAssembly` and wait for the binaries to download. But this download will be done one time because it will be cached. + +![image-20231106173849303](image-20231106173849303.png) + + + +## Conclusion + +I summarized the new generation Blazor in a very simple way. This architecture will be useful to everyone who uses Blazor. + + + +*Resources:* + +* You can check out Dan Roth's GitHub issue 👉 [github.com/dotnet/aspnetcore/issues/46636](https://github.com/dotnet/aspnetcore/issues/46636). +* Steven Sanderson's YouTube video is very good for understanding these concepts 👉 [Blazor United Prototype Video](https://youtu.be/48G_CEGXZZM). + diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106163046763-1699282281622-2.png b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106163046763-1699282281622-2.png new file mode 100644 index 0000000000..62c2bcea95 Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106163046763-1699282281622-2.png differ diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172420148.png b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172420148.png new file mode 100644 index 0000000000..62d2a704b8 Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172420148.png differ diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172638604.png b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172638604.png new file mode 100644 index 0000000000..f28987d536 Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106172638604.png differ diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173021958.png b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173021958.png new file mode 100644 index 0000000000..6120776e90 Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173021958.png differ diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173411309.png b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173411309.png new file mode 100644 index 0000000000..2cc92a9ee3 Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173411309.png differ diff --git a/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173849303.png b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173849303.png new file mode 100644 index 0000000000..13413ca55f Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-Blazor-Fullstack-Web-Ui/image-20231106173849303.png differ diff --git a/docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md b/docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md new file mode 100644 index 0000000000..9255c39a5a --- /dev/null +++ b/docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/POST.md @@ -0,0 +1,79 @@ +# HierarchyId Support in Entity Framework Core + +Entity Framework Core has official support for HierarchyId, which allows you to store and query hierarchical data in SQL Server databases. Hierarchical data is a common data structure found in many applications. Whether you are dealing with organizational structures, product categories, or threaded discussions, handling hierarchies efficiently is crucial. In this blog post, we will explore how to manage hierarchical data using Entity Framework Core (EF Core) in combination with HierarchyId. + +## How to use HierarchyId in EF Core + +To use HierarchyId in EF Core, you need to install the [Microsoft.EntityFrameworkCore.SqlServer.HierarchyId](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer.HierarchyId/) NuGet package. This package contains query and update support for HierarchyId. + +To configure EF Core to use HierarchyId, you need to call the `UseHierarchyId` method when configuring the SQL Server provider. + +```csharp +options.UseSqlServer( + connectionString, + x => x.UseHierarchyId()); +``` + +## Modeling Hierarchies + +The `HierarchyId` type can be used for properties of an entity type. For example, assume we want to model the personnel in an organization. Each person has a name and a manager. The manager is also a person. We can model this using the following entity type: + +```csharp +public class Person +{ + public int Id { get; set; } + public string Name { get; set; } + public HierarchyId Manager { get; set; } +} +``` + +Each person can be traced from the patriarch down the tree using its `Manager` property. SQL Server uses a compact binary format for these paths, but it is common to parse to and from a human-readable string representation when working with code. In this representation, the position at each level is separated by a `/` character. + +![Hierarchy Tree](hierarchy-tree.png) + +## Querying Hierarchies + +HierarchyId exposes several methods that can be used in LINQ queries. + +* `GetAncestor(int level)` returns the ancestor at the specified level. + +* `GetDescendant(HierarchyId? child1, HierarchyId? child2)` returns the descendant at the specified level. + +* `GetLevel()` returns the level of the node in the hierarchy. + +* `GetReparentedValue(HierarchyId? oldRoot, HierarchyId? newRoot)` returns the node with a new parent. + +* `IsDescendantOf(HierarchyId? parent)` returns whether the node is a descendant of the specified node. + +Example: + +```csharp +// Get entities at a given level in the tree +var generation = await context.Persons.Where(x => x.Manager.GetLevel() == level).ToListAsync(); + +// Get the direct ancestor of an entity +var parent = await context.Persons.Where(x => x.Manager.GetAncestor(1) == manager).SingleAsync(); +``` + +For example, your company may want to change the manager of an organizational unit. To do this, you can use the `GetReparentedValue` method to update the manager for all descendants of the organizational unit. + +```csharp +var newManager = await context.Persons.SingleAsync(x => x.Id == newManagerId); + +var descendants = await context.Persons + .Where(x => x.Manager.IsDescendantOf(oldManager)) + .ToListAsync(); + +foreach (var descendant in descendants) +{ + descendant.Manager = descendant.Manager.GetReparentedValue(oldManager.Manager, newManager.Manager); +} + +await context.SaveChangesAsync(); +``` + +## References + +For more information about hierarchy id, see the following resource: + +https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#hierarchyid-in-net-and-ef-core diff --git a/docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png b/docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png new file mode 100644 index 0000000000..e22ed9781c Binary files /dev/null and b/docs/en/Community-Articles/2023-11-06-EF-Core_Hierarchy-Id/hierarchy-tree.png differ diff --git a/docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md b/docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md new file mode 100644 index 0000000000..2b994b59c3 --- /dev/null +++ b/docs/en/Community-Articles/2023-11-97-AOT-Compilation/Post.md @@ -0,0 +1,75 @@ +# Native AOT Compilation in .NET 8 +Native AOT (Ahead-of-Time) compilation is a feature that allows developers to create a self-contained app compiled to native code that can run on machines without the .NET runtime installed. It results in benefits such as minimized disk footprint, reduced executable size, reduced startup time, and reduced memory demand. + +Native AOT compilation isn't a new feature in .NET 8. It's first introduced in .NET 7. + + +Differences between the AOT Compilation of .NET 7 and .NET 8 are: + + +- **System.Text.Json improvements**: .NET 8 adds support for more types, source generation, interface hierarchies, naming policies, read-only properties, and more. +- **New types for performance**: .NET 8 introduces new types such as FrozenDictionary, FrozenSet, SearchValues, CompositeFormat, TimeProvider, and ITimer to improve the app performance. +- **System.Numerics and System.Runtime.Intrinsics enhancements**: .NET 8 adds support for Vector512, AVX-512, IUtf8SpanFormattable, Lerp, and more. +- **System.ComponentModel.DataAnnotations additions**: .NET 8 adds new data validation attributes for cloud-native services and a new ValidateOptionsResultBuilder type. +- **Hosted services lifecycle methods**: .NET 8 adds new methods such as StartAsync, StopAsync, StartBackgroundAsync, and StopBackgroundAsync for hosted services. + +It's important to note that not all features in ASP.NET Core are currently compatible with native AOT. For more information, see [Native AOT deployment overview](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). + +## How to use Native AOT Compilation in .NET 8 + +You can add `true` in your project .csproj file to enable Native AOT Compilation. + + - For the new projects, you can create them with the `--aot` parameter. Example: `dotnet new console --aot`. + +By default, the compiler chooses a blended approach code optimization but you can specify an optimization preference inside your .csproj file. You can choose **size** or **speed** according your requirements. + +```xml +Size +``` + +or + +```xml +Speed +``` + +### Results + +I have created a simple console application to test the Native AOT Compilation. I have used a simple console application that writes "Hello World!" to the console 100 times. I have tested the application with different optimization preferences. I have used the following results: + + +| | Size | Speed | +| --- | --- | --- | +| .NET 8
_(Self-Contained, Single File)_ | 65938 kb | 00.0051806 ~5ms | +| .NET 7 AOT (default) | 4452 kb | 00.0029823 ~2ms | +| .NET 8 AOT (default) | 1242 kb | 00.0028638 ~2ms | +| AOT (Speed)| 1280 kb | 00.0023838 ~2ms | +| AOT (Size) | 1111 kb | 00.0025145 ~2ms | + +Most of existing libraries don't support AOT compilation yet, so I couldn't use [BenchmarkDotnet](https://github.com/dotnet/BenchmarkDotNet) to measure the performance. I have used [Stopwatch](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.stopwatch?view=net-8.0) to measure the performance. So the performance results may not be accurate but gives insight about the performance difference. + +## AOT Support in MAUI +You can now use Native AOT Compilation on iOS-like target frameworks in .NET MAUI. You can enable AOT compilation with the exact same method by adding `true` to your project .csproj file. According to the dotnet team, apps sizes reduced by 35% and startup times reduced by 28% with AOT compilation. And runtime performance is also improved by 50%. + +But there are some limitations in MAUI AOT Compilation. A lot of libraries still don't support AOT compilation and some of platform-specific feaetures may not work at the moment. + +## When to use Native AOT Compilation? + +Native AOT Compilation is beneficial when you need to optimize your .NET application for speed and size. It's particularly useful for applications that require quick startup times and efficient runtime performance, such as mobile apps or high-performance computing applications. + +However, due to its current limitations, it might not be suitable for all projects. If your project relies heavily on libraries that do not support AOT compilation, or if it uses platform-specific features that are not yet compatible with AOT, you might want to hold off on using Native AOT Compilation until further improvements are made. + +Always consider the specific needs and constraints of your project before deciding to use Native AOT Compilation. + +## Conclusion + +Native AOT Compilation is a great feature that improves the performance of .NET applications. It's still in early-stages and not all libraries support it yet. But it's a great beginning for the future of .NET 🚀 + + +## Links +- Native AOT deployment overview - .NET | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/. +- +- Optimize AOT deployments https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/optimizing +- +- What's new in .NET 8 | Microsoft Learn. https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8. + diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 0c5e9c4c38..fdb7569ca0 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -316,7 +316,7 @@ You should add `routes` property in the `data` object to add a link on the menu { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), - canActivate: [AuthGuard, PermissionGuard], + canActivate: [authGuard, permissionGuard], data: { routes: { name: 'ProjectName::Menu:Dashboard', @@ -328,7 +328,7 @@ You should add `routes` property in the `data` object to add a link on the menu } ``` In the above example; -* If the user is not logged in, AuthGuard blocks access and redirects to the login page. +* If the user is not logged in, authGuard blocks access and redirects to the login page. * PermissionGuard checks the user's permission with the `requiredPolicy` property of the `routes` object. If the user is not authorized to access the page, the 403 page appears. * The `name` property of `routes` is the menu link label. A localization key can be defined. * The `iconClass` property of the `routes` object is the menu link icon class. diff --git a/docs/en/Timing.md b/docs/en/Timing.md index fce17de4cf..9b3eb0cca9 100644 --- a/docs/en/Timing.md +++ b/docs/en/Timing.md @@ -102,7 +102,7 @@ This section covers the ABP Framework infrastructure related to managing time zo ### TimeZone Setting -ABP Framework defines **a setting**, named `Abp.Timing.Timezone`, that can be used to set and get the time zone for a user, [tenant](Multi-Tenancy.md) or globally for the application. The default value is `UTC`. +ABP Framework defines **a setting**, named `Abp.Timing.TimeZone`, that can be used to set and get the time zone for a user, [tenant](Multi-Tenancy.md) or globally for the application. The default value is `UTC`. See the [setting documentation](Settings.md) to learn more about the setting system. diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md index f33a41f392..f306b7f4a2 100644 --- a/docs/en/Tutorials/Part-5.md +++ b/docs/en/Tutorials/Part-5.md @@ -323,11 +323,11 @@ Open the `/src/app/book/book-routing.module.ts` and replace with the following c ````js import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AuthGuard, PermissionGuard } from '@abp/ng.core'; +import { authGuard, permissionGuard } from '@abp/ng.core'; import { BookComponent } from './book.component'; const routes: Routes = [ - { path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] }, + { path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] }, ]; @NgModule({ @@ -337,8 +337,8 @@ const routes: Routes = [ export class BookRoutingModule {} ```` -* Imported `AuthGuard` and `PermissionGuard` from the `@abp/ng.core`. -* Added `canActivate: [AuthGuard, PermissionGuard]` to the route definition. +* Imported `authGuard` and `permissionGuard` from the `@abp/ng.core`. +* Added `canActivate: [authGuard, permissionGuard]` to the route definition. Open the `/src/app/route.provider.ts` and add `requiredPolicy: 'BookStore.Books'` to the `/books` route. The `/books` route block should be following: diff --git a/docs/en/UI/Angular/Data-Table-Column-Extensions.md b/docs/en/UI/Angular/Data-Table-Column-Extensions.md index 6f1367d934..7660ec5550 100644 --- a/docs/en/UI/Angular/Data-Table-Column-Extensions.md +++ b/docs/en/UI/Angular/Data-Table-Column-Extensions.md @@ -1,6 +1,5 @@ # Data Table Column (or Entity Prop) Extensions for Angular UI - ## Introduction Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below: @@ -196,6 +195,14 @@ type PropCallback = (data?: PropData) => R; type PropPredicate = (data?: PropData) => boolean; ``` +### ColumnPredicate + +`ColumnPredicate` is the type of the predicate function that can be passed to an `EntityProp` as `columnVisible` parameter. A column predicate gets a single parameter, the `GetInjected`, you can use the `GetInjected` parameter to reach injected `Service` or `Component`. The return type must be `boolean`. Here is a simplified representation: + +```js +type ColumnPredicate = (getInjected: GetInjected) => boolean; +``` + ### EntityPropOptions\ `EntityPropOptions` is the type that defines required and optional properties you have to pass in order to create an entity prop. @@ -212,6 +219,7 @@ type EntityPropOptions = { columnWidth?: number; permission?: string; visible?: PropPredicate; + columnVisible?: ColumnPredicate; }; ``` @@ -224,9 +232,12 @@ As you see, passing `type` and `name` is enough to create an entity prop. Here i - **sortable** defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (_default:_ `false`) - **columnWidth** defines a minimum width for the column. Good for horizontal scroll. (_default:_ `undefined`) - **permission** is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (_default:_ `undefined`) -- **visible** is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (_default:_ `() => true`) +- **visible** is a predicate that will be used to decide if the cell content of this entity prop should be displayed on the table or not based on the data record. (_default:_ `() => true`) +- **columnVisible** is a predicate that will be used to decide if the column of this entity prop should be displayed on the table or not. (_default:_ `() => true`) > Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be `undefined`. Second, if some cells are displayed and others are not, the table will be broken. Use the `valueResolver` and render an empty cell when you need to hide a specific cell. +> +> `visible` predicate only hide the cell content, not the column. Use `columnVisible` to hide the entire column. You may find a full example below. @@ -257,6 +268,10 @@ const options: EntityPropOptions = { return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true'; } + columnVisible: getInjected => { + const sessionStateService = getInjected(SessionStateService); + return !sessionStateService.getTenant()?.isAvailable; // hide this column when the tenant is available. + }, }; const prop = new EntityProp(options); @@ -281,19 +296,19 @@ The items in the list will be displayed according to the linked list order, i.e. ```js export function reorderUserContributors( - propList: EntityPropList, + propList: EntityPropList ) { // drop email node const emailPropNode = propList.dropByValue( 'AbpIdentity::EmailAddress', - (prop, text) => prop.text === text, + (prop, text) => prop.text === text ); // add it back after phoneNumber propList.addAfter( emailPropNode.value, 'phoneNumber', - (value, name) => value.name === name, + (value, name) => value.name === name ); } ``` @@ -304,7 +319,7 @@ export function reorderUserContributors( ```js export function isLockedOutPropContributor( - propList: EntityPropList, + propList: EntityPropList ) { // add isLockedOutProp as 2nd column propList.add(isLockedOutProp).byIndex(1); diff --git a/docs/en/UI/Angular/Entity-Action-Extensions.md b/docs/en/UI/Angular/Entity-Action-Extensions.md index 3ab035b04a..d93b2045f6 100644 --- a/docs/en/UI/Angular/Entity-Action-Extensions.md +++ b/docs/en/UI/Angular/Entity-Action-Extensions.md @@ -16,19 +16,16 @@ In this example, we will add a "Click Me!" action and alert the current row's `u The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module: -```js +```ts // src/app/entity-action-contributors.ts -import { - eIdentityComponents, - IdentityEntityActionContributors, - IdentityUserDto, -} from '@abp/ng.identity'; +import { eIdentityComponents, IdentityEntityActionContributors } from '@abp/ng.identity'; +import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions'; const alertUserName = new EntityAction({ text: 'Click Me!', - action: data => { + action: (data) => { // Replace alert with your custom code alert(data.record.userName); }, diff --git a/docs/en/UI/Angular/OAuth-Module.md b/docs/en/UI/Angular/OAuth-Module.md index e7a62d1d2e..b747c5221d 100644 --- a/docs/en/UI/Angular/OAuth-Module.md +++ b/docs/en/UI/Angular/OAuth-Module.md @@ -6,7 +6,6 @@ If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot Those abstractions can be found in the @abp/ng-core packages. - `AuthService` (the class that implements the IAuthService interface). - `NAVIGATE_TO_MANAGE_PROFILE` Inject token. -- `AuthGuard` (the class that implements the IAuthGuard interface). - `ApiInterceptor` (the class that implements the IApiInterceptor interface). Those base classes are overridden by the "AbpOAuthModule" for oAuth. There are also three functions provided with AbpOAuthModule. diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md index dd373d6274..63d919f599 100644 --- a/docs/en/UI/Angular/Permission-Management.md +++ b/docs/en/UI/Angular/Permission-Management.md @@ -54,20 +54,20 @@ As shown above you can remove elements from DOM with `abpPermission` structural ## Permission Guard -You can use `PermissionGuard` if you want to control authenticated user's permission to access to the route during navigation. +You can use `permissionGuard` if you want to control authenticated user's permission to access to the route during navigation. * Import the PermissionGuard from @abp/ng.core. -* Add `canActivate: [PermissionGuard]` to your route object. +* Add `canActivate: [permissionGuard]` to your route object. * Add `requiredPolicy` to the `data` property of your route in your routing module. ```js -import { PermissionGuard } from '@abp/ng.core'; +import { permissionGuard } from '@abp/ng.core'; // ... const routes: Routes = [ { path: 'path', component: YourComponent, - canActivate: [PermissionGuard], + canActivate: [permissionGuard], data: { requiredPolicy: 'YourProjectName.YourComponent', // policy key for your component }, diff --git a/docs/en/UI/AspNetCore/Bundling-Minification.md b/docs/en/UI/AspNetCore/Bundling-Minification.md index dd2282c883..6164d00c7d 100644 --- a/docs/en/UI/AspNetCore/Bundling-Minification.md +++ b/docs/en/UI/AspNetCore/Bundling-Minification.md @@ -382,6 +382,83 @@ Configure(options => ```` +### External/CDN file Support + +The bundling system automatically recognizes the external/CDN files and adds them to the page without any change. + +#### Using External/CDN files in `AbpBundlingOptions` + +````csharp +Configure(options => +{ + options.StyleBundles + .Add("MyStyleBundle", configuration => + { + configuration + .AddFiles("/styles/my-style1.css") + .AddFiles("/styles/my-style2.css") + .AddFiles("https://cdn.abp.io/bootstrap.css") + .AddFiles("/styles/my-style3.css") + .AddFiles("/styles/my-style4.css"); + }); + + options.ScriptBundles + .Add("MyScriptBundle", configuration => + { + configuration + .AddFiles("/scripts/my-script1.js") + .AddFiles("/scripts/my-script2.js") + .AddFiles("https://cdn.abp.io/bootstrap.js") + .AddFiles("/scripts/my-script3.js") + .AddFiles("/scripts/my-script4.js"); + }); +}); +```` + +**Output HTML:** + +````html + + + + + + + +```` + +#### Using External/CDN files in Tag Helpers. + +````html + + + + + + + + + + + + + + + +```` + +**Output HTML:** + +````html + + + + + + + +```` + ## Themes Themes uses the standard package contributors to add library resources to page layouts. Themes may also define some standard/global bundles, so any module can contribute to these standard/global bundles. See the [theming documentation](Theming.md) for more. diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md index 91d7308226..d6733b8633 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -302,7 +302,7 @@ ABP 配置模块也已经导入到 `AppModule` 中, 以满足可延迟加载 ABP { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), - canActivate: [AuthGuard, PermissionGuard], + canActivate: [authGuard, permissionGuard], data: { routes: { name: 'ProjectName::Menu:Dashboard', @@ -315,7 +315,7 @@ ABP 配置模块也已经导入到 `AppModule` 中, 以满足可延迟加载 ABP ``` 在上面的例子中; * 如果用户没有登录, AuthGuard 会阻塞访问并重定向到登录页面. -* PermissionGuard 使用 `rotues` 对象的 `requiredPolicy` 属性检查用户的权限. 如果用户未被授权访问该页, 则显示403页. +* permissionGuard 使用 `rotues` 对象的 `requiredPolicy` 属性检查用户的权限. 如果用户未被授权访问该页, 则显示403页. * `routes` 的 `name` 属性是菜单链接标签. 可以定义本地化 key. * `routes` 对象的 `iconClass` 属性是菜单链接图标类. * `routes` 对象的 `requiredPolicy` 属性是访问页面所需的策略 key. diff --git a/docs/zh-Hans/Timing.md b/docs/zh-Hans/Timing.md index 71ddbb9058..1d220a7aad 100644 --- a/docs/zh-Hans/Timing.md +++ b/docs/zh-Hans/Timing.md @@ -102,7 +102,7 @@ var normalizedDateTime = Clock.Normalize(dateTime) ### 时区设置 -ABP框架定义了一个名为 `Abp.Timing.Timezone` 的**设置**,可用于为应用程序的用户,[租户](Multi-Tenancy.md)或全局设置和获取时区. 默认值为 `UTC`. +ABP框架定义了一个名为 `Abp.Timing.TimeZone` 的**设置**,可用于为应用程序的用户,[租户](Multi-Tenancy.md)或全局设置和获取时区. 默认值为 `UTC`. 参阅[设置系统]了解更多关于设置系统. @@ -110,4 +110,4 @@ ABP框架定义了一个名为 `Abp.Timing.Timezone` 的**设置**,可用于为 `ITimezoneProvider` 是一个服务,可将[Windows时区ID](https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values)值简单转换为[Iana时区名称](https://www.iana.org/time-zones)值,反之亦然. 它还提供了获取这些时区列表与获取具有给定名称的 `TimeZoneInfo` 的方法. -它已使用[TimeZoneConverter](https://github.com/mj1856/TimeZoneConverter)库实现. \ No newline at end of file +它已使用[TimeZoneConverter](https://github.com/mj1856/TimeZoneConverter)库实现. diff --git a/docs/zh-Hans/Tutorials/Part-5.md b/docs/zh-Hans/Tutorials/Part-5.md index 5bdad55c1c..ec308f3ee7 100644 --- a/docs/zh-Hans/Tutorials/Part-5.md +++ b/docs/zh-Hans/Tutorials/Part-5.md @@ -389,11 +389,11 @@ UI的第一步是防止未认证用户看见"图书"菜单项并进入图书管 ````js import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; -import { AuthGuard, PermissionGuard } from '@abp/ng.core'; +import { authGuard, permissionGuard } from '@abp/ng.core'; import { BookComponent } from './book.component'; const routes: Routes = [ - { path: '', component: BookComponent, canActivate: [AuthGuard, PermissionGuard] }, + { path: '', component: BookComponent, canActivate: [authGuard, permissionGuard] }, ]; @NgModule({ @@ -403,8 +403,8 @@ const routes: Routes = [ export class BookRoutingModule {} ```` -* 从 `@abp/ng.core` 引入 `AuthGuard` 和 `PermissionGuard`. -* 在路由定义中添加 `canActivate: [AuthGuard, PermissionGuard]`. +* 从 `@abp/ng.core` 引入 `authGuard` 和 `permissionGuard`. +* 在路由定义中添加 `canActivate: [authGuard, permissionGuard]`. 打开 `/src/app/route.provider.ts`, 在 `/books` 路由中添加 `requiredPolicy: 'BookStore.Books'`. `/books` 路由应该如以下配置: diff --git a/docs/zh-Hans/UI/Angular/Permission-Management.md b/docs/zh-Hans/UI/Angular/Permission-Management.md index 08496f03fe..05e3dacbf1 100644 --- a/docs/zh-Hans/UI/Angular/Permission-Management.md +++ b/docs/zh-Hans/UI/Angular/Permission-Management.md @@ -53,20 +53,20 @@ export class YourComponent { ## 权限守卫 -如果你想要在导航过程中控制经过身份验证的用户对路由的访问权限,可以使用 `PermissionGuard`. +如果你想要在导航过程中控制经过身份验证的用户对路由的访问权限,可以使用 `permissionGuard`. -* 从@abp/ng.core导入PermissionGuard. -* 添加 `canActivate: [PermissionGuard]` 到你的路由对象. +* 从@abp/ng.core导入permissionGuard. +* 添加 `canActivate: [permissionGuard]` 到你的路由对象. * 添加 `requiredPolicy` 到路由模块路由的 `data` 属性. ```js -import { PermissionGuard } from '@abp/ng.core'; +import { permissionGuard } from '@abp/ng.core'; // ... const routes: Routes = [ { path: 'path', component: YourComponent, - canActivate: [PermissionGuard], + canActivate: [permissionGuard], data: { requiredPolicy: 'YourProjectName.YourComponent', // policy key for your component }, diff --git a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md index 338afe8967..72bee2eedb 100644 --- a/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md +++ b/docs/zh-Hans/UI/AspNetCore/Bundling-Minification.md @@ -353,6 +353,83 @@ services.Configure(options => }); ```` +### 外部/CDN文件支持 + +捆绑系统会自动识别外部/CDN文件,并将其添加到页面中,无需进行任何更改。 + +#### 在`AbpBundlingOptions`中添加外部/CDN文件 + +````csharp +Configure(options => +{ + options.StyleBundles + .Add("MyStyleBundle", configuration => + { + configuration + .AddFiles("/styles/my-style1.css") + .AddFiles("/styles/my-style2.css") + .AddFiles("https://cdn.abp.io/bootstrap.css") + .AddFiles("/styles/my-style3.css") + .AddFiles("/styles/my-style4.css"); + }); + + options.ScriptBundles + .Add("MyScriptBundle", configuration => + { + configuration + .AddFiles("/scripts/my-script1.js") + .AddFiles("/scripts/my-script2.js") + .AddFiles("https://cdn.abp.io/bootstrap.js") + .AddFiles("/scripts/my-script3.js") + .AddFiles("/scripts/my-script4.js"); + }); +}); +```` + +**输出HTMl:** + +````html + + + + + + + +```` + +#### 在TagHelpers中添加外部/CDN文件 + +````html + + + + + + + + + + + + + + + +```` + +**输出HTMl:** + +````html + + + + + + + +```` + ### 主题 主题使用标准包贡献者将库资源添加到页面布局. 主题还可以定义一些标准/全局包, 因此任何模块都可以为这些标准/全局包做出贡献. 有关更多信息, 请参阅[主题文档](Theming.md). diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs index 3223be417c..3a94e1a568 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Bundling/BlazorGlobalStyleContributor.cs @@ -17,5 +17,6 @@ public class BlazorGlobalStyleContributor : BundleContributor context.Files.AddIfNotContains("/_content/Blazorise/blazorise.css"); context.Files.AddIfNotContains("/_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); context.Files.AddIfNotContains("/_content/Blazorise.Snackbar/blazorise.snackbar.css"); + context.Files.AddIfNotContains("/_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css"); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs index 61f42e8222..45b06e5484 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming/ComponentsComponentsBundleContributor.cs @@ -26,5 +26,6 @@ public class ComponentsComponentsBundleContributor : IBundleContributor context.Add("_content/Blazorise/blazorise.css"); context.Add("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); context.Add("_content/Blazorise.Snackbar/blazorise.snackbar.css"); + context.Add("_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css"); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs index b67c8f6d3d..bc4dc14d11 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationContext.cs @@ -8,7 +8,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; public class BundleConfigurationContext : IBundleConfigurationContext { - public List Files { get; } + public List Files { get; } public IFileProvider FileProvider { get; } @@ -18,7 +18,7 @@ public class BundleConfigurationContext : IBundleConfigurationContext public BundleConfigurationContext(IServiceProvider serviceProvider, IFileProvider fileProvider) { - Files = new List(); + Files = new List(); ServiceProvider = serviceProvider; LazyServiceProvider = ServiceProvider.GetRequiredService(); FileProvider = fileProvider; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs index 3ee0fe6cd9..62f7950c5a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; @@ -10,6 +11,18 @@ public static class BundleConfigurationExtensions return bundleConfiguration; } + public static BundleConfiguration AddFiles(this BundleConfiguration bundleConfiguration, params BundleFile[] files) + { + bundleConfiguration.Contributors.AddFiles(files); + return bundleConfiguration; + } + + public static BundleConfiguration AddExternalFiles(this BundleConfiguration bundleConfiguration, params string[] files) + { + bundleConfiguration.Contributors.AddExternalFiles(files.Select(x => new BundleFile(x, true)).ToArray()); + return bundleConfiguration; + } + public static BundleConfiguration AddContributors(this BundleConfiguration bundleConfiguration, params IBundleContributor[] contributors) { Check.NotNull(contributors, nameof(contributors)); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs index dc0ac8bfbb..addbe7e2cd 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs @@ -6,4 +6,14 @@ public static class BundleContributorCollectionExtensions { contributors.Add(new BundleFileContributor(files)); } + + public static void AddFiles(this BundleContributorCollection contributors, params BundleFile[] files) + { + contributors.Add(new BundleFileContributor(files)); + } + + public static void AddExternalFiles(this BundleContributorCollection contributors, params BundleFile[] files) + { + contributors.Add(new BundleFileContributor(files)); + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFile.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFile.cs new file mode 100644 index 0000000000..fbd6c3eaa1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFile.cs @@ -0,0 +1,31 @@ +using System; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +public class BundleFile +{ + public string FileName { get; set; } + + public bool IsExternalFile { get; set; } + + public BundleFile(string fileName) + { + FileName = fileName; + IsExternalFile = fileName.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + fileName.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + } + + public BundleFile(string fileName, bool isExternalFile) + { + FileName = fileName; + IsExternalFile = isExternalFile; + } + + /// + /// This method is used to compatible with old code. + /// + public static implicit operator BundleFile(string fileName) + { + return new BundleFile(fileName); + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs index 7f218e7fbf..df55266110 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleFileContributor.cs @@ -1,22 +1,30 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; public class BundleFileContributor : BundleContributor { - public string[] Files { get; } + public List Files { get; } + + public BundleFileContributor(params BundleFile[] files) + { + Files = new List(); + Files.AddRange(files); + } public BundleFileContributor(params string[] files) { - Files = files ?? Array.Empty(); + Files = new List(); + Files.AddRange(files.Select(file => new BundleFile(file))); } public override void ConfigureBundle(BundleConfigurationContext context) { foreach (var file in Files) { - context.Files.AddIfNotContains(x => x.Equals(file, StringComparison.OrdinalIgnoreCase), () => file); + context.Files.AddIfNotContains(x => x.FileName.Equals(file.FileName, StringComparison.OrdinalIgnoreCase), () => file); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs index 61d772d638..96df341514 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleConfigurationContext.cs @@ -5,5 +5,5 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; public interface IBundleConfigurationContext : IServiceProviderAccessor { - List Files { get; } + List Files { get; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs index 5c3890a852..f6830cbd74 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleCacheItem.cs @@ -5,11 +5,11 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; public class BundleCacheItem { - public List Files { get; } + public List Files { get; } public List WatchDisposeHandles { get; } - public BundleCacheItem(List files) + public BundleCacheItem(List files) { Files = files; WatchDisposeHandles = new List(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs index 8291c2c75f..20c33e86e5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleManager.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Mvc.UI.Bundling.Scripts; using Volo.Abp.AspNetCore.Mvc.UI.Bundling.Styles; using Volo.Abp.AspNetCore.Mvc.UI.Resources; -using Volo.Abp.AspNetCore.VirtualFileSystem; using Volo.Abp.DependencyInjection; using Volo.Abp.VirtualFileSystem; @@ -56,18 +55,20 @@ public class BundleManager : IBundleManager, ITransientDependency Logger = NullLogger.Instance; } - public virtual async Task> GetStyleBundleFilesAsync(string bundleName) + public virtual async Task> GetStyleBundleFilesAsync(string bundleName) { return await GetBundleFilesAsync(Options.StyleBundles, bundleName, StyleBundler); } - public virtual async Task> GetScriptBundleFilesAsync(string bundleName) + public virtual async Task> GetScriptBundleFilesAsync(string bundleName) { return await GetBundleFilesAsync(Options.ScriptBundles, bundleName, ScriptBundler); } - protected virtual async Task> GetBundleFilesAsync(BundleConfigurationCollection bundles, string bundleName, IBundler bundler) + protected virtual async Task> GetBundleFilesAsync(BundleConfigurationCollection bundles, string bundleName, IBundler bundler) { + var files = new List(); + var contributors = GetContributors(bundles, bundleName); var bundleFiles = RequestResources.TryAdd(await GetBundleFilesAsync(contributors)); var dynamicResources = RequestResources.TryAdd(await GetDynamicResourcesAsync(contributors)); @@ -77,16 +78,45 @@ public class BundleManager : IBundleManager, ITransientDependency return bundleFiles.Union(dynamicResources).ToImmutableList(); } + var localBundleFiles = new List(); + foreach (var bundleFile in bundleFiles) + { + if (!bundleFile.IsExternalFile) + { + localBundleFiles.Add(bundleFile.FileName); + } + else + { + if (localBundleFiles.Count != 0) + { + files.AddRange(AddToBundleCache(bundleName, bundler, localBundleFiles).Files); + localBundleFiles.Clear(); + } + + files.Add(bundleFile); + } + } + + if (localBundleFiles.Count != 0) + { + files.AddRange(AddToBundleCache(bundleName, bundler, localBundleFiles).Files); + } + + return files.Union(dynamicResources).ToImmutableList(); + } + + private BundleCacheItem AddToBundleCache(string bundleName, IBundler bundler, List bundleFiles) + { var bundleRelativePath = Options.BundleFolderName.EnsureEndsWith('/') + bundleName + "." + bundleFiles.JoinAsString("|").ToMd5() + "." + bundler.FileExtension; - var cacheItem = BundleCache.GetOrAdd(bundleRelativePath, () => + return BundleCache.GetOrAdd(bundleRelativePath, () => { var cacheValue = new BundleCacheItem( - new List + new List { - "/" + bundleRelativePath + new BundleFile("/" + bundleRelativePath) } ); @@ -104,8 +134,6 @@ public class BundleManager : IBundleManager, ITransientDependency return cacheValue; }); - - return cacheItem.Files.Union(dynamicResources).ToImmutableList(); } private void WatchChanges(BundleCacheItem cacheValue, List files, string bundleRelativePath) @@ -176,7 +204,7 @@ public class BundleManager : IBundleManager, ITransientDependency } } - protected async Task> GetBundleFilesAsync(List contributors) + protected async Task> GetBundleFilesAsync(List contributors) { var context = CreateBundleConfigurationContext(); @@ -198,7 +226,7 @@ public class BundleManager : IBundleManager, ITransientDependency return context.Files; } - protected virtual async Task> GetDynamicResourcesAsync(List contributors) + protected virtual async Task> GetDynamicResourcesAsync(List contributors) { var context = CreateBundleConfigurationContext(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs index b4ce56cb62..067554e102 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/IBundleManager.cs @@ -5,7 +5,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; public interface IBundleManager { - Task> GetStyleBundleFilesAsync(string bundleName); + Task> GetStyleBundleFilesAsync(string bundleName); - Task> GetScriptBundleFilesAsync(string bundleName); + Task> GetScriptBundleFilesAsync(string bundleName); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs index 1240dbef11..03ed327dc0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpBundleItemTagHelper.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Razor.TagHelpers; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers; namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers; @@ -49,7 +50,7 @@ public abstract class AbpBundleItemTagHelper : Ab if (Src != null) { - return new BundleTagHelperFileItem(Src); + return new BundleTagHelperFileItem(new BundleFile(Src)); } throw new AbpException("abp-script tag helper requires to set either src or type!"); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs index a247022674..9be91c535f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperResourceService.cs @@ -3,10 +3,12 @@ using Microsoft.AspNetCore.Razor.TagHelpers; using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -63,18 +65,24 @@ public abstract class AbpTagHelperResourceService : ITransientDependency foreach (var bundleFile in bundleFiles) { - var file = HostingEnvironment.WebRootFileProvider.GetFileInfo(bundleFile); - - if (file == null || !file.Exists) + if (bundleFile.IsExternalFile) { - Logger.LogError($"Could not find the bundle file '{bundleFile}' for the bundle '{bundleName}'!"); - AddErrorScript(viewContext, tagHelper, context, output, bundleFile, bundleName!); - continue; + AddHtmlTag(viewContext, tagHelper, context, output, bundleFile, null); } - - if (file.Length > 0) + else { - AddHtmlTag(viewContext, tagHelper, context, output, bundleFile + "?_v=" + file.LastModified.UtcTicks); + var file = HostingEnvironment.WebRootFileProvider.GetFileInfo(bundleFile.FileName); + if (file == null || !file.Exists) + { + Logger.LogError($"Could not find the bundle file '{bundleFile.FileName}' for the bundle '{bundleName}'!"); + AddErrorScript(viewContext, tagHelper, context, output, bundleFile, bundleName!); + continue; + } + + if (file.Length > 0) + { + AddHtmlTag(viewContext, tagHelper, context, output, bundleFile, file); + } } } @@ -84,13 +92,13 @@ public abstract class AbpTagHelperResourceService : ITransientDependency protected abstract void CreateBundle(string bundleName, List bundleItems); - protected abstract Task> GetBundleFilesAsync(string bundleName); + protected abstract Task> GetBundleFilesAsync(string bundleName); - protected abstract void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file); + protected abstract void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, IFileInfo? fileInfo = null); - protected virtual void AddErrorScript(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file, string bundleName) + protected virtual void AddErrorScript(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, string bundleName) { - output.Content.AppendHtml($"{Environment.NewLine}"); + output.Content.AppendHtml($"{Environment.NewLine}"); } protected virtual string GenerateBundleName(List bundleItems) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs index 260f28f812..c8486ebadd 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperScriptService.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -32,12 +33,12 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService ); } - protected override async Task> GetBundleFilesAsync(string bundleName) + protected override async Task> GetBundleFilesAsync(string bundleName) { return await BundleManager.GetScriptBundleFilesAsync(bundleName); } - protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file) + protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, IFileInfo? fileInfo = null) { var defer = tagHelper switch { @@ -46,12 +47,13 @@ public class AbpTagHelperScriptService : AbpTagHelperResourceService _ => false }; - var deferText = (defer || Options.DeferScriptsByDefault || Options.DeferScripts.Any(x => file.StartsWith(x, StringComparison.OrdinalIgnoreCase))) - ? "defer" + var deferText = (defer || Options.DeferScriptsByDefault || Options.DeferScripts.Any(x => file.FileName.StartsWith(x, StringComparison.OrdinalIgnoreCase))) + ? "defer " : string.Empty; var nonceText = (viewContext.HttpContext.Items.TryGetValue(AbpAspNetCoreConsts.ScriptNonceKey, out var nonce) && nonce is string nonceString && !string.IsNullOrEmpty(nonceString)) - ? $"nonce=\"{nonceString}\"" + ? $"nonce=\"{nonceString}\" " : string.Empty; - output.Content.AppendHtml($"{Environment.NewLine}"); + var src = file.IsExternalFile ? file.FileName : viewContext.GetUrlHelper().Content((file.FileName + "?_v=" + fileInfo!.LastModified.UtcTicks).EnsureStartsWith('~')); + output.Content.AppendHtml($"{Environment.NewLine}"); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs index 0f68433f4c..bcfd7d95e2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/AbpTagHelperStyleService.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Volo.Abp.AspNetCore.Security; @@ -18,7 +19,7 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService public AbpTagHelperStyleService( IBundleManager bundleManager, IOptions options, - IWebHostEnvironment hostingEnvironment, + IWebHostEnvironment hostingEnvironment, IOptions securityHeadersOptions) : base( bundleManager, options, @@ -36,12 +37,12 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService ); } - protected override async Task> GetBundleFilesAsync(string bundleName) + protected override async Task> GetBundleFilesAsync(string bundleName) { return await BundleManager.GetStyleBundleFilesAsync(bundleName); } - protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, string file) + protected override void AddHtmlTag(ViewContext viewContext, TagHelper tagHelper, TagHelperContext context, TagHelperOutput output, BundleFile file, IFileInfo? fileInfo = null) { var preload = tagHelper switch { @@ -50,15 +51,16 @@ public class AbpTagHelperStyleService : AbpTagHelperResourceService _ => false }; - if (preload || Options.PreloadStylesByDefault || Options.PreloadStyles.Any(x => file.StartsWith(x, StringComparison.OrdinalIgnoreCase))) + var href = file.IsExternalFile ? file.FileName : viewContext.GetUrlHelper().Content((file.FileName + "?_v=" + fileInfo!.LastModified.UtcTicks).EnsureStartsWith('~')); + if (preload || Options.PreloadStylesByDefault || Options.PreloadStyles.Any(x => file.FileName.StartsWith(x, StringComparison.OrdinalIgnoreCase))) { output.Content.AppendHtml(SecurityHeadersOptions.UseContentSecurityPolicyScriptNonce - ? $"{Environment.NewLine}" - : $"{Environment.NewLine}"); + ? $"{Environment.NewLine}" + : $"{Environment.NewLine}"); } else { - output.Content.AppendHtml($"{Environment.NewLine}"); + output.Content.AppendHtml($"{Environment.NewLine}"); } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs index 45ab8b64eb..b5f6ca74aa 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Bundling/TagHelpers/BundleTagHelperFileItem.cs @@ -5,16 +5,16 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers; public class BundleTagHelperFileItem : BundleTagHelperItem { [NotNull] - public string File { get; } + public BundleFile File { get; } - public BundleTagHelperFileItem([NotNull] string file) + public BundleTagHelperFileItem([NotNull] BundleFile file) { File = Check.NotNull(file, nameof(file)); } public override string ToString() { - return File; + return File.FileName; } public override void AddToConfiguration(BundleConfiguration configuration) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs index 2eefbcc0dd..9479dec8ce 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/IWebRequestResources.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; namespace Volo.Abp.AspNetCore.Mvc.UI.Resources; public interface IWebRequestResources { - List TryAdd(List resources); + List TryAdd(List resources); } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs index 3e046499ab..e68b671fc6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling/Volo/Abp/AspNetCore/Mvc/UI/Resources/WebRequestResources.cs @@ -1,23 +1,24 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.DependencyInjection; namespace Volo.Abp.AspNetCore.Mvc.UI.Resources; public class WebRequestResources : IWebRequestResources, IScopedDependency { - protected Dictionary> Resources { get; } + protected Dictionary> Resources { get; } protected IHttpContextAccessor HttpContextAccessor { get; } public WebRequestResources(IHttpContextAccessor httpContextAccessor) { HttpContextAccessor = httpContextAccessor; - Resources = new Dictionary>(); + Resources = new Dictionary>(); } - public List TryAdd(List resources) + public List TryAdd(List resources) { var path = HttpContextAccessor.HttpContext?.Request?.Path ?? ""; diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs index 4970015a46..7ba8105d43 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/AbpAspNetCoreMvcUiPackagesModule.cs @@ -30,11 +30,13 @@ public class AbpAspNetCoreMvcUiPackagesModule : AbpModule //moment options.AddLanguagesMapOrUpdate(MomentScriptContributor.PackageName, new NameValue("zh-Hans", "zh-cn"), - new NameValue("zh-Hant", "zh-tw")); + new NameValue("zh-Hant", "zh-tw"), + new NameValue("de-DE", "de")); options.AddLanguageFilesMapOrUpdate(MomentScriptContributor.PackageName, new NameValue("zh-Hans", "zh-cn"), - new NameValue("zh-Hant", "zh-tw")); + new NameValue("zh-Hant", "zh-tw"), + new NameValue("de-DE", "de")); //Timeago options.AddLanguageFilesMapOrUpdate(TimeagoScriptContributor.PackageName, diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs index 2ac299959a..2ca7c191c8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalScriptContributor.cs @@ -35,20 +35,20 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; )] public class SharedThemeGlobalScriptContributor : BundleContributor { - + public override void ConfigureBundle(BundleConfigurationContext context) { - context.Files.AddRange(new[] + context.Files.AddRange(new BundleFile[] { - "/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/widget-manager.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/sweetalert2/abp-sweetalert2.js", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/toastr/abp-toastr.js" + "/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/jquery-extensions.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery-form/jquery-form-extensions.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/jquery/widget-manager.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/modal-manager.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/sweetalert2/abp-sweetalert2.js", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/toastr/abp-toastr.js" }); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs index 01f490425c..f959d73f05 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/Bundling/SharedThemeGlobalStyleContributor.cs @@ -27,10 +27,10 @@ public class SharedThemeGlobalStyleContributor : BundleContributor { public override void ConfigureBundle(BundleConfigurationContext context) { - context.Files.AddRange(new[] + context.Files.AddRange(new BundleFile[] { - "/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-styles.css", - "/libs/abp/aspnetcore-mvc-ui-theme-shared/date-range-picker/date-range-picker-styles.css" - }); + "/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-styles.css", + "/libs/abp/aspnetcore-mvc-ui-theme-shared/date-range-picker/date-range-picker-styles.css" + }); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js index 1baa46de9a..8e567d626a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/bootstrap/dom-event-handlers.js @@ -417,7 +417,7 @@ var isUtc = options.isUtc; dateFormat = dateFormat || options.dateFormat; if (!date) { - return isUtc ? moment.utc() : moment(); + return isUtc ? moment.utc().startOf('day') : moment().startOf('day'); } if (isUtc) { diff --git a/framework/src/Volo.Abp.BlazoriseUI/wwwroot/volo.abp.blazoriseui.css b/framework/src/Volo.Abp.BlazoriseUI/wwwroot/volo.abp.blazoriseui.css new file mode 100644 index 0000000000..84814c2ef3 --- /dev/null +++ b/framework/src/Volo.Abp.BlazoriseUI/wwwroot/volo.abp.blazoriseui.css @@ -0,0 +1,60 @@ +.radar-spinner, .radar-spinner * { + box-sizing: border-box; +} + +.radar-spinner { + height: 60px; + width: 60px; + position: relative; +} + +.radar-spinner .circle { + position: absolute; + height: 100%; + width: 100%; + top: 0; + left: 0; + animation: radar-spinner-animation 2s infinite; +} + +.radar-spinner .circle:nth-child(1) { + padding: calc(60px * 5 * 2 * 0 / 110); + animation-delay: 300ms; +} + +.radar-spinner .circle:nth-child(2) { + padding: calc(60px * 5 * 2 * 1 / 110); + animation-delay: 300ms; +} + +.radar-spinner .circle:nth-child(3) { + padding: calc(60px * 5 * 2 * 2 / 110); + animation-delay: 300ms; +} + +.radar-spinner .circle:nth-child(4) { + padding: calc(60px * 5 * 2 * 3 / 110); + animation-delay: 0ms; +} + +.radar-spinner .circle-inner, .radar-spinner .circle-inner-container { + height: 100%; + width: 100%; + border-radius: 50%; + border: calc(60px * 5 / 110) solid transparent; +} + +.radar-spinner .circle-inner { + border-left-color: var(--secondary, #ff1d5e); + border-right-color: var(--secondary, #ff1d5e); +} + +@keyframes radar-spinner-animation { + 50% { + transform: rotate(180deg); + } + + 100% { + transform: rotate(0deg); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj b/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj index 92377ef51a..34a505110c 100644 --- a/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj +++ b/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj @@ -12,6 +12,7 @@ + diff --git a/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs b/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs index 43e9dbc953..92a2961cd4 100644 --- a/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs +++ b/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs @@ -1,10 +1,14 @@ using System; +using System.Globalization; using System.Net.Http; +using System.Net.Http.Headers; using System.Text.Json; using Dapr.Client; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Json.SystemTextJson; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Tracing; namespace Volo.Abp.Dapr; @@ -13,13 +17,21 @@ public class AbpDaprClientFactory : IAbpDaprClientFactory, ISingletonDependency protected AbpDaprOptions DaprOptions { get; } protected JsonSerializerOptions JsonSerializerOptions { get; } protected IDaprApiTokenProvider DaprApiTokenProvider { get; } + protected ICurrentTenant CurrentTenant { get; } + protected ICorrelationIdProvider CorrelationIdProvider { get; } + protected IOptions AbpCorrelationIdOptions { get; } public AbpDaprClientFactory( IOptions options, IOptions systemTextJsonSerializerOptions, - IDaprApiTokenProvider daprApiTokenProvider) + IDaprApiTokenProvider daprApiTokenProvider, + ICurrentTenant currentTenant, ICorrelationIdProvider correlationIdProvider, + IOptions abpCorrelationIdOptions) { DaprApiTokenProvider = daprApiTokenProvider; + CurrentTenant = currentTenant; + CorrelationIdProvider = correlationIdProvider; + AbpCorrelationIdOptions = abpCorrelationIdOptions; DaprOptions = options.Value; JsonSerializerOptions = CreateJsonSerializerOptions(systemTextJsonSerializerOptions.Value); } @@ -61,11 +73,39 @@ public class AbpDaprClientFactory : IAbpDaprClientFactory, ISingletonDependency daprEndpoint = DaprOptions.HttpEndpoint; } - return DaprClient.CreateInvokeHttpClient( + var httpClient = DaprClient.CreateInvokeHttpClient( appId, daprEndpoint, daprApiToken ?? DaprApiTokenProvider.GetDaprApiToken() ); + + AddHeaders(httpClient); + + return httpClient; + } + + protected virtual void AddHeaders(HttpClient httpClient) + { + //CorrelationId + httpClient.DefaultRequestHeaders.Add(AbpCorrelationIdOptions.Value.HttpHeaderName, CorrelationIdProvider.Get()); + + //TenantId + if (CurrentTenant.Id.HasValue) + { + //TODO: Use AbpAspNetCoreMultiTenancyOptions to get the key + httpClient.DefaultRequestHeaders.Add(TenantResolverConsts.DefaultTenantKey, CurrentTenant.Id.Value.ToString()); + } + + //Culture + //TODO: Is that the way we want? Couldn't send the culture (not ui culture) + var currentCulture = CultureInfo.CurrentUICulture.Name ?? CultureInfo.CurrentCulture.Name; + if (!currentCulture.IsNullOrEmpty()) + { + httpClient.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue(currentCulture)); + } + + //X-Requested-With + httpClient.DefaultRequestHeaders.Add("X-Requested-With", "XMLHttpRequest"); } protected virtual JsonSerializerOptions CreateJsonSerializerOptions(AbpSystemTextJsonSerializerOptions systemTextJsonSerializerOptions) diff --git a/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs b/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs index e1ca4bb0db..65605bc342 100644 --- a/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs +++ b/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprModule.cs @@ -4,10 +4,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Volo.Abp.Json; using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.Dapr; -[DependsOn(typeof(AbpJsonModule))] +[DependsOn(typeof(AbpJsonModule), typeof(AbpMultiTenancyAbstractionsModule))] public class AbpDaprModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 3274360ba2..799397218c 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -161,16 +161,18 @@ public class EfCoreRepository : RepositoryBase, IE { var dbContext = await GetDbContextAsync(); - dbContext.Attach(entity); - - var updatedEntity = dbContext.Update(entity).Entity; + if (dbContext.Set().Local.All(e => e != entity)) + { + dbContext.Set().Attach(entity); + dbContext.Update(entity); + } if (autoSave) { await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken)); } - return updatedEntity; + return entity; } public async override Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs index cb97111c59..dc3754be3b 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EntityHistory/EntityHistoryHelper.cs @@ -85,8 +85,10 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency case EntityState.Modified: changeType = IsDeleted(entityEntry) ? EntityChangeType.Deleted : EntityChangeType.Updated; break; - case EntityState.Detached: case EntityState.Unchanged: + changeType = EntityChangeType.Updated; // Navigation property changes. + break; + case EntityState.Detached: default: return null; } @@ -184,6 +186,21 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency } } + if (entityEntry.State == EntityState.Unchanged) + { + foreach (var navigation in entityEntry.Navigations) + { + if (navigation.IsModified || (navigation is ReferenceEntry && navigation.As().TargetEntry?.State == EntityState.Modified)) + { + propertyChanges.Add(new EntityPropertyChangeInfo + { + PropertyName = navigation.Metadata.Name, + PropertyTypeFullName = navigation.Metadata.ClrType.GetFirstGenericArgumentIfNullable().FullName! + }); + } + } + } + return propertyChanges; } @@ -205,12 +222,26 @@ public class EntityHistoryHelper : IEntityHistoryHelper, ITransientDependency protected virtual bool ShouldSaveEntityHistory(EntityEntry entityEntry, bool defaultValue = false) { - if (entityEntry.State == EntityState.Detached || - entityEntry.State == EntityState.Unchanged) + if (entityEntry.State == EntityState.Detached) { return false; } + if (entityEntry.State == EntityState.Unchanged) + { + if (entityEntry.Navigations.Any(navigationEntry => navigationEntry.IsModified)) + { + return true; + } + + if (entityEntry.Navigations.Where(x => x is ReferenceEntry).Cast().Any(x => x.TargetEntry != null && x.TargetEntry.State == EntityState.Modified)) + { + return true; + } + + return false; + } + var entityType = entityEntry.Metadata.ClrType; if (!EntityHelper.IsEntity(entityType) && !EntityHelper.IsValueObject(entityType)) diff --git a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs index c22645e33a..83f7f70cc9 100644 --- a/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs +++ b/framework/src/Volo.Abp.UI.Navigation/Volo/Abp/Ui/Navigation/StandardMenus.cs @@ -2,12 +2,6 @@ namespace Volo.Abp.UI.Navigation; public static class StandardMenus { - /* TODO: Consider to create nested class like - * StandardMenus.Application.Main - * StandardMenus.Application.User - * StandardMenus.Application.Shortcut - */ - public const string Main = "Main"; public const string User = "User"; public const string Shortcut = "Shortcut"; diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json index bcbab76211..a7de87d5ef 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "سيتم حذف {0}!", "ItemWillBeDeletedMessage": "سوف يتم حذف هذا البند!", "ManageYourAccount": "إدارة حسابك", - "OthersGroup": "آخرون" + "OthersGroup": "آخرون", + "Today": "اليوم", + "Apply": "يتقدم" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json index 5dc41952a6..a1f1856c9e 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/cs.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} bude smazáno!", "ItemWillBeDeletedMessage": "Tato položka bude smazána!", "ManageYourAccount": "Spravujte svůj účet", - "OthersGroup": "Jiný" + "OthersGroup": "Jiný", + "Today": "Dnes", + "Apply": "Aplikovat" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json index dc99f75586..22fddc3e27 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/de.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} wird gelöscht!", "ItemWillBeDeletedMessage": "Dieses Element wird gelöscht!", "ManageYourAccount": "Verwalten Sie Ihr Benutzerkonto", - "OthersGroup":"Andere" + "OthersGroup":"Andere", + "Today": "Heute", + "Apply": "Anwenden" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json index 24a7642eb0..8cb7b4c0dc 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/el.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "Το {0} θα διαγραφεί", "ItemWillBeDeletedMessage": "Αυτό το στοιχείο θα διαγραφεί!", "ManageYourAccount": "Διαχείριση Λογαριασμού", - "OthersGroup":"άλλος" + "OthersGroup":"άλλος", + "Today": "Σήμερα", + "Apply": "Ισχύουν" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json index c6ea44f1f3..68ad3dda07 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en-GB.json @@ -50,6 +50,8 @@ "ItemWillBeDeletedMessage": "This item will be deleted!", "ManageYourAccount": "Manage your account", "OthersGroup": "Other", - "NotAssigned": "Not Assigned" + "NotAssigned": "Not Assigned", + "Today": "Today", + "Apply": "Apply" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json index c28e8049bc..abf136d87b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/es.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} serán borrados!", "ItemWillBeDeletedMessage": "Este elemento será borrado", "ManageYourAccount": "Administrar cuenta", - "OthersGroup": "Otra" + "OthersGroup": "Otra", + "Today": "Hoy", + "Apply": "Aplicar" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json index 4fd179573b..bcc6250656 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fa.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} حذف خواهد شد!", "ItemWillBeDeletedMessage": "این مورد حذف خواهد شد!", "ManageYourAccount": "حساب خود را مدیریت کنید", - "OthersGroup": "دیگر" + "OthersGroup": "دیگر", + "Today": "امروز", + "Apply": "درخواست دادن" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json index 25811c92eb..25ab3ee59a 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fi.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} poistetaan!", "ItemWillBeDeletedMessage": "Tämä kohde poistetaan!", "ManageYourAccount": "Hallitse tiliäsi", - "OthersGroup": "Muut" + "OthersGroup": "Muut", + "Today": "Tänään", + "Apply": "Käytä" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json index b850315dfe..6e5c67d2d6 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/fr.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} sera supprimé!", "ItemWillBeDeletedMessage": "Cet objet va être supprimé!", "ManageYourAccount": "Gérer votre compte", - "OthersGroup": "Autre" + "OthersGroup": "Autre", + "Today": "Aujourd'hui", + "Apply": "Appliquer" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json index 0a20f1dcbb..96b73848f5 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hi.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} हटा दिया जाएगा!", "ItemWillBeDeletedMessage": "यह आइटम हटा दिया जाएगा!", "ManageYourAccount": "अपने खाते का प्रबंधन", - "OthersGroup": "अन्य" + "OthersGroup": "अन्य", + "Today": "आज", + "Apply": "आवेदन करना" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json index a6285a4cee..3126f17bbf 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hr.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} zapis će biti obrisan!", "ItemWillBeDeletedMessage": "Ovaj zapis će biti obrisan!", "ManageYourAccount": "Upravljaj korisničkim računom", - "OthersGroup": "Drugi" + "OthersGroup": "Drugi", + "Today": "Danas", + "Apply": "primijeniti" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json index 98c0c46f54..f3e6454b5b 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/hu.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} törlésre kerül!", "ItemWillBeDeletedMessage": "Ez az elem törlődik!", "ManageYourAccount": "Kezelje fiókját", - "OthersGroup": "Egyéb" + "OthersGroup": "Egyéb", + "Today": "Ma", + "Apply": "Alkalmaz" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json index 12b1565c2e..5a9d9eab54 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/is.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} verður eytt!", "ItemWillBeDeletedMessage": "Þessum lið verður eytt!", "ManageYourAccount": "Stillingar notandaaðgangs", - "OthersGroup": "Annað" + "OthersGroup": "Annað", + "Today": "Í dag", + "Apply": "Sækja um" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json index f07bf67e2d..171681980d 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/it.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} sarà eliminato!", "ItemWillBeDeletedMessage": "Questo elemento sarà eliminato!", "ManageYourAccount": "Gestisci il tuo account", - "OthersGroup": "Altra" + "OthersGroup": "Altra", + "Today": "Oggi", + "Apply": "Fare domanda a" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json index 5875fe07b9..42d48d5900 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/nl.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} wordt verwijderd!", "ItemWillBeDeletedMessage": "Dit item wordt verwijderd!", "ManageYourAccount": "Beheer uw account", - "OthersGroup": "Ander" + "OthersGroup": "Ander", + "Today": "Vandaag", + "Apply": "Toepassen" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json index f2b8227137..bd7a6d5818 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pl-PL.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} zostanie usunięty!", "ItemWillBeDeletedMessage": "Ten element zostanie usunięty!", "ManageYourAccount": "Zarządzaj kontem", - "OthersGroup": "Inny" + "OthersGroup": "Inny", + "Today": "Dzisiaj", + "Apply": "Stosować" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json index 928c9299fb..a205b9442a 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/pt-BR.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} será excluído!", "ItemWillBeDeletedMessage": "Este item será excluído!", "ManageYourAccount": "Gerenciar sua conta", - "OthersGroup": "Outra" + "OthersGroup": "Outra", + "Today": "Hoje", + "Apply": "Aplicar" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json index 544b893c39..0edf60e833 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ro-RO.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} va fi şters!", "ItemWillBeDeletedMessage": "Acest articol va fi şters!", "ManageYourAccount": "Administraţi-vă contul", - "OthersGroup": "Alte" + "OthersGroup": "Alte", + "Today": "Astăzi", + "Apply": "aplica" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json index 66f24ca65d..53104c18f4 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ru.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} будет удален!", "ItemWillBeDeletedMessage": "Этот предмет будет удален!", "ManageYourAccount": "Настройте свой аккаунт", - "OthersGroup": "Другой" + "OthersGroup": "Другой", + "Today": "Сегодня", + "Apply": "Применять" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json index 28278f04f2..313ce021de 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sk.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} sa vymaže!", "ItemWillBeDeletedMessage": "Táto položka bude vymazaná!", "ManageYourAccount": "Spravovať svoje konto", - "OthersGroup": "Iné" + "OthersGroup": "Iné", + "Today": "Dnes", + "Apply": "Použiť" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json index c05c1dc202..e7ff5ddc66 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/sl.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} bo izbrisan!", "ItemWillBeDeletedMessage": "Ta element bo izbrisan!", "ManageYourAccount": "Upravljajte svoj račun", - "OthersGroup": "Ostalo" + "OthersGroup": "Ostalo", + "Today": "Danes", + "Apply": "Prijavite se" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json index 1b8bc5fb5b..581964a9b8 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/vi.json @@ -49,6 +49,8 @@ "ItemWillBeDeletedMessageWithFormat": "{0} sẽ bị xóa!", "ItemWillBeDeletedMessage": "Vật phẩm này sẽ bị xoá!", "ManageYourAccount": "Quản lý tài khoản của bạn", - "OthersGroup": "Khác" + "OthersGroup": "Khác", + "Today": "Hôm nay", + "Apply": "Áp dụng" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json index bcf68dbcaa..51d5865924 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hans.json @@ -51,6 +51,8 @@ "ManageYourAccount": "管理你的账户", "OthersGroup": "其他", "CopiedToTheClipboard": "已复制到剪贴板", - "NotAssigned": "未分配" + "NotAssigned": "未分配", + "Today": "今天", + "Apply": "应用" } } diff --git a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json index 1c2719b954..dac062d1cd 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/zh-Hant.json @@ -51,6 +51,8 @@ "ManageYourAccount": "管理個人帳號", "OthersGroup": "其他", "CopiedToTheClipboard": "已复制到剪贴板", - "NotAssigned": "未分配" + "NotAssigned": "未分配", + "Today": "今天", + "Apply": "應用" } } diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs new file mode 100644 index 0000000000..e68d9d94b8 --- /dev/null +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/Entities/AppEntityWithNavigations.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.Auditing.App.Entities; + +[Audited] +public class AppEntityWithNavigations : AggregateRoot +{ + protected AppEntityWithNavigations() + { + + } + + public AppEntityWithNavigations(Guid id, string name) + : base(id) + { + Name = name; + FullName = name; + } + + public string Name { get; set; } + + public string FullName { get; set; } + + public virtual AppEntityWithNavigationChildOneToOne OneToOne { get; set; } + + public virtual List OneToMany { get; set; } + + public virtual List ManyToMany { get; set; } +} + +[Audited] +public class AppEntityWithNavigationChildOneToOne : Entity +{ + public string ChildName { get; set; } +} + +[Audited] +public class AppEntityWithNavigationChildOneToMany : Entity +{ + public Guid AppEntityWithNavigationId { get; set; } + + public string ChildName { get; set; } +} + +[Audited] +public class AppEntityWithNavigationChildManyToMany : Entity +{ + public string ChildName { get; set; } +} diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs index 8a0a798598..f6d4bf49ef 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/App/EntityFrameworkCore/AbpAuditingTestDbContext.cs @@ -27,6 +27,8 @@ public class AbpAuditingTestDbContext : AbpDbContext public DbSet AppEntityWithValueObject { get; set; } + public DbSet AppEntityWithNavigations { get; set; } + public AbpAuditingTestDbContext(DbContextOptions options) : base(options) { @@ -42,5 +44,14 @@ public class AbpAuditingTestDbContext : AbpDbContext b.ConfigureByConvention(); b.OwnsOne(v => v.AppEntityWithValueObjectAddress); }); + + modelBuilder.Entity(b => + { + b.ConfigureByConvention(); + b.HasOne(x => x.OneToOne).WithOne().HasForeignKey(x => x.Id); + b.HasMany(x => x.OneToMany).WithOne().HasForeignKey(x => x.AppEntityWithNavigationId); + b.HasMany(x => x.ManyToMany).WithMany(); + }); + } } diff --git a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs index 3aa0da5a9b..7154fd805c 100644 --- a/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs +++ b/framework/test/Volo.Abp.Auditing.Tests/Volo/Abp/Auditing/Auditing_Tests.cs @@ -411,14 +411,172 @@ public class Auditing_Tests : AbpAuditingTestBase #pragma warning disable 4014 AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && x.EntityChanges[0].ChangeType == EntityChangeType.Updated && - x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"" && + + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName && + x.EntityChanges[1].PropertyChanges.Count == 1 && + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObject.AppEntityWithValueObjectAddress))); + +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.AppEntityWithValueObjectAddress = null; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"England\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"Germany\"" && + + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObject).FullName)); + +#pragma warning restore 4014 + } + + [Fact] + public virtual async Task Should_Write_AuditLog_For_Navigations_Changes() + { + var entityId = Guid.NewGuid(); + var repository = ServiceProvider.GetRequiredService>(); + await repository.InsertAsync(new AppEntityWithNavigations(entityId, "test name")); + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.FullName = "test full name"; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 1 && + x.EntityChanges[0].ChangeType == EntityChangeType.Updated && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[0].PropertyChanges.Count == 1 && + x.EntityChanges[0].PropertyChanges[0].OriginalValue == "\"test name\"" && + x.EntityChanges[0].PropertyChanges[0].NewValue == "\"test full name\"" && + x.EntityChanges[0].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.FullName) && + x.EntityChanges[0].PropertyChanges[0].PropertyTypeFullName == typeof(string).FullName)); +#pragma warning restore 4014 + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName" + }; + + await repository.UpdateAsync(entity); + + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[1].PropertyChanges.Count == 1 && + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToOne) && + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(AppEntityWithNavigationChildOneToOne).FullName)); +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1" + } + }; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildOneToMany).FullName && + x.EntityChanges[1].ChangeType == EntityChangeType.Updated && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && + x.EntityChanges[1].PropertyChanges.Count == 1 && + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.OneToMany) && + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); + +#pragma warning restore 4014 + + using (var scope = _auditingManager.BeginScope()) + { + using (var uow = _unitOfWorkManager.Begin()) + { + var entity = await repository.GetAsync(entityId); + + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + + await repository.UpdateAsync(entity); + await uow.CompleteAsync(); + await scope.SaveAsync(); + } + } + +#pragma warning disable 4014 + AuditingStore.Received().SaveAsync(Arg.Is(x => x.EntityChanges.Count == 2 && + x.EntityChanges[0].ChangeType == EntityChangeType.Created && + x.EntityChanges[0].EntityTypeFullName == typeof(AppEntityWithNavigationChildManyToMany).FullName && x.EntityChanges[1].ChangeType == EntityChangeType.Updated && - x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithValueObjectAddress).FullName && + x.EntityChanges[1].EntityTypeFullName == typeof(AppEntityWithNavigations).FullName && x.EntityChanges[1].PropertyChanges.Count == 1 && - x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithValueObjectAddress.Country) && - x.EntityChanges[1].PropertyChanges[0].OriginalValue == "\"England\"" && - x.EntityChanges[1].PropertyChanges[0].NewValue == "\"Germany\"")); + x.EntityChanges[1].PropertyChanges[0].PropertyName == nameof(AppEntityWithNavigations.ManyToMany) && + x.EntityChanges[1].PropertyChanges[0].PropertyTypeFullName == typeof(List).FullName)); #pragma warning restore 4014 } diff --git a/latest-versions.json b/latest-versions.json index 7823e6af9d..e2911011e7 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,6 +1,6 @@ [ { - "version": "7.4.0", + "version": "7.4.1", "releaseDate": "", "type": "stable", "message": "" diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs index ab5e2819cb..d4bb0dbc5e 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -242,14 +242,16 @@ public class LoginModel : AccountPageModel var user = await UserManager.FindByEmailAsync(email); if (user == null) { - user = await CreateExternalUserAsync(loginInfo); + return RedirectToPage("./Register", new { + IsExternalLogin = true, + ExternalLoginAuthSchema = loginInfo.LoginProvider, + ReturnUrl = returnUrl + }); } - else + + if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null) { - if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null) - { - CheckIdentityErrors(await UserManager.AddLoginAsync(user, loginInfo)); - } + CheckIdentityErrors(await UserManager.AddLoginAsync(user, loginInfo)); } await SignInManager.SignInAsync(user, false); @@ -264,35 +266,6 @@ public class LoginModel : AccountPageModel return RedirectSafely(returnUrl, returnUrlHash); } - protected virtual async Task CreateExternalUserAsync(ExternalLoginInfo info) - { - await IdentityOptions.SetAsync(); - - var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email) ?? info.Principal.FindFirstValue(ClaimTypes.Email); - var userName = await GetUserNameFromEmail(emailAddress); - - var user = new IdentityUser(GuidGenerator.Create(), userName, emailAddress, CurrentTenant.Id); - - CheckIdentityErrors(await UserManager.CreateAsync(user)); - CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress)); - CheckIdentityErrors(await UserManager.AddLoginAsync(user, info)); - CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user)); - - user.Name = info.Principal.FindFirstValue(AbpClaimTypes.Name); - user.Surname = info.Principal.FindFirstValue(AbpClaimTypes.SurName); - - var phoneNumber = info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumber); - if (!phoneNumber.IsNullOrWhiteSpace()) - { - var phoneNumberConfirmed = string.Equals(info.Principal.FindFirstValue(AbpClaimTypes.PhoneNumberVerified), "true", StringComparison.InvariantCultureIgnoreCase); - user.SetPhoneNumber(phoneNumber, phoneNumberConfirmed); - } - - await UserManager.UpdateAsync(user); - - return user; - } - protected virtual async Task ReplaceEmailToUsernameOfInputIfNeeds() { if (!ValidationHelper.IsValidEmailAddress(LoginInput.UserNameOrEmailAddress)) diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml index fabe41ae6f..745368402c 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml @@ -12,7 +12,7 @@ @L["Login"]
- @if ((!Model.IsExternalLogin || Model.UserNameExtracted) && Model.EnableLocalRegister) + @if (Model.EnableLocalRegister || Model.IsExternalLogin) { } diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs index 722eca26b7..322a17e82f 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs @@ -37,7 +37,6 @@ public class RegisterModel : AccountPageModel [BindProperty(SupportsGet = true)] public string ExternalLoginAuthSchema { get; set; } - public bool UserNameExtracted { get; set; } public IEnumerable ExternalProviders { get; set; } public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName)); public bool EnableLocalRegister { get; set; } @@ -128,7 +127,6 @@ public class RegisterModel : AccountPageModel { Input.UserName = await GetUserNameFromEmail(Input.EmailAddress); } - UserNameExtracted = true; await RegisterExternalUserAsync(externalLoginInfo, Input.UserName, Input.EmailAddress); } else diff --git a/npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts b/npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts index 65d063f82d..f43c114c8b 100644 --- a/npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/http-error-reporter.service.ts @@ -19,8 +19,8 @@ export class HttpErrorReporterService { return this._errors$.value; } - reportError = (error: HttpErrorResponse) => { + reportError(error: HttpErrorResponse) { this._reporter$.next(error); this._errors$.next([...this.errors, error]); - }; + } } diff --git a/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts index 372f3434f3..8b2a99476a 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/permission.guard.spec.ts @@ -1,15 +1,26 @@ import { APP_BASE_HREF } from '@angular/common'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest'; +import { provideRouter, Route, Router, RouterModule } from '@angular/router'; +import { + createServiceFactory, + createSpyObject, + SpectatorService, + SpyObject, +} from '@ngneat/spectator/jest'; import { of } from 'rxjs'; -import { PermissionGuard } from '../guards/permission.guard'; +import { permissionGuard, PermissionGuard } from '../guards/permission.guard'; import { HttpErrorReporterService } from '../services/http-error-reporter.service'; import { PermissionService } from '../services/permission.service'; import { RoutesService } from '../services/routes.service'; import { CORE_OPTIONS } from '../tokens/options.token'; import { IncludeLocalizationResourcesProvider } from '../providers'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { OTHERS_GROUP } from '../tokens'; +import { SORT_COMPARE_FUNC, compareFuncFactory } from '../tokens/compare-func.token'; +import { CoreModule } from '../core.module'; +import { AuthService } from '../abstracts'; describe('PermissionGuard', () => { let spectator: SpectatorService; @@ -18,6 +29,10 @@ describe('PermissionGuard', () => { let httpErrorReporter: SpyObject; let permissionService: SpyObject; + const mockOAuthService = { + isAuthenticated: true, + }; + @Component({ template: '' }) class DummyComponent {} @@ -27,25 +42,25 @@ describe('PermissionGuard', () => { declarations: [DummyComponent], imports: [ HttpClientTestingModule, - RouterModule.forRoot( - [ - { - path: 'test', - component: DummyComponent, - data: { - requiredPolicy: 'TestPolicy', - }, + RouterModule.forRoot([ + { + path: 'test', + component: DummyComponent, + data: { + requiredPolicy: 'TestPolicy', }, - ], - {}, - ), + }, + ]), ], providers: [ { provide: APP_BASE_HREF, useValue: '/', }, + { provide: AuthService, useValue: mockOAuthService }, { provide: CORE_OPTIONS, useValue: { skipGetAppConfiguration: true } }, + { provide: OTHERS_GROUP, useValue: 'AbpUi::OthersGroup' }, + { provide: SORT_COMPARE_FUNC, useValue: compareFuncFactory }, IncludeLocalizationResourcesProvider, ], }); @@ -108,3 +123,77 @@ describe('PermissionGuard', () => { }); }); }); + +@Component({ standalone: true, template: '' }) +class DummyComponent {} +describe('authGuard', () => { + let permissionService: SpyObject; + let httpErrorReporter: SpyObject; + + const mockOAuthService = { + isAuthenticated: true, + }; + + const routes: Route[] = [ + { + path: 'dummy', + component: DummyComponent, + canActivate: [permissionGuard], + data: { + requiredPolicy: 'TestPolicy', + }, + }, + { + path: 'zibzib', + component: DummyComponent, + canActivate: [permissionGuard], + }, + ]; + + beforeEach(() => { + httpErrorReporter = createSpyObject(HttpErrorReporterService); + permissionService = createSpyObject(PermissionService); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, CoreModule.forRoot()], + providers: [ + { provide: AuthService, useValue: mockOAuthService }, + { provide: PermissionService, useValue: permissionService }, + { provide: HttpErrorReporterService, useValue: httpErrorReporter }, + provideRouter(routes), + ], + }); + }); + + it('should return true when the grantedPolicy is true', async () => { + permissionService.getGrantedPolicy$.andReturn(of(true)); + await RouterTestingHarness.create('/dummy'); + + expect(TestBed.inject(Router).url).toEqual('/dummy'); + expect(httpErrorReporter.reportError).not.toHaveBeenCalled(); + }); + + it('should return false and report an error when the grantedPolicy is false', async () => { + permissionService.getGrantedPolicy$.andReturn(of(false)); + await RouterTestingHarness.create('/dummy'); + + expect(TestBed.inject(Router).url).not.toEqual('/dummy'); + expect(httpErrorReporter.reportError).toHaveBeenCalled(); + expect(httpErrorReporter.reportError).toBeCalledWith({ status: 403 }); + }); + + it('should check the requiredPolicy from RoutesService', async () => { + permissionService.getGrantedPolicy$.mockImplementation(policy => { + return of(policy === 'TestPolicy'); + }); + await RouterTestingHarness.create('/dummy'); + + expect(TestBed.inject(Router).url).toEqual('/dummy'); + expect(httpErrorReporter.reportError).not.toHaveBeenCalled(); + }); + + it('should return Observable if RoutesService does not have requiredPolicy for given URL', async () => { + await RouterTestingHarness.create('/zibzib'); + expect(TestBed.inject(Router).url).toEqual('/zibzib'); + }); +}); diff --git a/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts b/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts index feca76666e..e8a0f2dbf0 100644 --- a/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts +++ b/npm/ng-packs/packages/oauth/src/lib/tests/auth.guard.spec.ts @@ -1,14 +1,25 @@ import { createServiceFactory, SpectatorService, createSpyObject } from '@ngneat/spectator/jest'; import { OAuthService } from 'angular-oauth2-oidc'; -import { AbpOAuthGuard } from '../guards/oauth.guard'; +import { AbpOAuthGuard, abpOAuthGuard } from '../guards/oauth.guard'; import { AuthService } from '@abp/ng.core'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { + ActivatedRouteSnapshot, + Route, + Router, + RouterStateSnapshot, + provideRouter, +} from '@angular/router'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { SpyObject } from '@ngneat/spectator'; describe('AuthGuard', () => { let spectator: SpectatorService; - let guard : AbpOAuthGuard; - const route = createSpyObject(ActivatedRouteSnapshot) - const state = createSpyObject(RouterStateSnapshot) + let guard: AbpOAuthGuard; + const route = createSpyObject(ActivatedRouteSnapshot); + const state = createSpyObject(RouterStateSnapshot); const createService = createServiceFactory({ service: AbpOAuthGuard, @@ -34,3 +45,47 @@ describe('AuthGuard', () => { expect(navigateToLoginSpy).toHaveBeenCalled(); }); }); + +@Component({ standalone: true, template: '' }) +class DummyComponent {} +describe('authGuard', () => { + let oAuthService: SpyObject; + let authService: SpyObject; + const routes: Route[] = [ + { + path: 'dummy', + canActivate: [abpOAuthGuard], + component: DummyComponent, + }, + ]; + + beforeEach(() => { + authService = createSpyObject(AuthService); + oAuthService = createSpyObject(OAuthService); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { provide: AuthService, useValue: authService }, + { provide: OAuthService, useValue: oAuthService }, + provideRouter(routes), + ], + }); + }); + + it('should move to the dummy route', async () => { + oAuthService.hasValidAccessToken.andReturn(true); + await RouterTestingHarness.create('/dummy'); + + expect(TestBed.inject(Router).url).toEqual('/dummy'); + }); + + it("should'nt move to the dummy route", async () => { + oAuthService.hasValidAccessToken.andReturn(false); + await RouterTestingHarness.create('/dummy'); + + expect(authService.navigateToLogin).toHaveBeenCalled(); + expect(TestBed.inject(Router).url).not.toEqual('/dummy'); + expect(TestBed.inject(Router).url).toEqual('/'); + }); +}); diff --git a/npm/ng-packs/packages/schematics/src/index.ts b/npm/ng-packs/packages/schematics/src/index.ts index cb0ff5c3b5..64f4e4cb7a 100644 --- a/npm/ng-packs/packages/schematics/src/index.ts +++ b/npm/ng-packs/packages/schematics/src/index.ts @@ -1 +1,3 @@ -export {}; +export * from './enums'; +export * from './models'; +export * from './constants'; diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html index 2d7105ae52..0d3aa9dd16 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/components/extensible-table/extensible-table.component.html @@ -24,23 +24,24 @@ - - - {{column.name}} - - - {{column.name}} + + + {{ column.name }} + + + {{ column.name }} + - diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts index e376fe44df..e86e3ae34e 100644 --- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts +++ b/npm/ng-packs/packages/theme-shared/extensions/src/lib/models/entity-props.ts @@ -1,5 +1,5 @@ import { ABP, escapeHtmlChars } from '@abp/ng.core'; -import { Type } from '@angular/core'; +import { InjectFlags, InjectOptions, InjectionToken, Type } from '@angular/core'; import { Observable, of } from 'rxjs'; import { O } from 'ts-toolbelt'; import { ActionCallback } from './actions'; @@ -31,6 +31,7 @@ export class EntityProp extends Prop { readonly component?: Type; readonly enumList?: Array>; readonly tooltip?: string; + readonly columnVisible: ColumnPredicate; constructor(options: EntityPropOptions) { super( @@ -41,7 +42,8 @@ export class EntityProp extends Prop { options.visible, options.isExtra, ); - + + this.columnVisible = options.columnVisible || (() => true); this.columnWidth = options.columnWidth; this.sortable = options.sortable || false; this.valueResolver = @@ -72,6 +74,7 @@ export type EntityPropOptions = O.Optional< O.Writable>, | 'permission' | 'visible' + | 'columnVisible' | 'displayName' | 'isExtra' | 'columnWidth' @@ -85,4 +88,10 @@ export type EntityPropOptions = O.Optional< export type EntityPropDefaults = Record[]>; export type EntityPropContributorCallback = PropContributorCallback>; export type EntityPropContributorCallbacks = PropContributorCallbacks>; +export type ColumnPredicate = (getInjected: GetInjected, auxData?: any) => boolean; +export type GetInjected = ( + token: Type | InjectionToken, + notFoundValue?: T, + options?: InjectOptions | InjectFlags, +) => T; type PropDataObject = { [key: string]: any };