mirror of https://github.com/abpframework/abp.git
108 changed files with 1426 additions and 340 deletions
@ -0,0 +1,236 @@ |
|||||
|
# Moving Background Job Execution To A Separate Application |
||||
|
|
||||
|
In this article, I will show you how to move the background job execution to a separate application. |
||||
|
|
||||
|
Here are some benefits of doing this: |
||||
|
|
||||
|
* If your background jobs consume high system resources (CPU, RAM or Disk), then you can deploy that background application to a dedicated server so it won't affect your application's performance. |
||||
|
* You can scale your background job application independently from your web application. For example, you can deploy multiple instances of your background job application to a Kubernetes cluster and scale it easily. |
||||
|
|
||||
|
Here are some disadvantages of doing this: |
||||
|
|
||||
|
* You need to deploy and maintain at least two applications instead of one. |
||||
|
* You need to implement a mechanism to share the common code between your applications. For example, you can create a shared project and add it to your applications as a project reference. |
||||
|
|
||||
|
## Source code |
||||
|
|
||||
|
You can find the source code of the application at [abpframework/abp-samples](https://github.com/abpframework/abp-samples/tree/master/SeparateBackgroundJob). |
||||
|
|
||||
|
You can check the PR to see the changes step by step: [abpframework/abp-samples#250](https://github.com/abpframework/abp-samples/pull/250) |
||||
|
|
||||
|
## Creating the Web Application |
||||
|
|
||||
|
First, we need to create a new web application using the ABP CLI: |
||||
|
|
||||
|
```bash |
||||
|
abp new SeparateBackgroundJob -t app |
||||
|
``` |
||||
|
|
||||
|
* Create a shared project named `SeparateBackgroundJob.Common.Shared` to share the `BackgroundJob` and `BackgroundJobArgs` classes between the web and job executor applications. |
||||
|
* Install the `Volo.Abp.BackgroundJobs.Abstractions` package to the `SeparateBackgroundJob.Common.Shared` project. |
||||
|
|
||||
|
Add the `SeparateBackgroundJobCommonSharedModule` class to the `SeparateBackgroundJob.Common.Shared` project: |
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn(typeof(AbpBackgroundJobsAbstractionsModule))] |
||||
|
public class SeparateBackgroundJobCommonSharedModule : AbpModule |
||||
|
{ |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Add the `MyReportJob` and `MyReportJobArgs` classes to the `SeparateBackgroundJob.Common.Shared` project: |
||||
|
|
||||
|
```csharp |
||||
|
public class MyReportJob : AsyncBackgroundJob<MyReportJobArgs>, ITransientDependency |
||||
|
{ |
||||
|
public override Task ExecuteAsync(MyReportJobArgs args) |
||||
|
{ |
||||
|
Logger.LogInformation("Executing MyReportJob with args: {0}", args.Content); |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class MyReportJobArgs |
||||
|
{ |
||||
|
public string? Content { get; set; } |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Add the `SeparateBackgroundJob.Common.Shared` project reference to the `SeparateBackgroundJob.Domain` project and add `SeparateBackgroundJobCommonSharedModule` to the `DependsOn` attribute of the `SeparateBackgroundJobDomainModule` class: |
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn( |
||||
|
typeof(SeparateBackgroundJobDomainSharedModule), |
||||
|
typeof(AbpAuditLoggingDomainModule), |
||||
|
typeof(AbpBackgroundJobsDomainModule), |
||||
|
typeof(AbpFeatureManagementDomainModule), |
||||
|
typeof(AbpIdentityDomainModule), |
||||
|
typeof(AbpOpenIddictDomainModule), |
||||
|
typeof(AbpPermissionManagementDomainOpenIddictModule), |
||||
|
typeof(AbpPermissionManagementDomainIdentityModule), |
||||
|
typeof(AbpSettingManagementDomainModule), |
||||
|
typeof(AbpTenantManagementDomainModule), |
||||
|
typeof(AbpEmailingModule), |
||||
|
typeof(SeparateBackgroundJobCommonSharedModule) //Add this line |
||||
|
)] |
||||
|
public class SeparateBackgroundJobDomainModule : AbpModule |
||||
|
``` |
||||
|
|
||||
|
Open the `Index.cshtml` and replace the content with the following code: |
||||
|
|
||||
|
```csharp |
||||
|
@page |
||||
|
@using Microsoft.AspNetCore.Mvc.Localization |
||||
|
@using SeparateBackgroundJob.Localization |
||||
|
@using Volo.Abp.Users |
||||
|
@model SeparateBackgroundJob.Web.Pages.IndexModel |
||||
|
@inject IHtmlLocalizer<SeparateBackgroundJobResource> L |
||||
|
@inject ICurrentUser CurrentUser |
||||
|
|
||||
|
@section styles { |
||||
|
<abp-style src="/Pages/Index.css"/> |
||||
|
} |
||||
|
|
||||
|
@section scripts { |
||||
|
<abp-script src="/Pages/Index.js"/> |
||||
|
} |
||||
|
|
||||
|
<div class="container"> |
||||
|
<abp-card> |
||||
|
<abp-card-header> |
||||
|
<abp-card-title> |
||||
|
Add NEW BACKGROUND JOB |
||||
|
</abp-card-title> |
||||
|
</abp-card-header> |
||||
|
<abp-card-body> |
||||
|
<form id="NewItemForm" method="post" class="row row-cols-lg-auto g-3 align-items-center"> |
||||
|
<div class="col-12"> |
||||
|
<div class="input-group"> |
||||
|
<input id="ReportContent" required name="ReportContent" type="text" class="form-control" placeholder="enter text..."> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-12"> |
||||
|
<button type="submit" class="btn btn-primary">Add</button> |
||||
|
</div> |
||||
|
</form> |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
</div> |
||||
|
``` |
||||
|
|
||||
|
Open the `Index.cshtml.cs` and replace the content with the following code: |
||||
|
|
||||
|
```csharp |
||||
|
public class IndexModel : SeparateBackgroundJobPageModel |
||||
|
{ |
||||
|
private readonly IBackgroundJobManager _backgroundJobManager; |
||||
|
|
||||
|
[BindProperty(SupportsGet = true)] |
||||
|
public string? ReportContent { get; set; } |
||||
|
|
||||
|
public IndexModel(IBackgroundJobManager backgroundJobManager) |
||||
|
{ |
||||
|
_backgroundJobManager = backgroundJobManager; |
||||
|
} |
||||
|
|
||||
|
public void OnGet() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public async Task OnPostAsync() |
||||
|
{ |
||||
|
await _backgroundJobManager.EnqueueAsync(new MyReportJobArgs |
||||
|
{ |
||||
|
Content = ReportContent |
||||
|
}); |
||||
|
|
||||
|
Alerts.Success("Job is queued!"); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Run the application and navigate to the home page. You should see the following page: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
When you enter some text and click the **Add** button, the job will be queued and executed in the web application: |
||||
|
|
||||
|
## Creating the Console Application |
||||
|
|
||||
|
Now we split the background job execution to a separate console application. |
||||
|
|
||||
|
Open the `SeparateBackgroundJobWebModule` class to disable the background job execution in the web application: |
||||
|
|
||||
|
```csharp |
||||
|
public class SeparateBackgroundJobWebModule : AbpModule |
||||
|
{ |
||||
|
.... |
||||
|
|
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
... |
||||
|
|
||||
|
//Disable background job execution in the web application |
||||
|
Configure<AbpBackgroundJobOptions>(options => |
||||
|
{ |
||||
|
options.IsJobExecutionEnabled = false; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
* Create a new console application using the ABP CLI: |
||||
|
|
||||
|
```bash |
||||
|
abp new BackgroundJobExecutor -t console |
||||
|
``` |
||||
|
|
||||
|
* Add the `BackgroundJobExecutor` project to the solution of the web application. |
||||
|
* Add the `SeparateBackgroundJob.Common.Shared` project reference to the `BackgroundJobExecutor` project. |
||||
|
* Install the `Volo.Abp.BackgroundJobs.EntityFrameworkCore` and `Volo.Abp.EntityFrameworkCore.SqlServer` packages to the `BackgroundJobExecutor` project. |
||||
|
|
||||
|
Update the `BackgroundJobExecutorModule` class as follows: |
||||
|
|
||||
|
```csharp |
||||
|
[DependsOn( |
||||
|
typeof(AbpAutofacModule), |
||||
|
typeof(AbpBackgroundJobsEntityFrameworkCoreModule), |
||||
|
typeof(AbpEntityFrameworkCoreSqlServerModule), |
||||
|
typeof(SeparateBackgroundJobCommonSharedModule) |
||||
|
)] |
||||
|
public class BackgroundJobExecutorModule : AbpModule |
||||
|
{ |
||||
|
public override void ConfigureServices(ServiceConfigurationContext context) |
||||
|
{ |
||||
|
Configure<AbpDbContextOptions>(options => |
||||
|
{ |
||||
|
options.UseSqlServer(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
.... |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Open the `appsettings.json` file to configure the [connection string](https://docs.abp.io/en/abp/latest/Connection-Strings#configure-the-connection-strings): |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"ConnectionStrings": { |
||||
|
"AbpBackgroundJobs": "Server=(LocalDb)\\MSSQLLocalDB;Database=SeparateBackgroundJob;Trusted_Connection=True" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
> You must use the same connection string for the web application, `AbpBackgroundJobs` is the default connection string name for the background job module. |
||||
|
|
||||
|
The solution structure should look like this: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Now, run the web and console application. When you enter some text and click the **Add** button, the job will be queued and executed in the console application: |
||||
|
|
||||
|
 |
||||
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 102 KiB |
@ -0,0 +1,12 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Volo.Abp.AspNetCore.Mvc.UI.Bundling; |
||||
|
|
||||
|
namespace Volo.Abp.AspNetCore.Mvc.UI.Packages.Zxcvbn; |
||||
|
|
||||
|
public class ZxcvbnScriptContributor : BundleContributor |
||||
|
{ |
||||
|
public override void ConfigureBundle(BundleConfigurationContext context) |
||||
|
{ |
||||
|
context.Files.AddIfNotContains("/libs/zxcvbn/zxcvbn.js"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using Volo.Abp.Cli.ProjectBuilding.Building; |
||||
|
|
||||
|
namespace Volo.Abp.Cli.ProjectBuilding.Templates; |
||||
|
|
||||
|
public class CheckRedisPreRequirements : ProjectBuildPipelineStep |
||||
|
{ |
||||
|
public override void Execute(ProjectBuildContext context) |
||||
|
{ |
||||
|
var modules = context.Files.Where(f => f.Name.EndsWith("Module.cs", StringComparison.OrdinalIgnoreCase)); |
||||
|
if (modules.Any(module => module.Content.Contains("Redis:Configuration"))) |
||||
|
{ |
||||
|
context.BuildArgs.ExtraProperties["PreRequirements:Redis"] = "true"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,228 +1,229 @@ |
|||||
{ |
{ |
||||
"culture": "en", |
"culture": "en", |
||||
"texts": { |
"texts": { |
||||
"AddSubMenuItem": "Add Sub Menu Item", |
"AddSubMenuItem": "Add Sub Menu Item", |
||||
"AreYouSure": "Are You Sure?", |
"AreYouSure": "Are You Sure?", |
||||
"BlogDeletionConfirmationMessage": "The blog '{0}' will be deleted. Are you sure?", |
"BlogDeletionConfirmationMessage": "The blog '{0}' will be deleted. Are you sure?", |
||||
"BlogFeatureNotAvailable": "This feature is not available now. Enable with 'GlobalFeatureManager' to use it.", |
"BlogFeatureNotAvailable": "This feature is not available now. Enable with 'GlobalFeatureManager' to use it.", |
||||
"BlogId": "Blog", |
"BlogId": "Blog", |
||||
"BlogPostDeletionConfirmationMessage": "The blog post '{0}' will be deleted. Are you sure?", |
"BlogPostDeletionConfirmationMessage": "The blog post '{0}' will be deleted. Are you sure?", |
||||
"BlogPosts": "Blog Posts", |
"BlogPosts": "Blog Posts", |
||||
"Blogs": "Blogs", |
"Blogs": "Blogs", |
||||
"ChoosePreference": "Choose Preference...", |
"ChoosePreference": "Choose Preference...", |
||||
"Cms": "CMS", |
"Cms": "CMS", |
||||
"CmsKit.Comments": "Comments", |
"CmsKit.Comments": "Comments", |
||||
"CmsKit.Ratings": "Ratings", |
"CmsKit.Ratings": "Ratings", |
||||
"CmsKit.Reactions": "Reactions", |
"CmsKit.Reactions": "Reactions", |
||||
"CmsKit.Tags": "Tags", |
"CmsKit.Tags": "Tags", |
||||
"CmsKit:0002": "Content already exists!", |
"CmsKit:0002": "Content already exists!", |
||||
"CmsKit:0003": "The entity {0} is not taggable.", |
"CmsKit:0003": "The entity {0} is not taggable.", |
||||
"CmsKit:Blog:0001": "The given slug ({Slug}) already exists!", |
"CmsKit:Blog:0001": "The given slug ({Slug}) already exists!", |
||||
"CmsKit:BlogPost:0001": "The given slug already exists!", |
"CmsKit:BlogPost:0001": "The given slug already exists!", |
||||
"CmsKit:Comments:0001": "The entity {EntityType} is not commentable.", |
"CmsKit:Comments:0001": "The entity {EntityType} is not commentable.", |
||||
"CmsKit:Media:0001": "'{Name}' is not a valid media name.", |
"CmsKit:Media:0001": "'{Name}' is not a valid media name.", |
||||
"CmsKit:Media:0002": "The entity can't have media.", |
"CmsKit:Media:0002": "The entity can't have media.", |
||||
"CmsKit:Page:0001": "The given url ({Slug}) already exists. Please try with different url.", |
"CmsKit:Page:0001": "The given url ({Slug}) already exists. Please try with different url.", |
||||
"CmsKit:Rating:0001": "The entity {EntityType} can't be rated.", |
"CmsKit:Rating:0001": "The entity {EntityType} can't be rated.", |
||||
"CmsKit:Reaction:0001": "The entity {EntityType} can't have reactions.", |
"CmsKit:Reaction:0001": "The entity {EntityType} can't have reactions.", |
||||
"CmsKit:Tag:0002": "The entity is not taggable!", |
"CmsKit:Tag:0002": "The entity is not taggable!", |
||||
"CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display.", |
"CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display.", |
||||
"CommentDeletionConfirmationMessage": "This comment and all replies will be deleted!", |
"CommentDeletionConfirmationMessage": "This comment and all replies will be deleted!", |
||||
"Comments": "Comments", |
"Comments": "Comments", |
||||
"Content": "Content", |
"Content": "Content", |
||||
"ContentDeletionConfirmationMessage": "Are you sure to delete this content?", |
"ContentDeletionConfirmationMessage": "Are you sure to delete this content?", |
||||
"Contents": "Contents", |
"Contents": "Contents", |
||||
"CoverImage": "Cover Image", |
"CoverImage": "Cover Image", |
||||
"CreateBlogPostPage": "New Blog Post", |
"CreateBlogPostPage": "New Blog Post", |
||||
"CreationTime": "Creation Time", |
"CreationTime": "Creation Time", |
||||
"Delete": "Delete", |
"Delete": "Delete", |
||||
"Detail": "Detail", |
"Detail": "Detail", |
||||
"Details": "Details", |
"Details": "Details", |
||||
"DisplayName": "Display Name", |
"DisplayName": "Display Name", |
||||
"DoYouPreferAdditionalEmails": "Do you prefer additional emails?", |
"DoYouPreferAdditionalEmails": "Do you prefer additional emails?", |
||||
"Edit": "Edit", |
"Edit": "Edit", |
||||
"EndDate": "End Date", |
"EndDate": "End Date", |
||||
"EntityId": "Entity Id", |
"EntityId": "Entity Id", |
||||
"EntityType": "Entity Type", |
"EntityType": "Entity Type", |
||||
"ExportCSV": "Export CSV", |
"ExportCSV": "Export CSV", |
||||
"Features": "Features", |
"Features": "Features", |
||||
"GenericDeletionConfirmationMessage": "Are you sure to delete '{0}'?", |
"GenericDeletionConfirmationMessage": "Are you sure to delete '{0}'?", |
||||
"IsActive": "Active", |
"IsActive": "Active", |
||||
"LastModification": "Last Modification", |
"LastModification": "Last Modification", |
||||
"LastModificationTime": "Last Modification Time", |
"LastModificationTime": "Last Modification Time", |
||||
"LoginToAddComment": "Login to add comment", |
"LoginToAddComment": "Login to add comment", |
||||
"LoginToRate": "Login to rate", |
"LoginToRate": "Login to rate", |
||||
"LoginToReact": "Login to react", |
"LoginToReact": "Login to react", |
||||
"LoginToReply": "Login to reply", |
"LoginToReply": "Login to reply", |
||||
"MainMenu": "Main Menu", |
"MainMenu": "Main Menu", |
||||
"MakeMainMenu": "Make Main Menu", |
"MakeMainMenu": "Make Main Menu", |
||||
"Menu:CMS": "CMS", |
"Menu:CMS": "CMS", |
||||
"Menus": "Menus", |
"Menus": "Menus", |
||||
"MenuDeletionConfirmationMessage": "The menu '{0}' will be deleted. Are you sure?", |
"MenuDeletionConfirmationMessage": "The menu '{0}' will be deleted. Are you sure?", |
||||
"MenuItemDeletionConfirmationMessage": "Are sure to delete this menu item?", |
"MenuItemDeletionConfirmationMessage": "Are sure to delete this menu item?", |
||||
"MenuItemMoveConfirmMessage": "Are you sure you want to move '{0}' under '{1}'?", |
"MenuItemMoveConfirmMessage": "Are you sure you want to move '{0}' under '{1}'?", |
||||
"MenuItems": "Menu Items", |
"MenuItems": "Menu Items", |
||||
"Message": "Message", |
"Message": "Message", |
||||
"MessageDeletionConfirmationMessage": "This comment will be deleted completely.", |
"MessageDeletionConfirmationMessage": "This comment will be deleted completely.", |
||||
"NewBlog": "New Blog", |
"NewBlog": "New Blog", |
||||
"NewBlogPost": "New Blog Post", |
"NewBlogPost": "New Blog Post", |
||||
"NewMenu": "New Menu", |
"NewMenu": "New Menu", |
||||
"NewMenuItem": "New Root Menu Item", |
"NewMenuItem": "New Root Menu Item", |
||||
"NewPage": "New Page", |
"NewPage": "New Page", |
||||
"NewTag": "New Tag", |
"NewTag": "New Tag", |
||||
"NoMenuItems": "There is no menu item yet!", |
"NoMenuItems": "There is no menu item yet!", |
||||
"OK": "OK", |
"OK": "OK", |
||||
"PageDeletionConfirmationMessage": "Are you sure to delete this page?", |
"PageDeletionConfirmationMessage": "Are you sure to delete this page?", |
||||
"PageId": "Page", |
"PageId": "Page", |
||||
"Pages": "Pages", |
"Pages": "Pages", |
||||
"PageSlugInformation": "Slug is used on url. Your url will be '/{{slug}}'.", |
"PageSlugInformation": "Slug is used on url. Your url will be '/{{slug}}'.", |
||||
"Permission:BlogManagement": "Blog Management", |
"Permission:BlogManagement": "Blog Management", |
||||
"Permission:BlogManagement.Create": "Create", |
"Permission:BlogManagement.Create": "Create", |
||||
"Permission:BlogManagement.Delete": "Delete", |
"Permission:BlogManagement.Delete": "Delete", |
||||
"Permission:BlogManagement.Features": "Features", |
"Permission:BlogManagement.Features": "Features", |
||||
"Permission:BlogManagement.Update": "Update", |
"Permission:BlogManagement.Update": "Update", |
||||
"Permission:BlogPostManagement": "Blog Post Management", |
"Permission:BlogPostManagement": "Blog Post Management", |
||||
"Permission:BlogPostManagement.Create": "Create", |
"Permission:BlogPostManagement.Create": "Create", |
||||
"Permission:BlogPostManagement.Delete": "Delete", |
"Permission:BlogPostManagement.Delete": "Delete", |
||||
"Permission:BlogPostManagement.Update": "Update", |
"Permission:BlogPostManagement.Update": "Update", |
||||
"Permission:BlogPostManagement.Publish": "Publish", |
"Permission:BlogPostManagement.Publish": "Publish", |
||||
"Permission:CmsKit": "CmsKit Admin", |
"Permission:CmsKit": "CmsKit Admin", |
||||
"Permission:Comments": "Comment Management", |
"Permission:Comments": "Comment Management", |
||||
"Permission:Comments.Delete": "Delete", |
"Permission:Comments.Delete": "Delete", |
||||
"Permission:Contents": "Content Management", |
"Permission:Contents": "Content Management", |
||||
"Permission:Contents.Create": "Create Content", |
"Permission:Contents.Create": "Create Content", |
||||
"Permission:Contents.Delete": "Delete Content", |
"Permission:Contents.Delete": "Delete Content", |
||||
"Permission:Contents.Update": "Update Content", |
"Permission:Contents.Update": "Update Content", |
||||
"Permission:MediaDescriptorManagement": "Media Management", |
"Permission:MediaDescriptorManagement": "Media Management", |
||||
"Permission:MediaDescriptorManagement:Create": "Create", |
"Permission:MediaDescriptorManagement:Create": "Create", |
||||
"Permission:MediaDescriptorManagement:Delete": "Delete", |
"Permission:MediaDescriptorManagement:Delete": "Delete", |
||||
"Permission:MenuItemManagement": "Menu Item Management", |
"Permission:MenuItemManagement": "Menu Item Management", |
||||
"Permission:MenuItemManagement.Create": "Create", |
"Permission:MenuItemManagement.Create": "Create", |
||||
"Permission:MenuItemManagement.Delete": "Delete", |
"Permission:MenuItemManagement.Delete": "Delete", |
||||
"Permission:MenuItemManagement.Update": "Update", |
"Permission:MenuItemManagement.Update": "Update", |
||||
"Permission:MenuManagement": "Menu Management", |
"Permission:MenuManagement": "Menu Management", |
||||
"Permission:MenuManagement.Create": "Create", |
"Permission:MenuManagement.Create": "Create", |
||||
"Permission:MenuManagement.Delete": "Delete", |
"Permission:MenuManagement.Delete": "Delete", |
||||
"Permission:MenuManagement.Update": "Update", |
"Permission:MenuManagement.Update": "Update", |
||||
"Permission:Menus": "Menu Management", |
"Permission:Menus": "Menu Management", |
||||
"Permission:Menus.Create": "Create", |
"Permission:Menus.Create": "Create", |
||||
"Permission:Menus.Delete": "Delete", |
"Permission:Menus.Delete": "Delete", |
||||
"Permission:Menus.Update": "Update", |
"Permission:Menus.Update": "Update", |
||||
"Permission:PageManagement": "Page Management", |
"Permission:PageManagement": "Page Management", |
||||
"Permission:PageManagement:Create": "Create", |
"Permission:PageManagement:Create": "Create", |
||||
"Permission:PageManagement:Delete": "Delete", |
"Permission:PageManagement:Delete": "Delete", |
||||
"Permission:PageManagement:Update": "Update", |
"Permission:PageManagement:Update": "Update", |
||||
"Permission:PageManagement:SetAsHomePage": "Set As Home Page", |
"Permission:PageManagement:SetAsHomePage": "Set As Home Page", |
||||
"Permission:TagManagement": "Tag Management", |
"Permission:TagManagement": "Tag Management", |
||||
"Permission:TagManagement.Create": "Create", |
"Permission:TagManagement.Create": "Create", |
||||
"Permission:TagManagement.Delete": "Delete", |
"Permission:TagManagement.Delete": "Delete", |
||||
"Permission:TagManagement.Update": "Update", |
"Permission:TagManagement.Update": "Update", |
||||
"Permission:GlobalResources": "Global Resources", |
"Permission:GlobalResources": "Global Resources", |
||||
"Permission:CmsKitPublic": "CmsKit Public", |
"Permission:CmsKitPublic": "CmsKit Public", |
||||
"Permission:Comments.DeleteAll": "Delete All", |
"Permission:Comments.DeleteAll": "Delete All", |
||||
"PickYourReaction": "Pick your reaction", |
"PickYourReaction": "Pick your reaction", |
||||
"Rating": "Rating", |
"Rating": "Rating", |
||||
"RatingUndoMessage": "Your rating will be undo.", |
"RatingUndoMessage": "Your rating will be undo.", |
||||
"Reactions": "Reactions", |
"Reactions": "Reactions", |
||||
"Read": "Read", |
"Read": "Read", |
||||
"RepliesToThisComment": "Replies to this comment", |
"RepliesToThisComment": "Replies to this comment", |
||||
"Reply": "Reply", |
"Reply": "Reply", |
||||
"ReplyTo": "Reply to", |
"ReplyTo": "Reply to", |
||||
"SamplePageMessage": "A sample page for the Pro module", |
"SamplePageMessage": "A sample page for the Pro module", |
||||
"SaveChanges": "Save Changes", |
"SaveChanges": "Save Changes", |
||||
"Script": "Script", |
"Script": "Script", |
||||
"SelectAll": "Select All", |
"SelectAll": "Select All", |
||||
"Send": "Send", |
"Send": "Send", |
||||
"SendMessage": "Send Message", |
"SendMessage": "Send Message", |
||||
"SelectedAuthor": "Author", |
"SelectedAuthor": "Author", |
||||
"ShortDescription": "Short description", |
"ShortDescription": "Short description", |
||||
"Slug": "Slug", |
"Slug": "Slug", |
||||
"Source": "Source", |
"Source": "Source", |
||||
"SourceUrl": "Source Url", |
"SourceUrl": "Source Url", |
||||
"Star": "Star", |
"Star": "Star", |
||||
"StartDate": "Start Date", |
"StartDate": "Start Date", |
||||
"Style": "Style", |
"Style": "Style", |
||||
"Subject": "Subject", |
"Subject": "Subject", |
||||
"SubjectPlaceholder": "Please type a subject", |
"SubjectPlaceholder": "Please type a subject", |
||||
"Submit": "Submit", |
"Submit": "Submit", |
||||
"Subscribe": "Subscribe", |
"Subscribe": "Subscribe", |
||||
"SuccessfullySaved": "Successfully saved!", |
"SuccessfullySaved": "Successfully saved!", |
||||
"TagDeletionConfirmationMessage": "Are you sure to delete '{0}' tag?", |
"TagDeletionConfirmationMessage": "Are you sure to delete '{0}' tag?", |
||||
"Tags": "Tags", |
"Tags": "Tags", |
||||
"Text": "Text", |
"Text": "Text", |
||||
"ThankYou": "Thank you", |
"ThankYou": "Thank you", |
||||
"Title": "Title", |
"Title": "Title", |
||||
"Undo": "Undo", |
"Undo": "Undo", |
||||
"Update": "Update", |
"Update": "Update", |
||||
"UpdatePreferenceSuccessMessage": "Your preferences have been saved.", |
"UpdatePreferenceSuccessMessage": "Your preferences have been saved.", |
||||
"UpdateYourEmailPreferences": "Update your email preferences", |
"UpdateYourEmailPreferences": "Update your email preferences", |
||||
"UnMakeMainMenu": "Unmake Main Menu", |
"UnMakeMainMenu": "Unmake Main Menu", |
||||
"UploadFailedMessage": "Upload failed.", |
"UploadFailedMessage": "Upload failed.", |
||||
"UserId": "User Id", |
"UserId": "User Id", |
||||
"Username": "Username", |
"Username": "Username", |
||||
"YourComment": "Your comment", |
"YourComment": "Your comment", |
||||
"YourEmailAddress": "Your e-mail address", |
"YourEmailAddress": "Your e-mail address", |
||||
"YourFullName": "Your full name", |
"YourFullName": "Your full name", |
||||
"YourMessage": "Your Message", |
"YourMessage": "Your Message", |
||||
"YourReply": "Your reply", |
"YourReply": "Your reply", |
||||
"MarkdownSupported": "<a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown</a> supported.", |
"MarkdownSupported": "<a href=\"https://www.markdownguide.org/basic-syntax/\">Markdown</a> supported.", |
||||
"GlobalResources": "Global Resources", |
"GlobalResources": "Global Resources", |
||||
"SavedSuccessfully": "Saved successfully", |
"SavedSuccessfully": "Saved successfully", |
||||
"CmsKit.BlogPost.Status.0": "Draft", |
"CmsKit.BlogPost.Status.0": "Draft", |
||||
"CmsKit.BlogPost.Status.1": "Published", |
"CmsKit.BlogPost.Status.1": "Published", |
||||
"CmsKit.BlogPost.Status.2": "Waiting for review", |
"CmsKit.BlogPost.Status.2": "Waiting for review", |
||||
"BlogPostPublishConfirmationMessage": "Are you sure to publish the blog post \"{0}\"?", |
"BlogPostPublishConfirmationMessage": "Are you sure to publish the blog post \"{0}\"?", |
||||
"SuccessfullyPublished": "Successfully published!", |
"SuccessfullyPublished": "Successfully published!", |
||||
"Draft": "Draft", |
"Draft": "Draft", |
||||
"Publish": "Publish", |
"Publish": "Publish", |
||||
"BlogPostDraftConfirmationMessage": "Are you sure to set the blog post \"{0}\" as draft?", |
"BlogPostDraftConfirmationMessage": "Are you sure to set the blog post \"{0}\" as draft?", |
||||
"BlogPostSendToReviewConfirmationMessage": "Are you sure to send the blog post \"{0}\" to admin review for publishing?", |
"BlogPostSendToReviewConfirmationMessage": "Are you sure to send the blog post \"{0}\" to admin review for publishing?", |
||||
"SaveAsDraft": "Save as draft", |
"SaveAsDraft": "Save as draft", |
||||
"SendToReview": "Send to review", |
"SendToReview": "Send to review", |
||||
"SendToReviewToPublish": "Send to review to publish", |
"SendToReviewToPublish": "Send to review to publish", |
||||
"BlogPostSendToReviewSuccessMessage": "The blog post \"{0}\" has been sent to admin review for publishing.", |
"BlogPostSendToReviewSuccessMessage": "The blog post \"{0}\" has been sent to admin review for publishing.", |
||||
"HasBlogPostWaitingForReviewMessage": "You have a blog post waiting for review. Click to list.", |
"HasBlogPostWaitingForReviewMessage": "You have a blog post waiting for review. Click to list.", |
||||
"SelectAStatus": "Select a status", |
"SelectAStatus": "Select a status", |
||||
"Status": "Status", |
"Status": "Status", |
||||
"CmsKit.BlogPost.ScrollIndex": "Quick navigation bar in blog posts", |
"CmsKit.BlogPost.ScrollIndex": "Quick navigation bar in blog posts", |
||||
"Add": "Add", |
"CmsKit.BlogPost.PreventXssFeature": "Prevent XSS", |
||||
"AddWidget": "Add Widget", |
"Add": "Add", |
||||
"PleaseConfigureWidgets": "Please configure widgets", |
"AddWidget": "Add Widget", |
||||
"SelectAnAuthor": "Select an Author", |
"PleaseConfigureWidgets": "Please configure widgets", |
||||
"InThisDocument": "In This Document", |
"SelectAnAuthor": "Select an Author", |
||||
"GoToTop": "Go To Top", |
"InThisDocument": "In This Document", |
||||
"SetAsHomePage": "Change Home Page Status", |
"GoToTop": "Go To Top", |
||||
"CompletedSettingAsHomePage": "Set as home page", |
"SetAsHomePage": "Change Home Page Status", |
||||
"IsHomePage": "Is Home Page", |
"CompletedSettingAsHomePage": "Set as home page", |
||||
"RemovedSettingAsHomePage": "Removed setting the home page", |
"IsHomePage": "Is Home Page", |
||||
"Feature:CmsKitGroup": "Cms Kit", |
"RemovedSettingAsHomePage": "Removed setting the home page", |
||||
"Feature:BlogEnable": "Blogpost", |
"Feature:CmsKitGroup": "Cms Kit", |
||||
"Feature:BlogEnableDescription": "CMS Kit's blogpost system that allows create blogs and posts dynamically in the application.", |
"Feature:BlogEnable": "Blogpost", |
||||
"Feature:CommentEnable": "Commenting", |
"Feature:BlogEnableDescription": "CMS Kit's blogpost system that allows create blogs and posts dynamically in the application.", |
||||
"Feature:CommentEnableDescription": "CMS Kit's comment system allows commenting on entities such as BlogPost.", |
"Feature:CommentEnable": "Commenting", |
||||
"Feature:GlobalResourceEnable": "Global resourcing", |
"Feature:CommentEnableDescription": "CMS Kit's comment system allows commenting on entities such as BlogPost.", |
||||
"Feature:GlobalResourceEnableDescription": "CMS Kit's global resoruces feature that allows managing global styles & scripts.", |
"Feature:GlobalResourceEnable": "Global resourcing", |
||||
"Feature:MenuEnable": "Menu", |
"Feature:GlobalResourceEnableDescription": "CMS Kit's global resoruces feature that allows managing global styles & scripts.", |
||||
"Feature:MenuEnableDescription": "CMS Kit's dynamic menu system that allows adding/removing application menus dynamically.", |
"Feature:MenuEnable": "Menu", |
||||
"Feature:PageEnable": "Paging", |
"Feature:MenuEnableDescription": "CMS Kit's dynamic menu system that allows adding/removing application menus dynamically.", |
||||
"Feature:PageEnableDescription": "CMS Kit's page system that allows creating static pages with specific URL.", |
"Feature:PageEnable": "Paging", |
||||
"Feature:RatingEnable": "Rating", |
"Feature:PageEnableDescription": "CMS Kit's page system that allows creating static pages with specific URL.", |
||||
"Feature:RatingEnableDescription": "CMS Kit's rating system that allows users to rate entities such as BlogPost.", |
"Feature:RatingEnable": "Rating", |
||||
"Feature:ReactionEnable": "Reaction", |
"Feature:RatingEnableDescription": "CMS Kit's rating system that allows users to rate entities such as BlogPost.", |
||||
"Feature:ReactionEnableDescription": "CMS Kit's reaction system that allows users to send reactions to entities such as BlogPost, Comments, etc.", |
"Feature:ReactionEnable": "Reaction", |
||||
"Feature:TagEnable": "Taging", |
"Feature:ReactionEnableDescription": "CMS Kit's reaction system that allows users to send reactions to entities such as BlogPost, Comments, etc.", |
||||
"Feature:TagEnableDescription": "CMS Kit's tag system that allows tagging entities such as BlogPost.", |
"Feature:TagEnable": "Taging", |
||||
"DeleteBlogPostMessage": "The blog will be deleted. Are you sure?", |
"Feature:TagEnableDescription": "CMS Kit's tag system that allows tagging entities such as BlogPost.", |
||||
"CaptchaCode": "Captcha code", |
"DeleteBlogPostMessage": "The blog will be deleted. Are you sure?", |
||||
"CommentTextRequired": "Comment is required", |
"CaptchaCode": "Captcha code", |
||||
"CaptchaCodeErrorMessage": "The answer you entered for the CAPTCHA was not correct. Please try again", |
"CommentTextRequired": "Comment is required", |
||||
"CaptchaCodeMissingMessage": "The captcha code is missing!", |
"CaptchaCodeErrorMessage": "The answer you entered for the CAPTCHA was not correct. Please try again", |
||||
"UnAllowedExternalUrlMessage": "You included an unallowed external URL. Please try again without the external URL.", |
"CaptchaCodeMissingMessage": "The captcha code is missing!", |
||||
"URL": "URL", |
"UnAllowedExternalUrlMessage": "You included an unallowed external URL. Please try again without the external URL.", |
||||
"PopularTags": "Popular Tags", |
"URL": "URL", |
||||
"RemoveCoverImageConfirmationMessage": "Are you sure you want to remove the cover image?", |
"PopularTags": "Popular Tags", |
||||
"RemoveCoverImage": "Remove cover image", |
"RemoveCoverImageConfirmationMessage": "Are you sure you want to remove the cover image?", |
||||
"CssClass": "CSS Class", |
"RemoveCoverImage": "Remove cover image", |
||||
"TagsHelpText": "Tags should be comma-separated (e.g.: tag1, tag2, tag3)" |
"CssClass": "CSS Class", |
||||
} |
"TagsHelpText": "Tags should be comma-separated (e.g.: tag1, tag2, tag3)" |
||||
|
} |
||||
} |
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; |
||||
|
import { Tree, readProjectConfiguration } from '@nx/devkit'; |
||||
|
|
||||
|
import { changeThemeGenerator } from './generator'; |
||||
|
import { ChangeThemeGeneratorSchema } from './schema'; |
||||
|
|
||||
|
describe('change-theme generator', () => { |
||||
|
let tree: Tree; |
||||
|
const options: ChangeThemeGeneratorSchema = { name: 'test' }; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
tree = createTreeWithEmptyWorkspace(); |
||||
|
}); |
||||
|
|
||||
|
it('should run successfully', async () => { |
||||
|
await changeThemeGenerator(tree, options); |
||||
|
const config = readProjectConfiguration(tree, 'test'); |
||||
|
expect(config).toBeDefined(); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,26 @@ |
|||||
|
import { Tree } from '@nx/devkit'; |
||||
|
import { wrapAngularDevkitSchematic } from '@nx/devkit/ngcli-adapter'; |
||||
|
import { ChangeThemeGeneratorSchema } from './schema'; |
||||
|
import { ThemeOptionsEnum } from './theme-options.enum'; |
||||
|
|
||||
|
export async function changeThemeGenerator(host: Tree, schema: ChangeThemeGeneratorSchema) { |
||||
|
const schematicPath = schema.localPath || '@abp/ng.schematics'; |
||||
|
|
||||
|
const runAngularLibrarySchematic = wrapAngularDevkitSchematic( |
||||
|
schema.localPath ? `${host.root}${schematicPath}` : schematicPath, |
||||
|
'change-theme', |
||||
|
); |
||||
|
|
||||
|
await runAngularLibrarySchematic(host, { |
||||
|
...schema, |
||||
|
}); |
||||
|
|
||||
|
return () => { |
||||
|
const destTheme = Object.values(ThemeOptionsEnum).find( |
||||
|
(theme, index) => index + 1 === schema.name, |
||||
|
); |
||||
|
console.log(`✅️ Switched to Theme ${destTheme}`); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export default changeThemeGenerator; |
||||
@ -0,0 +1,3 @@ |
|||||
|
export * from './theme-options.enum'; |
||||
|
export * from './schema'; |
||||
|
export * from './generator'; |
||||
@ -0,0 +1,5 @@ |
|||||
|
export interface ChangeThemeGeneratorSchema { |
||||
|
name: number; |
||||
|
targetOption: string; |
||||
|
localPath?: string; |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
{ |
||||
|
"$schema": "http://json-schema.org/schema", |
||||
|
"$id": "SchematicsABPThemeChanger", |
||||
|
"title": "ABP Theme Style Generator API Schema", |
||||
|
"type": "object", |
||||
|
"properties": { |
||||
|
"name": { |
||||
|
"description": "The name of theme will change.", |
||||
|
"type": "number", |
||||
|
"$default": { |
||||
|
"$source": "argv", |
||||
|
"index": 0 |
||||
|
}, |
||||
|
"enum": [1, 2, 3, 4], |
||||
|
"x-prompt": { |
||||
|
"message": "Which theme would you like to use?", |
||||
|
"type": "list", |
||||
|
"items": [ |
||||
|
{ "value": 1, "label": "Basic" }, |
||||
|
{ "value": 2, "label": "Lepton" }, |
||||
|
{ "value": 3, "label": "LeptonXLite" }, |
||||
|
{ "value": 4, "label": "LeptonX" } |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
"targetProject": { |
||||
|
"description": "The name of the project will change the style.The project type must be 'application'", |
||||
|
"type": "string", |
||||
|
"x-prompt": "Please enter the project name", |
||||
|
"$default": { |
||||
|
"$source": "argv", |
||||
|
"index": 1 |
||||
|
} |
||||
|
}, |
||||
|
"localPath": { |
||||
|
"description": "If set value schematics will work on given path", |
||||
|
"type": "string" |
||||
|
} |
||||
|
}, |
||||
|
"required": ["name", "targetProject"] |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
// this enum create by https://raw.githubusercontent.com/abpframework/abp/rel-7.4/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Theme.cs
|
||||
|
|
||||
|
export enum ThemeOptionsEnum { |
||||
|
Basic = 1, |
||||
|
Lepton = 2, |
||||
|
LeptonXLite = 3, |
||||
|
LeptonX = 4, |
||||
|
} |
||||
@ -1 +1,3 @@ |
|||||
export * from './generators/generate-proxy' |
export * from './generators/change-theme'; |
||||
|
export * from './generators/generate-proxy'; |
||||
|
export * from './generators/update-version'; |
||||
|
|||||
@ -0,0 +1,129 @@ |
|||||
|
## ℹ️ Description |
||||
|
|
||||
|
ABP Framework is a complete open-source infrastructure to create modern web applications by following the best practices and conventions of software development. This package is a part of the [ABP Framework](https://abp.io) and contains client-side files. |
||||
|
For more information, check out the below links: |
||||
|
|
||||
|
🔗Official Website: https://abp.io |
||||
|
|
||||
|
🔗Commercial Website: https://commercial.abp.io |
||||
|
|
||||
|
🔗Commercial Demo: https://commercial.abp.io/demo |
||||
|
|
||||
|
🔗GitHub Repository: https://github.com/abpframework/abp |
||||
|
|
||||
|
🔗Official Theme: https://www.LeptonTheme.com |
||||
|
|
||||
|
🔗Documentation: https://docs.abp.io |
||||
|
|
||||
|
🔗Community: https://community.abp.io |
||||
|
|
||||
|
🔗Blog: https://blog.abp.io |
||||
|
|
||||
|
🔗Books: https://abp.io/books |
||||
|
|
||||
|
🔗Twitter: https://twitter.com/abpframework |
||||
|
|
||||
|
🔗Discord: https://community.abp.io/discord |
||||
|
|
||||
|
🔗Stackoverflow: https://stackoverflow.com/questions/tagged/abp |
||||
|
|
||||
|
🔗YouTube: https://www.youtube.com/@Volosoft |
||||
|
|
||||
|
|
||||
|
## 🤔 Why ABP Platform? |
||||
|
|
||||
|
Why should you use the ABP.IO Platform instead of creating a new solution from scratch? |
||||
|
|
||||
|
You can find the answer here 👉🏻 [Why ABP Platform?](https://docs.abp.io/en/commercial/latest/why-abp-io-platform) |
||||
|
|
||||
|
## 🚀 Key Features of the ABP Framework |
||||
|
|
||||
|
🟡 Modularity |
||||
|
|
||||
|
🟡 Multi-Tenancy |
||||
|
|
||||
|
🟡 Bootstrap Tag Helpers |
||||
|
|
||||
|
🟡 Dynamic Forms |
||||
|
|
||||
|
🟡 Authentication |
||||
|
|
||||
|
🟡 Authorization |
||||
|
|
||||
|
🟡 Distributed Event Bus |
||||
|
|
||||
|
🟡 BLOB Storing |
||||
|
|
||||
|
🟡 Text Templating |
||||
|
|
||||
|
🟡 Tooling: ABP CLI |
||||
|
|
||||
|
🟡 Cross-Cutting Concerns |
||||
|
|
||||
|
🟡 Bundling & Minification |
||||
|
|
||||
|
🟡 Virtual File System |
||||
|
|
||||
|
🟡 Theming |
||||
|
|
||||
|
🟡 Background Jobs |
||||
|
|
||||
|
🟡 DDD Infrastructure |
||||
|
|
||||
|
🟡 Auto REST APIs |
||||
|
|
||||
|
🟡 Dynamic Client Proxies |
||||
|
|
||||
|
🟡 Multiple Database Providers |
||||
|
|
||||
|
🟡 Data filtering |
||||
|
|
||||
|
🟡 Test Infrastructure |
||||
|
|
||||
|
🟡 Audit Logging |
||||
|
|
||||
|
🟡 Object to Object Mapping |
||||
|
|
||||
|
🟡 Email & SMS Abstractions |
||||
|
|
||||
|
🟡 Localization |
||||
|
|
||||
|
🟡 Setting Management |
||||
|
|
||||
|
🟡 Extension Methods |
||||
|
|
||||
|
🟡 Aspect Oriented Programming |
||||
|
|
||||
|
🟡 Dependency Injection |
||||
|
|
||||
|
|
||||
|
## 🧐 How It Works? |
||||
|
|
||||
|
The following page explains how you use the ABP.IO Platform as a .NET developer 👉 [How it works?](https://commercial.abp.io/how-it-works) |
||||
|
|
||||
|
|
||||
|
### 📘 Supported Database Providers |
||||
|
|
||||
|
🔵 Entity Framework Core |
||||
|
|
||||
|
🔵 MongoDB |
||||
|
|
||||
|
🔵 Dapper |
||||
|
|
||||
|
|
||||
|
### 🎴 Supported UI Frameworks |
||||
|
|
||||
|
🔵 Angular |
||||
|
|
||||
|
🔵 Razor Pages |
||||
|
|
||||
|
🔵 Blazor Web Assembly |
||||
|
|
||||
|
🔵 Blazor Server |
||||
|
|
||||
|
🔵 MAUI with Blazor Hybrid |
||||
|
|
||||
|
|
||||
|
## 📫 Bug & Support |
||||
|
|
||||
|
Support for open-source ABP Framework client-side packages is available at [GitHub Issues](https://github.com/abpframework/abp/issues), and the commercial support is available at [support.abp.io](https://support.abp.io). |
||||
@ -0,0 +1,5 @@ |
|||||
|
module.exports = { |
||||
|
mappings: { |
||||
|
"@node_modules/zxcvbn/dist/zxcvbn.js": "@libs/zxcvbn/" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
{ |
||||
|
"version": "7.4.0-rc.3", |
||||
|
"name": "@abp/zxcvbn", |
||||
|
"publishConfig": { |
||||
|
"access": "public" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@abp/core": "~7.4.0-rc.3", |
||||
|
"zxcvbn" : "^4.4.2" |
||||
|
}, |
||||
|
"gitHead": "bb4ea17d5996f01889134c138d00b6c8f858a431", |
||||
|
"homepage": "https://abp.io", |
||||
|
"repository": { |
||||
|
"type": "git", |
||||
|
"url": "https://github.com/abpframework/abp.git" |
||||
|
}, |
||||
|
"license": "LGPL-3.0", |
||||
|
"keywords": [ |
||||
|
"aspnetcore", |
||||
|
"boilerplate", |
||||
|
"framework", |
||||
|
"web", |
||||
|
"best-practices", |
||||
|
"angular", |
||||
|
"maui", |
||||
|
"blazor", |
||||
|
"mvc", |
||||
|
"csharp", |
||||
|
"webapp" |
||||
|
] |
||||
|
} |
||||
|
|
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue