diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000000..2e36a791b6 --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,38 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 30 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: [] + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: > + This thread has been automatically locked since there has not been + any recent activity after it was closed. Please open a new issue for + related bugs. + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: true + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo \ No newline at end of file diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..c1133728da --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,15 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true +# Label to use when marking an issue as stale +staleLabel: inactive +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false \ No newline at end of file diff --git a/.github/workflows/angular.yml b/.github/workflows/angular.yml index 3f576ee9c1..60051c2dc1 100644 --- a/.github/workflows/angular.yml +++ b/.github/workflows/angular.yml @@ -3,15 +3,6 @@ on: pull_request: paths: - 'npm/ng-packs/**' - branches: - #- master - - dev - push: - paths: - - 'npm/ng-packs/**' - branches: - #- master - - dev jobs: build-test-lint: runs-on: ubuntu-18.04 @@ -19,6 +10,6 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: - node-version: '10.x' + node-version: '12.x' - run: yarn && yarn ci working-directory: npm/ng-packs diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/AbpIoLocalizationModule.cs b/abp_io/AbpIoLocalization/AbpIoLocalization/AbpIoLocalizationModule.cs index 37541b4d4c..b3ebccd72f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/AbpIoLocalizationModule.cs +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/AbpIoLocalizationModule.cs @@ -3,6 +3,7 @@ using AbpIoLocalization.Admin.Localization; using AbpIoLocalization.Base.Localization; using AbpIoLocalization.Blog.Localization; using AbpIoLocalization.Commercial.Localization; +using AbpIoLocalization.Community.Localization; using AbpIoLocalization.Docs.Localization; using AbpIoLocalization.Support.Localization; using AbpIoLocalization.Www; @@ -29,6 +30,7 @@ namespace AbpIoLocalization { options.MapCodeNamespace("Volo.AbpIo.Commercial", typeof(AbpIoCommercialResource)); options.MapCodeNamespace("Volo.AbpIo.Domain", typeof(AbpIoBaseResource)); + options.MapCodeNamespace("Volo.AbpIo.Community", typeof(AbpIoCommunityResource)); }); Configure(options => @@ -74,6 +76,11 @@ namespace AbpIoLocalization .Add("en") .AddVirtualJson("/Www/Localization/Resources") .AddBaseTypes(typeof(AbpIoBaseResource)); + + options.Resources + .Add("en") + .AddVirtualJson("/Community/Localization/Resources") + .AddBaseTypes(typeof(AbpIoBaseResource)); }); } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 776b61b168..e5cfbcae4e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -166,6 +166,30 @@ "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount !", "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero !", "UnlimitedQuestionCount": "Unlimited question count", - "Notes": "Notes" + "Notes": "Notes", + "Menu:Community": "Community", + "Menu:Articles": "Articles", + "Wait": "Wait", + "Approve": "Approve", + "Reject": "Reject", + "Details": "Details", + "Url": "Url", + "Title": "Title", + "ContentSource": "Content source", + "Status": "Status", + "ReadArticle": "Read article", + "ArticleHasBeenWaiting": "Article has been waiting", + "ArticleHasBeenApproved": "Article has been approved", + "ArticleHasBeenRejected": "Article has been rejected", + "Permission:Community": "Community", + "Permission:CommunityArticle": "Article", + "Link": "Link", + "Enum:ContentSource:0": "Github", + "Enum:ContentSource:1": "External", + "Enum:Status:0": "Waiting", + "Enum:Status:1": "Rejected", + "Enum:Status:2": "Approved", + "Summary": "Summary", + "AuthorName": "Author name" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/AbpIoCommunityResource.cs b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/AbpIoCommunityResource.cs new file mode 100644 index 0000000000..2d7f93aeae --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/AbpIoCommunityResource.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Localization; + +namespace AbpIoLocalization.Community.Localization +{ + [LocalizationResourceName("AbpIoCommunity")] + public class AbpIoCommunityResource + { + + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json new file mode 100644 index 0000000000..dbe786fb97 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -0,0 +1,41 @@ +{ + "culture": "en", + "texts": { + "Permission:CommunityArticle": "Community Article", + "Permission:Edit": "Edit", + "Waiting": "Waiting", + "Approved": "Approved", + "Rejected": "Rejected", + "Wait": "Wait", + "Approve": "Approve", + "Reject": "Reject", + "ReadArticle": "Read Article", + "Status": "Status", + "ContentSource": "Content Source", + "Details": "Details", + "Url": "Url", + "Title": "Title", + "CreationTime": "Creation time", + "Save": "Save", + "SameUrlAlreadyExist": "Same url already exist if you want to add this article, you should change the url!", + "UrlIsNotValid": "Url is not valid.", + "UrlNotFound" : "Url not found.", + "UrlContentNotFound": "Url content not found.", + "Summary": "Summary", + "MostRead": "Most Read", + "LatestArticles": "Latest Articles", + "ContributeAbpCommunity": "Contribute to the ABP Community", + "SubmitYourArticle": "Submit Your Article", + "ContributionGuide": "Contribution Guide", + "BugReport": "Bug Report", + "SeeAllArticles": "See All Articles", + "WelcomeToABPCommunity!": "Welcome to the ABP Community!", + "MyProfile": "My profile", + "MyOrganizations": "My organizations", + "EmailNotValid": "Please enter a valid email address.", + "FeatureRequest": "Feature Request", + "CreateArticleTitleInfo": "Title of the article to be shown on the article list.", + "CreateArticleUrlInfo": "Original GitHub/External URL of the article.", + "CreateArticleSummaryInfo": "A short summary of the article to be shown on the article list." + } +} diff --git a/common.props b/common.props index f834d1ab49..eea8755d42 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ - + latest - 3.1.0 + 3.1.0 $(NoWarn);CS1591 https://abp.io/assets/abp_nupkg.png https://abp.io diff --git a/docs/en/API/Application-Configuration.md b/docs/en/API/Application-Configuration.md index db624987c5..3aea468f8c 100644 --- a/docs/en/API/Application-Configuration.md +++ b/docs/en/API/Application-Configuration.md @@ -2,12 +2,12 @@ ABP Framework provides a pre-built and standard endpoint that contains some useful information about the application/service. Here, the list of some fundamental information at this endpoint: -* [Localization](Localization.md) values, supported and the current language of the application. -* Available and granted [policies](Authorization.md) (permissions) for the current user. -* [Setting](Settings.md) values for the current user. -* Info about the [current user](CurrentUser.md) (like id and user name). -* Info about the current [tenant](Multi-Tenancy.md) (like id and name). -* [Time zone](Timing.md) information for the current user and the [clock](Timing.md) type of the application. +* [Localization](../Localization.md) values, supported and the current language of the application. +* Available and granted [policies](../Authorization.md) (permissions) for the current user. +* [Setting](../Settings.md) values for the current user. +* Info about the [current user](../CurrentUser.md) (like id and user name). +* Info about the current [tenant](../Multi-Tenancy.md) (like id and name). +* [Time zone](../Timing.md) information for the current user and the [clock](../Timing.md) type of the application. ## HTTP API diff --git a/docs/en/Authentication/Social-External-Logins.md b/docs/en/Authentication/Social-External-Logins.md new file mode 100644 index 0000000000..c48763a92a --- /dev/null +++ b/docs/en/Authentication/Social-External-Logins.md @@ -0,0 +1,30 @@ +# Social/External Logins + +## ASP.NET Core MVC / Razor Pages UI + +The [Account Module](../Modules/Account.md) has already configured to handle social or external logins out of the box. You can follow the ASP.NET Core documentation to add a social/external login provider to your application. + +### Example: Facebook Authentication + +Follow the [ASP.NET Core Facebook integration document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to support the Facebook login for your application. + +#### Add the NuGet Package + +Add the [Microsoft.AspNetCore.Authentication.Facebook](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Facebook) package to your project. Based on your architecture, this can be `.Web`, `.IdentityServer` (for tiered setup) or `.Host` project. + +#### Configure the Provider + +Use the `.AddFacebook(...)` extension method in the `ConfigureServices` method of your [module](../Module-Development-Basics.md), to configure the client: + +````csharp +context.Services.AddAuthentication() + .AddFacebook(facebook => + { + facebook.AppId = "..."; + facebook.AppSecret = "..."; + facebook.Scope.Add("email"); + facebook.Scope.Add("public_profile"); + }); +```` + +> It would be a better practice to use the `appsettings.json` or the ASP.NET Core User Secrets system to store your credentials, instead of a hard-coded value like that. Follow the [Microsoft's document](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins) to learn the user secrets usage. \ No newline at end of file diff --git a/docs/en/Authorization.md b/docs/en/Authorization.md index 1ede9f7577..4b7502f437 100644 --- a/docs/en/Authorization.md +++ b/docs/en/Authorization.md @@ -284,8 +284,6 @@ You may need to check a policy/permission on the client side. For ASP.NET Core M abp.auth.isGranted('MyPermissionName'); ``` -See [abp.auth](UI/AspNetCore/JavaScript-API/Index.md) API documentation for details. - ## Permission Management Permission management is normally done by an admin user using the permission management modal: diff --git a/docs/en/Background-Workers-Quartz.md b/docs/en/Background-Workers-Quartz.md index f8495b27a4..e1d8287019 100644 --- a/docs/en/Background-Workers-Quartz.md +++ b/docs/en/Background-Workers-Quartz.md @@ -1,6 +1,6 @@ # Quartz Background Worker Manager -[Quartz](https://www.quartz-scheduler.net/) is an advanced background worker manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background worker manager](Background-Worker.md). ABP simply integrates quartz. +[Quartz](https://www.quartz-scheduler.net/) is an advanced background worker manager. You can integrate Quartz with the ABP Framework to use it instead of the [default background worker manager](Background-Workers.md). ABP simply integrates quartz. ## Installation @@ -36,6 +36,9 @@ public class YourModule : AbpModule } ```` +> Quartz background worker integration provided `QuartzPeriodicBackgroundWorkerAdapter` to adapt `PeriodicBackgroundWorkerBase` and `AsyncPeriodicBackgroundWorkerBase` derived class. So, you can still fllow the [background workers document](Background-Workers.md) to define the background worker. +> `BackgroundJobWorker` checks todo jobs every 5 seconds, but quartz will not block when long time jobs are executing. So,after Added Quartz background worker integration, you also need to add [Quartz Background Jobs](Background-Jobs-Quartz.md) or [Hangfire Background Jobs](Background-Jobs-Hangfire.md) to avoid duplicate execution jobs. + ## Configuration See [Configuration](Background-Jobs-Quartz#Configuration). diff --git a/docs/en/Best-Practices/Application-Services.md b/docs/en/Best-Practices/Application-Services.md index 876105c214..3332ce08ea 100644 --- a/docs/en/Best-Practices/Application-Services.md +++ b/docs/en/Best-Practices/Application-Services.md @@ -206,7 +206,7 @@ This method votes a question and returns the current score of the question. #### Extra Properties -* **Do** use either `MapExtraPropertiesTo` extension method ([see](Object-Extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services. +* **Do** use either `MapExtraPropertiesTo` extension method ([see](../Object-Extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services. #### Manipulating / Deleting Entities diff --git a/docs/en/Blob-Storing-Aliyun.md b/docs/en/Blob-Storing-Aliyun.md index 602d666787..62e57e58c7 100644 --- a/docs/en/Blob-Storing-Aliyun.md +++ b/docs/en/Blob-Storing-Aliyun.md @@ -23,18 +23,21 @@ Configuration is done in the `ConfigureServices` method of your [module](Module- ````csharp Configure(options => { - options.Containerscontainer.UseAliyun(aliyun => + options.Containers.ConfigureDefault(container => { - aliyun.AccessKeyId = "your aliyun access key id"; - aliyun.AccessKeySecret = "your aliyun access key secret"; - aliyun.Endpoint = "your oss endpoint"; - aliyun.RegionId = "your sts region id"; - aliyun.RoleArn = "the arn of ram role"; - aliyun.RoleSessionName = "the name of the certificate"; - aliyun.Policy = "policy"; - aliyun.DurationSeconds = "expiration date"; - aliyun.ContainerName = "your aliyun container name"; - aliyun.CreateContainerIfNotExists = false; + container.UseAliyun(aliyun => + { + aliyun.AccessKeyId = "your aliyun access key id"; + aliyun.AccessKeySecret = "your aliyun access key secret"; + aliyun.Endpoint = "your oss endpoint"; + aliyun.RegionId = "your sts region id"; + aliyun.RoleArn = "the arn of ram role"; + aliyun.RoleSessionName = "the name of the certificate"; + aliyun.Policy = "policy"; + aliyun.DurationSeconds = "expiration date"; + aliyun.ContainerName = "your aliyun container name"; + aliyun.CreateContainerIfNotExists = false; + }); }); }); ```` diff --git a/docs/en/Blob-Storing-Aws.md b/docs/en/Blob-Storing-Aws.md index a2ac8309ca..cfee5c3f57 100644 --- a/docs/en/Blob-Storing-Aws.md +++ b/docs/en/Blob-Storing-Aws.md @@ -23,23 +23,27 @@ Configuration is done in the `ConfigureServices` method of your [module](Module- ````csharp Configure(options => { - options.Containerscontainer.UseAws(Aws => + options.Containers.ConfigureDefault(container => { - Aws.AccessKeyId = "your Aws access key id"; - Aws.SecretAccessKey = "your Aws access key secret"; - Aws.UseCredentials = "set true to use credentials"; - Aws.UseTemporaryCredentials = "set true to use temporary credentials"; - Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; - Aws.ProfileName = "the name of the profile to get credentials from"; - Aws.ProfilesLocation = "the path to the aws credentials file to look at"; - Aws.Region = "the system name of the service"; - Aws.Name = "the name of the federated user"; - Aws.Policy = "policy"; - Aws.DurationSeconds = "expiration date"; - Aws.ContainerName = "your Aws container name"; - Aws.CreateContainerIfNotExists = false; + container.UseAws(Aws => + { + Aws.AccessKeyId = "your Aws access key id"; + Aws.SecretAccessKey = "your Aws access key secret"; + Aws.UseCredentials = "set true to use credentials"; + Aws.UseTemporaryCredentials = "set true to use temporary credentials"; + Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; + Aws.ProfileName = "the name of the profile to get credentials from"; + Aws.ProfilesLocation = "the path to the aws credentials file to look at"; + Aws.Region = "the system name of the service"; + Aws.Name = "the name of the federated user"; + Aws.Policy = "policy"; + Aws.DurationSeconds = "expiration date"; + Aws.ContainerName = "your Aws container name"; + Aws.CreateContainerIfNotExists = false; + }); }); }); + ```` > See the [BLOB Storing document](Blob-Storing.md) to learn how to configure this provider for a specific container. diff --git a/docs/en/Blob-Storing-Azure.md b/docs/en/Blob-Storing-Azure.md index d43f8b3a43..9d033013da 100644 --- a/docs/en/Blob-Storing-Azure.md +++ b/docs/en/Blob-Storing-Azure.md @@ -23,11 +23,14 @@ Configuration is done in the `ConfigureServices` method of your [module](Module- ````csharp Configure(options => { - options.Containerscontainer.UseAzure(azure => + options.Containers.ConfigureDefault(container => { - azure.ConnectionString = "your azure connection string"; - azure.ContainerName = "your azure container name"; - azure.CreateContainerIfNotExists = false; + container.UseAzure(azure => + { + azure.ConnectionString = "your azure connection string"; + azure.ContainerName = "your azure container name"; + azure.CreateContainerIfNotExists = false; + }); }); }); ```` diff --git a/docs/en/Blob-Storing-Database.md b/docs/en/Blob-Storing-Database.md index 69b9232ee8..c14086a2f4 100644 --- a/docs/en/Blob-Storing-Database.md +++ b/docs/en/Blob-Storing-Database.md @@ -27,7 +27,7 @@ This command adds all the NuGet packages to corresponding layers of your solutio Here, all the NuGet packages defined by this provider; -* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Domain.Shared) +* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain.Shared) * [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain) * [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) * [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) diff --git a/docs/en/Blob-Storing-Minio.md b/docs/en/Blob-Storing-Minio.md index e7ec455716..505c6721b0 100644 --- a/docs/en/Blob-Storing-Minio.md +++ b/docs/en/Blob-Storing-Minio.md @@ -23,12 +23,15 @@ Configuration is done in the `ConfigureServices` method of your [module](Module- ````csharp Configure(options => { - options.Containerscontainer.UseMinio(minio => - { - minio.EndPoint = "your minio endPoint"; - minio.AccessKey = "your minio accessKey"; - minio.SecretKey = "your minio secretKey"; - minio.BucketName = "your minio bucketName"; + options.Containers.ConfigureDefault(container => + { + container.UseMinio(minio => + { + minio.EndPoint = "your minio endPoint"; + minio.AccessKey = "your minio accessKey"; + minio.SecretKey = "your minio secretKey"; + minio.BucketName = "your minio bucketName"; + }); }); }); ```` diff --git a/docs/en/How-To/Customize-SignIn-Manager.md b/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md similarity index 83% rename from docs/en/How-To/Customize-SignIn-Manager.md rename to docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md index 4fc6291bc4..d46049526d 100644 --- a/docs/en/How-To/Customize-SignIn-Manager.md +++ b/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md @@ -1,6 +1,6 @@ # How to Customize the SignIn Manager for ABP Applications -After creating a new application using the [application startup template](../Startup-Templates/Application.md), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](../Modules/Account.md) uses the [Identity Management Module](../Modules/Identity.md) for SignIn Manager and the [Identity Management Module](../Modules/Identity.md) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). +After creating a new application using the [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application), you may want extend or change the default behavior of the SignIn Manager for your authentication and registration flow needs. ABP [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) uses the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) for SignIn Manager and the [Identity Management Module](https://docs.abp.io/en/abp/latest/Modules/Identity) uses default [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) ([see here](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/identity/src/Volo.Abp.Identity.AspNetCore/Volo/Abp/Identity/AspNetCore/AbpIdentityAspNetCoreModule.cs#L17)). To write your Custom SignIn Manager, you need to extend [Microsoft Identity SignIn Manager](https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs) class and register it to the DI container. @@ -76,7 +76,7 @@ public override async Task GetE } ```` -To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](../Dependency-Injection.md). +To get your overridden method invoked and your customized SignIn Manager class to work, you need to register your class to the [Dependency Injection System](https://docs.abp.io/en/abp/latest/Dependency-Injection). ## Register to Dependency Injection @@ -93,9 +93,4 @@ PreConfigure(identityBuilder => ## The Source Code -You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). - -## See Also - -* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). -* [Identity Management Module](../Modules/Identity.md). \ No newline at end of file +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). \ No newline at end of file diff --git a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md b/docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md similarity index 95% rename from docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md rename to docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md index 7e6319839a..c7b0258d55 100644 --- a/docs/en/How-To/Azure-Active-Directory-Authentication-MVC.md +++ b/docs/en/Community-Articles/2020-04-27-Use-Azure-Active-Directory-Authentication-for-MVC-Razor-Page-Applications/POST.md @@ -80,7 +80,7 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi > * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear()`. This will disable the default Microsoft claim type mapping. > * Add `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Add("sub", ClaimTypes.NameIdentifier)`. Mapping this to [ClaimTypes.NameIdentifier](https://github.com/dotnet/runtime/blob/6d395de48ac718a913e567ae80961050f2a9a4fa/src/libraries/System.Security.Claims/src/System/Security/Claims/ClaimTypes.cs#L59) is important since default SignIn Manager behavior uses this claim type for external login information. > * Add `options.SignInScheme = IdentityConstants.ExternalScheme` since [default signin scheme is `AzureADOpenID`](https://github.com/dotnet/aspnetcore/blob/c56aa320c32ee5429d60647782c91d53ac765865/src/Azure/AzureAD/Authentication.AzureAD.UI/src/AzureADOpenIdConnectOptionsConfiguration.cs#L35). -> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](../Modules/Account.md) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). +> * Add `options.Scope.Add("email")` if you are using **v2.0** endpoint of AzureAD since v2.0 endpoint doesn't return the `email` claim as default. The [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) uses `email` claim to [register external users](https://github.com/abpframework/abp/blob/be32a55449e270d2d456df3dabdc91f3ffdd4fa9/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml.cs#L215). You are done and integration is completed. @@ -205,8 +205,3 @@ You can find the source code of the completed example [here](https://github.com/ await Task.CompletedTask; }); ```` - -## See Also - -* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md). -* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md). \ No newline at end of file diff --git a/docs/en/How-To/Customize-Login-Page-MVC.md b/docs/en/Community-Articles/2020-05-09-Customize-the-Login-Page-for-MVC-Razor-Page-Applications/POST.md similarity index 90% rename from docs/en/How-To/Customize-Login-Page-MVC.md rename to docs/en/Community-Articles/2020-05-09-Customize-the-Login-Page-for-MVC-Razor-Page-Applications/POST.md index 89c280cd5a..562d545ac6 100644 --- a/docs/en/How-To/Customize-Login-Page-MVC.md +++ b/docs/en/Community-Articles/2020-05-09-Customize-the-Login-Page-for-MVC-Razor-Page-Applications/POST.md @@ -1,6 +1,6 @@ # How to Customize the Login Page for MVC / Razor Page Applications -When you create a new application using the [application startup template](../Startup-Templates/Application.md), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](../Modules/Account.md) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. +When you create a new application using the [application startup template](https://docs.abp.io/en/abp/latest/Startup-Templates/Application), source code of the login page will not be inside your solution, so you can not directly change it. The login page comes from the [Account Module](https://docs.abp.io/en/abp/latest/Modules/Account) that is used a [NuGet package](https://www.nuget.org/packages/Volo.Abp.Account.Web) reference. This document explains how to customize the login page for your own application. @@ -20,7 +20,7 @@ public class CustomLoginModel : LoginModel } ```` -> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](../Dependency-Injection.md) system. +> Naming convention is important here. If your class name doesn't end with `LoginModel`, you need to manually replace the `LoginModel` using the [dependency injection](https://docs.abp.io/en/abp/latest/Dependency-Injection) system. Then you can override any method you need and add new methods and properties needed by the UI. @@ -105,8 +105,4 @@ Just changed the `@model` to `Acme.BookStore.Web.Pages.Account.CustomLoginModel` ## The Source Code -You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). - -## See Also - -* [ASP.NET Core (MVC / Razor Pages) User Interface Customization Guide](../UI/AspNetCore/Customization-User-Interface.md). +You can find the source code of the completed example [here](https://github.com/abpframework/abp-samples/tree/master/Authentication-Customization). \ No newline at end of file diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/POST.md b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/POST.md new file mode 100644 index 0000000000..915d4ef5c1 --- /dev/null +++ b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/POST.md @@ -0,0 +1,462 @@ +# Real Time Messaging In A Distributed Architecture Using Abp Framework, SingalR & RabbitMQ + +In this article, we will build a basic real time messaging application in a distributed architecture. We will use [Abp Framework](https://abp.io) for infrastructure and tiered startup template, [SignalR](https://dotnet.microsoft.com/apps/aspnet/signalr) for real time server-client communication and [RabbitMQ](https://www.rabbitmq.com/) as the distributed event bus. + +When Web & API tiers are separated, it is impossible to directly send a server-to-client message from the HTTP API. This is also true for a microservice architected application. We suggest to use the distributed event bus to deliver the message from API application to the web application, then to the client. + + +![data flow](dataflow-diagram.png) + +Above, you can see the data-flow that we will implement in this article. This diagram represents how data will flow in our application when **Client 1** sends a message to **Client 2**. It is explained in 5 steps: + +1. **Client 1** sends a message data to **Web Application** via REST call. +2. **Web Application** redirects the message data to **Http Api**. +3. The message data is processed in **Http Api** and **Http Api** publishes an event that holds the data that will be sent to **Client 2**. +4. **Web application**, that is subscribed to that event, receives it. +5. **Web Application** sends the message to **Client 2**. + +For this example flow, we could send message from **Client 1** to **Client 2** directly on the **SignalR Hub**. However, what we are trying here to demonstrate is sending a real-time message from the **Http Api** to a specific user who is connected to the web application. + +## Implementation + +### Startup template and initial run + +[Abp Framework](https://www.abp.io) offers startup templates to get into the business faster. We can download a new tiered startup template using [Abp CLI](https://docs.abp.io/en/abp/latest/CLI): + +`abp new SignalRTieredDemo --tiered` + +After download is finished, we run ***.DbMigrator** project to create the database and seed initial data (admin user, role etc). Then we run ***.IdentityServer**, ***.HttpApi.Host** and ***.Web** to see our application working. + +### Creating Application Layer + +We create an [application service](https://docs.abp.io/en/abp/latest/Application-Services) that publishes the message as event. + +In ***.Application.Contracts** project: + +````csharp +using System.Threading.Tasks; +using Volo.Abp.Application.Services; + +namespace SignalRTieredDemo +{ + public interface IChatAppService : IApplicationService + { + Task SendMessageAsync(SendMessageInput input); + } +} +```` +Input DTO for SendMessageAsync method: + +````csharp +namespace SignalRTieredDemo +{ + public class SendMessageInput + { + public string TargetUserName { get; set; } + + public string Message { get; set; } + } +} +```` +Event transfer object (ETO) for communication on event bus: + +````csharp +using System; + +namespace SignalRTieredDemo +{ + public class ReceivedMessageEto + { + public string ReceivedText { get; set; } + + public Guid TargetUserId { get; set; } + + public string SenderUserName { get; set; } + + public ReceivedMessageEto( + Guid targetUserId, string senderUserName, string receivedText) + { + ReceivedText = receivedText; + TargetUserId = targetUserId; + SenderUserName = senderUserName; + } + } +} + +```` + +In ***.Application** project: + +````csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Identity; + +namespace SignalRTieredDemo +{ + public class ChatAppService: SignalRTieredDemoAppService, IChatAppService + { + private readonly IIdentityUserRepository _identityUserRepository; + private readonly ILookupNormalizer _lookupNormalizer; + private readonly IDistributedEventBus _distributedEventBus; + + public ChatAppService(IIdentityUserRepository identityUserRepository, ILookupNormalizer lookupNormalizer, IDistributedEventBus distributedEventBus) + { + _identityUserRepository = identityUserRepository; + _lookupNormalizer = lookupNormalizer; + _distributedEventBus = distributedEventBus; + } + + public async Task SendMessageAsync(SendMessageInput input) + { + var targetId = (await _identityUserRepository.FindByNormalizedUserNameAsync(_lookupNormalizer.NormalizeName(input.TargetUserName))).Id; + + await _distributedEventBus.PublishAsync(new ReceivedMessageEto(targetId, CurrentUser.UserName, input.Message)); + } + } +} +```` + +### Creating API Layer + +We create an endpoint for sending message that redirects the process to application layer: + +In **controllers** folder of ***.HttpApi** project: + +````csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace SignalRTieredDemo.Controllers +{ + [Route("api/app/chat")] + public class ChatController : AbpController, IChatAppService + { + private readonly IChatAppService _chatAppService; + + public ChatController(IChatAppService chatAppService) + { + _chatAppService = chatAppService; + } + + [HttpPost] + [Route("send-message")] + public async Task SendMessageAsync(SendMessageInput input) + { + await _chatAppService.SendMessageAsync(input); + } + } +} +```` +### Adding SignalR + +To add SignalR to our solution, we add `Volo.Abp.AspNetCore.SignalR` nuget package to ***.Web** project. + +And then add `AbpAspNetCoreSignalRModule` dependency: + +````csharp + +namespace SignalRTieredDemo.Web +{ + [DependsOn( + ... + typeof(AbpAspNetCoreSignalRModule) // <--- + )] + public class SignalRTieredDemoWebModule : AbpModule + { +```` + +Also, we need to add [@abp/signalr](https://www.npmjs.com/package/@abp/signalr) npm package to package.json in ***.Web** project, then run **yarn** and **gulp** commands. + +`````json +{ + . + . + "dependencies": { + . + . + "@abp/signalr": "^2.9.0" + } +} +````` + +*Remember to add the latest package version.* + +You can find more information for Abp SignalR Integration on [the related document](https://docs.abp.io/en/abp/latest/SignalR-Integration). + +### Creating A Hub + +We need a hub for SignalR connection. We can inherit it from `AbpHup` base class. + +In ***.Web** project: + +````csharp +using Microsoft.AspNetCore.Authorization; +using Volo.Abp.AspNetCore.SignalR; + +namespace SignalRTieredDemo.Web +{ + [Authorize] + public class ChatHub : AbpHub + { + } +} + +```` + +While you could inherit from the standard `Hub` class, `AbpHub` has some common services pre-injected as base properties, which is useful on your development. + +### Adding & Configuring RabbitMQ + +To add RabbitMQ to our solution, we add `Volo.Abp.EventBus.RabbitMQ` nuget package to ***.HttpApi.Host** and ***.Web** projects. + +Launch a **command line**, navigate to directory where ***.HttpApi.Host.csproj** file exist, and run the command below using [Abp CLI](https://docs.abp.io/en/abp/latest/CLI): + +````bash + abp add-package Volo.Abp.EventBus.RabbitMQ +```` + +Then do the same for ***.Web** project. + +After we add the package, we configure RabbitMQ by adding configuration in **appsettings.json** files of those projects. + +For ***.HttpApi.Host** project: + +````json +{ + ... + "RabbitMQ": { + "Connections": { + "Default": { + "HostName": "localhost" + } + }, + "EventBus": { + "ClientName": "SignalRTieredDemo_HttpApi", + "ExchangeName": "SignalRTieredDemoTest" + } + }, + ... +} +```` +For ***.Web** project: +````json +{ + ... + "RabbitMQ": { + "Connections": { + "Default": { + "HostName": "localhost" + } + }, + "EventBus": { + "ClientName": "SignalRTieredDemo_Web", + "ExchangeName": "SignalRTieredDemoTest" + } + }, + ... +} + +```` + +### Handling New Message Event + +Once we publish a new message event from `Http Api`, we must to handle it in `Web Application`. Therefore we need an event handler in ***.Web** Project: + +````csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; + +namespace SignalRTieredDemo.Web +{ + public class ReceivedMessageEventHandler : + IDistributedEventHandler, + ITransientDependency + { + private readonly IHubContext _hubContext; + + public ReceivedMessageEventHandler(IHubContext hubContext) + { + _hubContext = hubContext; + } + + public async Task HandleEventAsync(ReceivedMessageEto eto) + { + var message = $"{eto.SenderUserName}: {eto.ReceivedText}"; + + await _hubContext.Clients + .User(eto.TargetUserId.ToString()) + .SendAsync("ReceiveMessage", message); + } + } +} +```` + +### Creating Chat Page + +We create the files below in **Pages** folder of ***.Web** Project. + + **Chat.cshtml**: + +````html +@page +@using Volo.Abp.AspNetCore.Mvc.UI.Packages.SignalR +@model SignalRTieredDemo.Web.Pages.ChatModel +@section styles { + +} +@section scripts { + + +} +

Chat

+ +
+ + +
All Messages:
+
    +
+
+ +
+ + + + + + + + + + + + + + + + + +
+
+
+
+```` + + **Chat.cshtml.cs**: + +````csharp +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace SignalRTieredDemo.Web.Pages +{ + public class ChatModel : PageModel + { + public void OnGet() + { + } + } +} +```` + +**Chat.css**: + +````css +#MessageList { + border: 1px solid gray; + height: 400px; + overflow: auto; + list-style: none; + padding-left: 0; + padding: 10px; +} + +#TargetUser { + width: 100%; +} + +#Message { + width: 100%; +} +```` + +**Chat.js**: + +````javascript +$(function () { + var connection = new signalR.HubConnectionBuilder().withUrl("/signalr-hubs/chat").build(); + + connection.on("ReceiveMessage", function (message) { + console.log(message); + $('#MessageList').append('
  • ' + message + '
  • '); + }); + + connection.start().then(function () { + + }).catch(function (err) { + return console.error(err.toString()); + }); + + $('#SendMessageButton').click(function (e) { + e.preventDefault(); + + var targetUserName = $('#TargetUser').val(); + var message = $('#Message').val(); + $('#Message').val(''); + + + signalRTieredDemo.controllers.chat.sendMessage({ + targetUserName: targetUserName, + message: message + }).then(function() { + $('#MessageList') + .append('
  • ' + abp.currentUser.userName + ': ' + message + '
  • '); + }); + + }); +}); +```` + +Then we can add this new page to menu on ***MenuContributor.cs** in **Menus** folder: + +````csharp + ... + public class SignalRTieredDemoMenuContributor : IMenuContributor + { + ... + private Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + ... + context.Menu.Items.Add(new ApplicationMenuItem("SignalRDemo.Chat", "Chat", "/Chat")); // <-- We add this line + + return Task.CompletedTask; + } + ... + } +```` + +## Running & Testing + +We run ***.IdentityServer**, ***.HttpApi.Host** and ***.Web** in order. After ***.Web** project is ran, firstly login with `admin` username and `1q2w3E*` password. + +![click on login](login1.png) + +![login with `admin` username and `1q2w3E*` password.](login2.png) + +After we login, go to `/Identity/Users` page and create a new user. So that we can chat with them. + +![create a new user](new-user.png) + +Then we open the application in another browser and login with the user we created above. Now we can go to chat page and start messaging: + +![messaging](chat.png) + +We can test with more user. All sent and incoming messages are displayed in the left box. + +### Source code + +Source code of the final application can be found on the [GitHub repository](https://github.com/abpframework/abp-samples/tree/master/SignalRTieredDemo). diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/chat.png b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/chat.png new file mode 100644 index 0000000000..b29daa9492 Binary files /dev/null and b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/chat.png differ diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/dataflow-diagram.png b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/dataflow-diagram.png new file mode 100644 index 0000000000..bb81ddf37a Binary files /dev/null and b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/dataflow-diagram.png differ diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/header.png b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/header.png new file mode 100644 index 0000000000..6e5a46b780 Binary files /dev/null and b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/header.png differ diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/login1.png b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/login1.png new file mode 100644 index 0000000000..4e371e2ee9 Binary files /dev/null and b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/login1.png differ diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/login2.png b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/login2.png new file mode 100644 index 0000000000..68c5a3259d Binary files /dev/null and b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/login2.png differ diff --git a/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/new-user.png b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/new-user.png new file mode 100644 index 0000000000..2ffdf99d45 Binary files /dev/null and b/docs/en/Community-Articles/2020-05-29-Real-Time-Messaging-In-A-Distributed-Architecture-Using-Abp-Framework-SingalR-RabbitMQ/new-user.png differ diff --git a/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/POST.md b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/POST.md new file mode 100644 index 0000000000..560e15e288 --- /dev/null +++ b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/POST.md @@ -0,0 +1,396 @@ +# File Upload/Download with BLOB Storage System in ASP.NET Core & ABP Framework + +## Introduction + +This step-by-step article describes how to upload a file to a Web server and also download by client with using ASP.NET Core & ABP Framework. By following this article, you will create a web project and its related code to upload and download files. + +Before the creating application, we need to know some fundamentals. + +## BLOB Storing + +It is typical to **store file contents** in an application and read these file contents on need. Not only files, but you may also need to save various types of **large binary objects**, a.k.a. [BLOB](https://en.wikipedia.org/wiki/Binary_large_object)s, into a **storage**. For example, you may want to save user profile pictures. + +A BLOB is a typically **byte array**. There are various places to store a BLOB item; storing in the local file system, in a shared database or on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/) can be options. + +The ABP Framework provides an abstraction to work with BLOBs and provides some pre-built storage providers that you can easily integrate to. Having such an abstraction has some benefits; + +- You can **easily integrate** to your favorite BLOB storage provides with a few lines of configuration. +- You can then **easily change** your BLOB storage without changing your application code. +- If you want to create **reusable application modules**, you don't need to make assumption about how the BLOBs are stored. + +ABP BLOB Storage system is also compatible to other ABP Framework features like [multi-tenancy](https://docs.abp.io/en/abp/latest/Multi-Tenancy). + +To get more information about ABP BLOB Storing system, please check this [documentation](https://docs.abp.io/en/abp/latest/Blob-Storing). + +## Preparing the Project + +### Startup template and the initial run + +Abp Framework offers startup templates to get into the business faster. We can download a new startup template using Abp CLI: + +`abp new FileActionsDemo -m none` + +After the download is finished, we run `FileActionsDemo.DbMigrator` project to create the database and seed initial data (admin user, role, etc). Then we run `FileActionsDemo.Web` to see our application working. + +> _Default admin username is **admin** and password is **1q2w3E\***_ + +![initial-project](initial-project.png) + +### Adding Blob Storing Module + +For this article, we use [Blob Storing Database Provider](https://docs.abp.io/en/abp/latest/Blob-Storing-Database). + +You can use [Azure](https://docs.abp.io/en/abp/latest/Blob-Storing-Azure) or [File System](https://docs.abp.io/en/abp/latest/Blob-Storing-File-System) providers also. + +Open a command prompt (terminal) in the folder containing your solution (.sln) file and run the following command: + +`abp add-module Volo.Abp.BlobStoring.Database` + +This action will add the module depencies and also module migration. After this action, run `FileActionsDemo.DbMigrator` to update the database. + +### Setting up Blob Storaging + +BLOB Strorage system works with `Containers`. Before the using blob storage, we need to create our blob container. + +Create a class that name `MyFileContainer` at the `FileActionsDemo.Domain` project. + +```csharp +using Volo.Abp.BlobStoring; + +namespace FileActionsDemo +{ + [BlobContainerName("my-file-container")] + public class MyFileContainer + { + + } +} +``` + +That's all, we can start to use BLOB storing in our application. + +## Creating Application Layer + +Before the creating Application Service, we need to create some [DTO](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects)s that used by Application Service. + +Create following DTOs in `FileActionsDemo.Application.Contracts` project. + +- `BlobDto.cs` + + ```csharp + namespace FileActionsDemo + { + public class BlobDto + { + public byte[] Content { get; set; } + + public string Name { get; set; } + } + } + ``` + +- `GetBlobRequestDto.cs` + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace FileActionsDemo + { + public class GetBlobRequestDto + { + [Required] + public string Name { get; set; } + } + } + ``` + +- `SaveBlobInputDto.cs` + + ```csharp + using System.ComponentModel.DataAnnotations; + + namespace FileActionsDemo + { + public class SaveBlobInputDto + { + public byte[] Content { get; set; } + + [Required] + public string Name { get; set; } + } + } + ``` + +Create `IFileAppService.cs` interface at the same place with DTOs. + +- `IFileAppService` + + ```csharp + using System.Threading.Tasks; + using Volo.Abp.Application.Services; + + namespace FileActionsDemo + { + public interface IFileAppService : IApplicationService + { + Task SaveBlobAsync(SaveBlobInputDto input); + + Task GetBlobAsync(GetBlobRequestDto input); + } + } + ``` + +After creating DTOs and interface, `FileActionsDemo.Application.Contracts` project should be like as following image. + +![application-contracts-project](application-contracts-project.png) + +Then we can create our Application Service. + +Create `FileAppService.cs` in `FileActionsDemo.Application` project. + +```csharp +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.BlobStoring; + +namespace FileActionsDemo +{ + public class FileAppService : ApplicationService, IFileAppService + { + private readonly IBlobContainer _fileContainer; + + public FileAppService(IBlobContainer fileContainer) + { + _fileContainer = fileContainer; + } + + public async Task SaveBlobAsync(SaveBlobInputDto input) + { + await _fileContainer.SaveAsync(input.Name, input.Content, true); + } + + public async Task GetBlobAsync(GetBlobRequestDto input) + { + var blob = await _fileContainer.GetAllBytesAsync(input.Name); + + return new BlobDto + { + Name = input.Name, + Content = blob + }; + } + } +} +``` + +As you see in previous code block, we inject `IBlobContainer` to our app service. It will handle all blob actions for us. + +- `SaveBlobAsync` method uses `SaveAsync` of `IBlobContainer` to save the given blob to storage, this is a simple example so we don't check is there any file exist with same name. We sent blob name, blob content and `true` for `overrideExisting` parameter. + +- `GetBlobAsync` method is uses `GetAllBytesAsync` of `IBlobContainer` to get blob content by name. + +We finished the application layer for this project. After that we will create a `Controller` for API and `Razor Page` for UI. + +## Creating Controller + +Create `FileController.cs` in your `FileActionsDemo.HttpApi` project. + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; + +namespace FileActionsDemo +{ + public class FileController : AbpController + { + private readonly IFileAppService _fileAppService; + + public FileController(IFileAppService fileAppService) + { + _fileAppService = fileAppService; + } + + [HttpGet] + [Route("download/{fileName}")] + public async Task DownloadAsync(string fileName) + { + var fileDto = await _fileAppService.GetBlobAsync(new GetBlobRequestDto{ Name = fileName }); + + return File(fileDto.Content, "application/octet-stream", fileDto.Name); + } + } +} +``` + +As you see, `FileController` injects `IFileAppService` that we defined before. This controller has only one endpoint. + +`DownloadAsync` is using to send file from server to client. + +This endpoint is requires only a `string` parameter, then we use that parameter to get stored blob. If blob is exist, we return a `File` result so download process can start. + +## Creating User Interface + +We will create only one page to prove download and upload actions are working. + +Create folder that name `Files` in your `Pages` folder at `FileActionsDemo.Web` project. + +Create a Razor page that name `Index` with its model. + +- `Index.cshtml.cs` + +```csharp +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; + +namespace FileActionsDemo.Web.Pages.Files +{ + public class Index : AbpPageModel + { + [BindProperty] + public UploadFileDto UploadFileDto { get; set; } + + private readonly IFileAppService _fileAppService; + + public bool Uploaded { get; set; } = false; + + public Index(IFileAppService fileAppService) + { + _fileAppService = fileAppService; + } + + public void OnGet() + { + + } + + public async Task OnPostAsync() + { + using (var memoryStream = new MemoryStream()) + { + await UploadFileDto.File.CopyToAsync(memoryStream); + + await _fileAppService.SaveBlobAsync( + new SaveBlobInputDto + { + Name = UploadFileDto.Name, + Content = memoryStream.ToArray() + } + ); + } + + return Page(); + } + } + + public class UploadFileDto + { + [Required] + [Display(Name = "File")] + public IFormFile File { get; set; } + + [Required] + [Display(Name = "Filename")] + public string Name { get; set; } + } +} +``` + +As you see, we use `UploadFileDto` as a `BindProperty` and we inject `IFileAppService` to upload files. + +The `UploadFileDto` is requires a `string` parameter for using as a blob name and a `IFormFile` that sent by user. + +At the post action (`OnPostAsync`), if everything is well, we use `MemoryStream` to get all bytes from file content. + +Then we save file with `SaveBlobAsync` method of `IFileAppService`. + +- `Index.cshtml` + +```csharp +@page +@model FileActionsDemo.Web.Pages.Files.Index + +@section scripts{ + +} + + + +

    File Upload and Download

    +
    + + + +

    Upload File

    +
    +
    + + + + + +
    +
    + + +

    Download File

    +
    +
    +
    + * + +
    + + +
    +
    +
    +
    +
    +``` + +We divided the page vertically, left side will be using for upload and right side will be using for download. We use [ABP Tag Helpers](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Tag-Helpers/Index) to create page. + +- `index.js` + +```javascript +$(function () { + var DOWNLOAD_ENDPOINT = "/download"; + + var downloadForm = $("form#DownloadFile"); + + downloadForm.submit(function (event) { + event.preventDefault(); + + var fileName = $("#fileName").val().trim(); + + var downloadWindow = window.open( + DOWNLOAD_ENDPOINT + "/" + fileName, + "_blank" + ); + downloadWindow.focus(); + }); + + $("#UploadFileDto_File").change(function () { + var fileName = $(this)[0].files[0].name; + + $("#UploadFileDto_Name").val(fileName); + }); +}); +``` + +This jQuery codes are using for download. Also we wrote a simple code to autofill `Filename` input when user selects a file. + +After creating razor page and js file, `FileActionsDemo.Web` project should be like as following image. + +![web-project](web-project.png) + +## Result + +After completing code tutorial, run `FileActionsDemo.Web` project and go `/Files`. You can upload any file with any name and also download those uploaded files. + +![file-up](file-upload-result.gif) diff --git a/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/application-contracts-project.png b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/application-contracts-project.png new file mode 100644 index 0000000000..924adab6f6 Binary files /dev/null and b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/application-contracts-project.png differ diff --git a/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/file-upload-result.gif b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/file-upload-result.gif new file mode 100644 index 0000000000..e59ee890c6 Binary files /dev/null and b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/file-upload-result.gif differ diff --git a/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/initial-project.png b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/initial-project.png new file mode 100644 index 0000000000..0e611deebe Binary files /dev/null and b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/initial-project.png differ diff --git a/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/web-project.png b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/web-project.png new file mode 100644 index 0000000000..f22d2dbdcb Binary files /dev/null and b/docs/en/Community-Articles/2020-07-21-File-Upload-Download-With-BLOB-Storage-System-in-ASPNET-Core-ABP-Framework/web-project.png differ diff --git a/docs/en/Contribution/Index.md b/docs/en/Contribution/Index.md index 871ec0b985..83ec0bf05d 100644 --- a/docs/en/Contribution/Index.md +++ b/docs/en/Contribution/Index.md @@ -1,8 +1,12 @@ -## Contribution Guide +# Contribution Guide ABP is an [open source](https://github.com/abpframework) and community driven project. This guide is aims to help anyone wants to contribute to the project. -### Code Contribution +## community.abp.io + +If you want to write articles or "how to" guides related to the ABP Framework and ASP.NET Core, please submit your article to the [community.abp.io](https://community.abp.io/) web site. + +## Code Contribution You can always send pull requests to the Github repository. @@ -12,15 +16,15 @@ You can always send pull requests to the Github repository. Before making any change, please discuss it on the [Github issues](https://github.com/abpframework/abp/issues). In this way, no other developer will work on the same issue and your PR will have a better chance to be accepted. -#### Bug Fixes & Enhancements +### Bug Fixes & Enhancements You may want to fix a known bug or work on a planned enhancement. See [the issue list](https://github.com/abpframework/abp/issues) on Github. -#### Feature Requests +### Feature Requests If you have a feature idea for the framework or modules, [create an issue](https://github.com/abpframework/abp/issues/new) on Github or attend to an existing discussion. Then you can implement it if it's embraced by the community. -### Document Translation +## Document Translation You may want to translate the complete [documentation](https://abp.io/documents/) (including this one) to your mother language. If so, follow these steps: @@ -37,13 +41,13 @@ There are some fundamental documents need to be translated before publishing a l A new language is published after these minimum translations have been completed. -### Resource Localization +## Resource Localization ABP framework has a flexible [localization system](../Localization.md). You can create localized user interfaces for your own application. In addition to that, the framework and the [pre-build modules](https://docs.abp.io/en/abp/latest/Modules/Index) have localized texts. As an example, see [the localization texts for the Volo.Abp.UI package](https://github.com/abpframework/abp/blob/master/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/en.json). -#### Using the "abp translate" command +### Using the "abp translate" command This is the recommended approach, since it automatically finds all missing texts for a specific culture and lets you to translate in one place. @@ -54,14 +58,10 @@ This is the recommended approach, since it automatically finds all missing texts * Once you done the translation, use `abp translate -a` command to apply changes to the related files. * Send a pull request on GitHub. -#### Manual Translation +### Manual Translation If you want to make a change on a specific resource file, you can find the file yourself, make the necessary change (or create a new file for your language) and send a pull request on GitHub. -### Blog Posts & Tutorials - -If you decide to create some tutorials or blog posts on ABP, please inform us (by creating a [Github issue](https://github.com/abpframework/abp/issues)), so we may add a link to your tutorial/post in the official documentation and we can announce it on our [Twitter account](https://twitter.com/abpframework). - -### Bug Report +## Bug Report If you find any bug, please [create an issue on the Github repository](https://github.com/abpframework/abp/issues/new). diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index 00e791a94e..d59f9f96c7 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -2,7 +2,7 @@ This document begins by **introducing the default structure** provided by [the application startup template](Startup-Templates/Application.md) and **discusses various scenarios** you may want to implement for your own application. -> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Index.md). +> This document is for who want to fully understand and customize the database structure comes with [the application startup template](Startup-Templates/Application.md). If you simply want to create entities and manage your code first migrations, just follow [the startup tutorials](Tutorials/Part-1.md). ### Source Code diff --git a/docs/en/How-To/Index.md b/docs/en/How-To/Index.md deleted file mode 100644 index da209d4bcb..0000000000 --- a/docs/en/How-To/Index.md +++ /dev/null @@ -1,9 +0,0 @@ -# "How To" Guides - -This section contains "how to" guides for some specific questions frequently asked. While some of them are common development tasks and not directly related to the ABP Framework, we think it is useful to have some concrete examples those directly work with your ABP based applications. - -## Authentication - -* [How to Customize the Login Page for MVC / Razor Page Applications](Customize-Login-Page-MVC.md) -* [How to Use the Azure Active Directory Authentication for MVC / Razor Page Applications](Azure-Active-Directory-Authentication-MVC.md) -* [How to Customize the SignIn Manager for ABP Applications](Customize-SignIn-Manager.md) \ No newline at end of file diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index b8f678dc48..e788a4c0f8 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -613,7 +613,7 @@ Configure(options => The `Index` is automatically created after the application starts if the `Index` does not exist. `DefaultElasticClientProvider` is responsible for creating `IElasticClient`. By default, it reads Elastic Search's `Url` from `IConfiguration`. -If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](Dependency-Injection.md) system. +If your `IElasticClient` needs additional configuration, please use override `IElasticClientProvider` service and replace it in the [dependency injection](../Dependency-Injection.md) system. ``` { diff --git a/docs/en/Multi-Tenancy.md b/docs/en/Multi-Tenancy.md index 77ca49961b..901a5c5cff 100644 --- a/docs/en/Multi-Tenancy.md +++ b/docs/en/Multi-Tenancy.md @@ -355,7 +355,7 @@ namespace MyCompany.MyProject } ```` -{0} is the the placeholder to determine current tenant's unique name. +{0} is the placeholder to determine current tenant's unique name. Instead of ``options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));`` you can use this shortcut: diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index af76bb14ff..7495b8f006 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -437,7 +437,11 @@ See the [Testing Overview](https://reactjs.org/docs/testing.html) document. * [Formik](https://github.com/jaredpalmer/formik) is used to build forms. * [Yup](https://github.com/jquense/yup) is used for form validations. +## Social / External Logins + +If you want to configure social/external logins for your application, please follow the [Social/External Logins](../Authentication/Social-External-Logins.md) document. + ## What's Next? -- [The getting started document](../Getting-Started-With-Startup-Templates.md) explains how to create a new application in a few minutes. -- [The application development tutorial](../Tutorials/Part-1) explains step by step application development. +- [The getting started document](../Getting-Started.md) explains how to create a new application in a few minutes. +- [The application development tutorial](../Tutorials/Part-1.md) explains step by step application development. diff --git a/docs/en/Startup-Templates/Module.md b/docs/en/Startup-Templates/Module.md index 45e123971b..ea2fe5acca 100644 --- a/docs/en/Startup-Templates/Module.md +++ b/docs/en/Startup-Templates/Module.md @@ -56,7 +56,7 @@ An `IssueType` enum and an `IssueConsts` class (which may have some constant fie ### .Domain Project -This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), [value types](../Value-Types.md), [repository interfaces](../Repositories.md) and other domain objects. +This is the domain layer of the solution. It mainly contains [entities, aggregate roots](../Entities.md), [domain services](../Domain-Services.md), value types, [repository interfaces](../Repositories.md) and other domain objects. An `Issue` entity, an `IssueManager` domain service and an `IIssueRepository` interface are good candidates for this project. diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md index 11b608b0eb..41c57181e4 100644 --- a/docs/en/Tutorials/Part-1.md +++ b/docs/en/Tutorials/Part-1.md @@ -92,7 +92,7 @@ namespace Acme.BookStore.Books ### BookType Enum -The `Book` entity uses the `BookType` enum. Create the `BookType` in the `Acme.BookStore.Domain.Shared` project: +The `Book` entity uses the `BookType` enum. Create a `Books` folder (namespace) in the `Acme.BookStore.Domain.Shared` project and add a `BookType` inside it: ````csharp namespace Acme.BookStore.Books @@ -233,32 +233,30 @@ namespace Acme.BookStore public async Task SeedAsync(DataSeedContext context) { - if (await _bookRepository.GetCountAsync() > 0) + if (await _bookRepository.GetCountAsync() <= 0) { - return; + await _bookRepository.InsertAsync( + new Book + { + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } - - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); } } } @@ -295,7 +293,7 @@ In this section, you will create an application service to get, create, update a ### BookDto -`CrudAppService` base class requires to define the fundamental DTOs for the entity. Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: +`CrudAppService` base class requires to define the fundamental DTOs for the entity. Create a `Books` folder (namespace) in the `Acme.BookStore.Application.Contracts` project and add a `BookDto` class inside it: ````csharp using System; @@ -342,7 +340,7 @@ namespace Acme.BookStore ### CreateUpdateBookDto -Create another DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: +Create a `CreateUpdateBookDto` class in the `Books` folder (namespace) of the `Acme.BookStore.Application.Contracts` project: ````csharp using System; @@ -393,7 +391,7 @@ namespace Acme.BookStore ### IBookAppService -Next step is to define an interface for the application service. Create an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: +Next step is to define an interface for the application service. Create an `IBookAppService` interface in the `Books` folder (namespace) of the `Acme.BookStore.Application.Contracts` project: ````csharp using System; @@ -420,7 +418,7 @@ namespace Acme.BookStore.Books ### BookAppService -Implement the `IBookAppService`, as named `BookAppService`, in the `Acme.BookStore.Application` project: +It is time to implement the `IBookAppService` interface. Create a new class, named `BookAppService` in the `Books` namespace (folder) of the Acme.BookStore.Application project: ````csharp using System; diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md index fbecf39b86..0fa4892298 100644 --- a/docs/en/Tutorials/Part-10.md +++ b/docs/en/Tutorials/Part-10.md @@ -80,7 +80,7 @@ This is a typical migration problem and the decision depends on your case; * You can do it programmatically on data migration or seed phase. * You can manually handle it on the database. -We prefer to delete the database {{if DB=="EF"}}(run the `Drop-Database` in the *Package Manager Console*){{end}} since this is just an example project and data loss is not important. Since this topic is not related to the ABP Framework, we don't go deeper for all the scenarios. +We prefer to **delete the database** {{if DB=="EF"}}(run the `Drop-Database` in the *Package Manager Console*){{end}} since this is just an example project and data loss is not important. Since this topic is not related to the ABP Framework, we don't go deeper for all the scenarios. {{if DB=="EF"}} diff --git a/docs/en/Tutorials/Part-4.md b/docs/en/Tutorials/Part-4.md index acbce65c3e..1a82afa924 100644 --- a/docs/en/Tutorials/Part-4.md +++ b/docs/en/Tutorials/Part-4.md @@ -78,7 +78,7 @@ If you had created a data seed contributor as described in the [first part](Part ## Testing the BookAppService -Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: +Add a new test class, named `BookAppService_Tests` in the `Books` namespace (folder) of the `Acme.BookStore.Application.Tests` project: ````csharp using System.Threading.Tasks; diff --git a/docs/en/Tutorials/Part-5.md b/docs/en/Tutorials/Part-5.md index 5674cf83d6..c1deb21cab 100644 --- a/docs/en/Tutorials/Part-5.md +++ b/docs/en/Tutorials/Part-5.md @@ -58,7 +58,7 @@ ABP Framework provides an [authorization system](../Authorization.md) based on t A permission must have a unique name (a `string`). The best way is to define it as a `const`, so we can reuse the permission name. -Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project and change the content as shown below: +Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: ````csharp namespace Acme.BookStore.Permissions @@ -84,7 +84,7 @@ This is a hierarchical way of defining permission names. For example, "create bo You should define permissions before using them. -Open the `BookStorePermissionDefinitionProvider` class inside the `Acme.BookStore.Application.Contracts` project and change the content as shown below: +Open the `BookStorePermissionDefinitionProvider` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: ````csharp using Acme.BookStore.Localization; diff --git a/docs/en/Tutorials/Part-6.md b/docs/en/Tutorials/Part-6.md index 1bb07007b6..b1b38f18dc 100644 --- a/docs/en/Tutorials/Part-6.md +++ b/docs/en/Tutorials/Part-6.md @@ -204,7 +204,7 @@ namespace Acme.BookStore.Authors > **DDD tip**: Do not introduce domain service methods unless they are really needed and perform some core business rules. For this case, we needed to this service to be able to force the unique name constraint. -Both methods checks if there is already an author with the given name and throws a special business exception, `AuthorAlreadyExistsException`, defined in the `Acme.BookStore.Domain` project as shown below: +Both methods checks if there is already an author with the given name and throws a special business exception, `AuthorAlreadyExistsException`, defined in the `Acme.BookStore.Domain` project (in the `Authors` folder) as shown below: ````csharp using Volo.Abp; @@ -242,6 +242,15 @@ This is a unique string represents the error code thrown by your application and "BookStore:00001": "There is already an author with the same name: {name}" ```` +Then open the `BookStoreDomainSharedModule` and add the following code block inside the `ConfigureServices` method: + +````csharp +Configure(options => +{ + options.MapCodeNamespace("BookStore", typeof(BookStoreResource)); +}); +```` + Whenever you throw an `AuthorAlreadyExistsException`, the end use will see a nice error message on the UI. ## IAuthorRepository diff --git a/docs/en/Tutorials/Part-8.md b/docs/en/Tutorials/Part-8.md index 38e5b6013f..fc10a799f2 100644 --- a/docs/en/Tutorials/Part-8.md +++ b/docs/en/Tutorials/Part-8.md @@ -339,7 +339,7 @@ public async Task DeleteAsync(Guid id) You can't compile the code since it is expecting some constants declared in the `BookStorePermissions` class. -Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project and change the content as shown below: +Open the `BookStorePermissions` class inside the `Acme.BookStore.Application.Contracts` project (in the `Permissions` folder) and change the content as shown below: ````csharp namespace Acme.BookStore.Permissions @@ -439,55 +439,66 @@ namespace Acme.BookStore public async Task SeedAsync(DataSeedContext context) { - if (await _bookRepository.GetCountAsync() > 0) + if (await _bookRepository.GetCountAsync() <= 0) { - return; + await _bookRepository.InsertAsync( + new Book + { + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } // ADDED SEED DATA FOR AUTHORS - - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "George Orwell", - new DateTime(1903, 06, 25), - "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." - ) - ); - - await _authorRepository.InsertAsync( - await _authorManager.CreateAsync( - "Douglas Adams", - new DateTime(1952, 03, 11), - "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." - ) - ); - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - }, - autoSave: true - ); + if (await _authorRepository.GetCountAsync() <= 0) + { + await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "George Orwell", + new DateTime(1903, 06, 25), + "Orwell produced literary criticism and poetry, fiction and polemical journalism; and is best known for the allegorical novella Animal Farm (1945) and the dystopian novel Nineteen Eighty-Four (1949)." + ) + ); - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - }, - autoSave: true - ); + await _authorRepository.InsertAsync( + await _authorManager.CreateAsync( + "Douglas Adams", + new DateTime(1952, 03, 11), + "Douglas Adams was an English author, screenwriter, essayist, humorist, satirist and dramatist. Adams was an advocate for environmentalism and conservation, a lover of fast cars, technological innovation and the Apple Macintosh, and a self-proclaimed 'radical atheist'." + ) + ); + } } } } ```` +{{if DB=="EF"}} + +You can now run the `.DbMigrator` console application to **migrate** the **database schema** and **seed** the initial data. + +{{else if DB=="Mongo"}} + +You can now run the `.DbMigrator` console application to **seed** the initial data. + +{{end}} + ## Testing the Author Application Service Finally, we can write some tests for the `IAuthorAppService`. Add a new class, named `AuthorAppService_Tests` in the `Authors` namespace (folder) of the `Acme.BookStore.Application.Tests` project: diff --git a/docs/en/UI/Angular/Localization.md b/docs/en/UI/Angular/Localization.md index 1ee68ace82..eec4db0cb4 100644 --- a/docs/en/UI/Angular/Localization.md +++ b/docs/en/UI/Angular/Localization.md @@ -199,7 +199,7 @@ Some of the culture names defined in .NET do not match Angular locales. In such ![locale-error](./images/locale-error.png) -If you see an error like this, you should pass the `cultureNameToLocaleFileNameMapping` property like below to CoreModule's forRoot static method. +If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to CoreModule's forRoot static method. ```js // app.module.ts @@ -209,7 +209,7 @@ If you see an error like this, you should pass the `cultureNameToLocaleFileNameM // other imports CoreModule.forRoot({ // other options - cultureNameToLocaleFileNameMapping: { + cultureNameLocaleFileMap: { "DotnetCultureName": "AngularLocaleFileName", "pt-BR": "pt" // example } diff --git a/docs/en/UI/Angular/Multi-Tenancy.md b/docs/en/UI/Angular/Multi-Tenancy.md new file mode 100644 index 0000000000..94b3954a03 --- /dev/null +++ b/docs/en/UI/Angular/Multi-Tenancy.md @@ -0,0 +1,135 @@ +# Multi Tenancy in Angular UI + +ABP Angular UI supports the multi-tenancy. The following features related to multi-tenancy are available in the startup templates. + + +![Tenants Page](./images/tenants-page.png) +

    Tenants page

    + +On the page above, you can; + +- See the all tenants. +- Create a new tenant. +- Edit an existing tenant. +- Delete a tenant. + + +![Tenant Switching Component](./images/tenant-switching-box.png) +

    Tenant Switching Component

    + +You can switch between existing tenants by using the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page). Angular UI sends the selected tenant id sends to the backend as `__tenant` header on each request. + + +## Domain Tenant Resolver + +Angular UI can get the tenant name from the app running URL. You can determine the current tenant by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). To do this, you need to set the `application.baseUrl` property in the environment: + +Subdomain resolver: + +```js +// environment.prod.ts + +export const environment = { + //... + application: { + baseUrl: 'https://{0}.mydomain.com/' + }, + //... +} +``` + +**{0}** is the placeholder to determine current tenant's unique name. + +After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get the tenant name as **mytenant1**. Then, the app will call the `/api/abp/multi-tenancy/tenants/by-name/mytenant1` endpoint to check if the tenant exists. If the tenant (mytenant1) exists, the app will keep this tenant data and send its `id` as `__tenant` header to the backend on each request. If the tenant does not exist, the app will not send `__tenant` header to the backend. + +> **Important Note:** If you define the `baseUrl` with the placeholder (**{0}**), the tenant switching component in the child pages of the `AccountLayoutComponent` (like Login page) will be hidden. + + +Domain resolver: + +```js +// environment.prod.ts + +export const environment = { + //... + application: { + baseUrl: 'https://{0}.com/' + }, + //... +} +``` + +After the configuration above, if your app runs on the `mytenant.com`, the app will get the tenant name as **mytenant**. + +### Tenant Specific Remote Endpoints + +The **{0}** placeholder can be put to the API URLs in the environment to determine tenant specific endpoints. + +```js +// environment.prod.ts + +export const environment = { + //... + application: { + baseUrl: 'https://{0}.mydomain.com/', + //... + }, + oAuthConfig: { + issuer: 'https://{0}.ids.mydomain.com', + //... + }, + apis: { + default: { + url: 'https://{0}.api.mydomain.com', + }, + AbpIdentity: { + url: 'https://{0}.identity.mydomain.com', + }, + }, +} +``` + +> **Important Note:** The `application.baseUrl` and the `{0}` placeholder in the value of the `baseUrl` property are required to be able to get tenant from running URL. Other placeholders in API URLs are optional. + +After the configuration above, if your app runs on the `mytenant1.mydomain.com`, the app will get tenant name as **mytenant1** and replace the environment object in `ConfigState` on app initialization as follows: + + +```js +// environment object in ConfigState + +{ + //... + application: { + baseUrl: 'https://mytenant1.mydomain.com/', + //... + }, + oAuthConfig: { + issuer: 'https://mytenant1.ids.mydomain.com', + //... + }, + apis: { + default: { + url: 'https://mytenant1.api.mydomain.com', + }, + AbpIdentity: { + url: 'https://mytenant1.identity.mydomain.com', + }, + }, +} +``` + +After this replacement, the app will use the following URLs: + +- `https://mytenant1.ids.mydomain.com` as IdentityServer URL. +- `https://mytenant1.api.mydomain.com` as default URL. +- `https://mytenant1.identity.mydomain.com` as `AbpIdentity` remote endpoint URL. + +The app sends the `__tenant` header that contains the current tenant id on each request. + +## See Also + +* [Multi Tenancy in ABP](../../Multi-Tenancy.md) + +## What's Next? + +- [Confirmation Popup](./Confirmation-Service.md) diff --git a/docs/en/UI/Angular/Permission-Management.md b/docs/en/UI/Angular/Permission-Management.md index 61169c85b8..8cc7325afd 100644 --- a/docs/en/UI/Angular/Permission-Management.md +++ b/docs/en/UI/Angular/Permission-Management.md @@ -78,4 +78,4 @@ Granted Policies are stored in the `auth` property of `ConfigState`. ## What's Next? -- [Confirmation Popup](./Confirmation-Service.md) \ No newline at end of file +* [Multi Tenancy](./Multi-Tenancy.md) \ No newline at end of file diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md index b194811d9a..faa5632ab8 100644 --- a/docs/en/UI/Angular/Toaster-Service.md +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -26,7 +26,7 @@ You can use the `success`, `warn`, `error`, and `info` methods of `ToasterServic ### How to Display a Toast Overlay ```js -this.toast.success('Message', 'Title'); +this.toaster.success('Message', 'Title'); ``` - The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`. @@ -72,17 +72,18 @@ With the options above, the toast overlay looks like this: The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast: ```js -const toastId = this.toast.success('Message', 'Title') +const toastId = this.toaster.success('Message', 'Title') -this.toast.remove(toastId); +this.toaster.remove(toastId); ``` -### How to Remove All Toasts + +### How to Remove All Toasts The all open toasts can be removed manually via the `clear` method: ```js -this.toast.clear(); +this.toaster.clear(); ``` ## API diff --git a/docs/en/UI/Angular/images/tenant-switching-box.png b/docs/en/UI/Angular/images/tenant-switching-box.png new file mode 100644 index 0000000000..ed4d299c55 Binary files /dev/null and b/docs/en/UI/Angular/images/tenant-switching-box.png differ diff --git a/docs/en/UI/Angular/images/tenants-page.png b/docs/en/UI/Angular/images/tenants-page.png new file mode 100644 index 0000000000..8882d58d2f Binary files /dev/null and b/docs/en/UI/Angular/images/tenants-page.png differ diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 09288e013b..07eed15dca 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -93,10 +93,6 @@ } ] }, - { - "text": "\"How to\" Guides", - "path": "How-To/Index.md" - }, { "text": "Migrating from the ASP.NET Boilerplate", "path": "AspNet-Boilerplate-Migration-Guide.md" @@ -107,6 +103,15 @@ "text": "CLI", "path": "CLI.md" }, + { + "text": "Authentication", + "items": [ + { + "text": "Social/External Logins", + "path": "Authentication/Social-External-Logins.md" + } + ] + }, { "text": "Fundamentals", "items": [ @@ -253,7 +258,7 @@ "path": "Blob-Storing-Custom-Provider.md" } ] - } + } ] }, { @@ -426,6 +431,10 @@ "text": "Permission Management", "path": "UI/Angular/Permission-Management.md" }, + { + "text": "Multi Tenancy", + "path": "UI/Angular/Multi-Tenancy.md" + }, { "text": "Confirmation Popup", "path": "UI/Angular/Confirmation-Service.md" diff --git a/docs/zh-Hans/API/Application-Configuration.md b/docs/zh-Hans/API/Application-Configuration.md index ba062d9cef..50e74d5bd7 100644 --- a/docs/zh-Hans/API/Application-Configuration.md +++ b/docs/zh-Hans/API/Application-Configuration.md @@ -2,12 +2,12 @@ ABP框架提供了一个预构建的标准端点,其中包含一些有关应用程序/服务的有用信息. 这里是此端点的一些基本信息的列表: -* [本地化](Localization.md)值, 支持应用程序的当前语言. -* 当前用户可用和已授予的[策略](Authorization.md)(权限). -* 当前用户的[设置](Settings.md)值. -* 关于[当前用户](CurrentUser.md)的信息 (如 id 和用户名). -* 关于当前[租户](Multi-Tenancy.md)的信息 (如 id 和名称). -* 当前用户的[时区](Timing.md)信息和应用程序的[时钟](Timing.md)类型. +* [本地化](../Localization.md)值, 支持应用程序的当前语言. +* 当前用户可用和已授予的[策略](../Authorization.md)(权限). +* 当前用户的[设置](../Settings.md)值. +* 关于[当前用户](../CurrentUser.md)的信息 (如 id 和用户名). +* 关于当前[租户](../Multi-Tenancy.md)的信息 (如 id 和名称). +* 当前用户的[时区](../Timing.md)信息和应用程序的[时钟](../Timing.md)类型. ## HTTP API diff --git a/docs/zh-Hans/Authentication/Social-External-Logins.md b/docs/zh-Hans/Authentication/Social-External-Logins.md new file mode 100644 index 0000000000..3d599a962d --- /dev/null +++ b/docs/zh-Hans/Authentication/Social-External-Logins.md @@ -0,0 +1,30 @@ +# 社交/外部登录 + +## ASP.NET Core MVC / Razor Pages UI + +[帐户模块](../Modules/Account.md)已配置为开箱即用的处理社交或外部登录. 你可以按照ASP.NET Core文档向你的应用程序添加社交/外部登录提供程序. + +### 示例: Facebook 认证 + +按照[ASP.NET Core Facebook集成文档](https://docs.microsoft.com/zh-cn/aspnet/core/security/authentication/social/facebook-logins)向你应用程序添加Facebook登录. + +#### 添加NuGet包 + +添加[Microsoft.AspNetCore.Authentication.Facebook]包到你的项目. 基于你的架构,可能是 `.Web`,`.IdentityServer`(对于分层启动)或 `.Host` 项目. + +#### 配置提供程序 + +在你模块的 `ConfigureServices` 方法中使用 `.AddFacebook(...)` 扩展方法来配置客户端: + +````csharp +context.Services.AddAuthentication() + .AddFacebook(facebook => + { + facebook.AppId = "..."; + facebook.AppSecret = "..."; + facebook.Scope.Add("email"); + facebook.Scope.Add("public_profile"); + }); +```` + +> 最佳实践是使用 `appsettings.json` 或ASP.NET Core用户机密系统来存储你的凭据,而不是像这样硬编码值. 请参阅[微软](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/social/facebook-logins)文档了解如何使用用户机密. \ No newline at end of file diff --git a/docs/zh-Hans/Authorization.md b/docs/zh-Hans/Authorization.md index 2c269ab5fa..96caae1404 100644 --- a/docs/zh-Hans/Authorization.md +++ b/docs/zh-Hans/Authorization.md @@ -285,8 +285,6 @@ public async Task CreateAsync(CreateAuthorDto input) abp.auth.isGranted('MyPermissionName'); ```` -参阅 [abp.auth](UI/AspNetCore/JavaScript-API/Index.md) API 文档了解详情. - ## 权限管理 通常权限管理是管理员用户使用权限管理模态框进行授权: diff --git a/docs/zh-Hans/Background-Workers-Quartz.md b/docs/zh-Hans/Background-Workers-Quartz.md index 196d0e1d07..9c4a0721e4 100644 --- a/docs/zh-Hans/Background-Workers-Quartz.md +++ b/docs/zh-Hans/Background-Workers-Quartz.md @@ -36,6 +36,9 @@ public class YourModule : AbpModule } ```` +> Quartz后台工作者集成提供了 `QuartzPeriodicBackgroundWorkerAdapter` 来适配 `PeriodicBackgroundWorkerBase` 和 `AsyncPeriodicBackgroundWorkerBase` 派生类. 所以你依然可以按照[后台工作者文档](Background-Workers.md)来定义后台作业. +> `BackgroundJobWorker` 每5秒检查待执行作业,但是长时间的作业不会阻塞quartz. 所以安装Quartz后台工作者集成后,你同时需要安装[Quartz后台作业](Background-Jobs-Quartz.md)或[Hangfire后台作业](Background-Jobs-Hangfire.md)以避免重复执行作业. + ## 配置 参阅[配置](Background-Jobs-Quartz.md#配置). diff --git a/docs/zh-Hans/Best-Practices/Application-Services.md b/docs/zh-Hans/Best-Practices/Application-Services.md index 6c681edde1..82a5dc8aba 100644 --- a/docs/zh-Hans/Best-Practices/Application-Services.md +++ b/docs/zh-Hans/Best-Practices/Application-Services.md @@ -203,7 +203,7 @@ Task VoteAsync(Guid id, VoteType type); #### 额外的属性 -* **推荐** 使用 `MapExtraPropertiesTo` 扩展方法 ([参阅](Object-Extensions.md)) 或配置对象映射 (`MapExtraProperties`) 以允许应用开发人员能够扩展对象和服务. +* **推荐** 使用 `MapExtraPropertiesTo` 扩展方法 ([参阅](../Object-Extensions.md)) 或配置对象映射 (`MapExtraProperties`) 以允许应用开发人员能够扩展对象和服务. #### 操作/删除 实体 diff --git a/docs/zh-Hans/Blob-Storing-Aliyun.md b/docs/zh-Hans/Blob-Storing-Aliyun.md index ba66d55104..ded28fbc93 100644 --- a/docs/zh-Hans/Blob-Storing-Aliyun.md +++ b/docs/zh-Hans/Blob-Storing-Aliyun.md @@ -23,18 +23,21 @@ BLOB存储Aliyun提供程序可以将BLOB存储在[Aliyun Blob storage](https:// ````csharp Configure(options => { - options.Containerscontainer.UseAliyun(aliyun => + options.Containers.ConfigureDefault(container => { - aliyun.AccessKeyId = "your aliyun access key id"; - aliyun.AccessKeySecret = "your aliyun access key secret"; - aliyun.Endpoint = "your oss endpoint"; - aliyun.RegionId = "your sts region id"; - aliyun.RoleArn = "the arn of ram role"; - aliyun.RoleSessionName = "the name of the certificate"; - aliyun.Policy = "policy"; - aliyun.DurationSeconds = "expiration date"; - aliyun.ContainerName = "your aliyun container name"; - aliyun.CreateContainerIfNotExists = false; + container.UseAliyun(aliyun => + { + aliyun.AccessKeyId = "your aliyun access key id"; + aliyun.AccessKeySecret = "your aliyun access key secret"; + aliyun.Endpoint = "your oss endpoint"; + aliyun.RegionId = "your sts region id"; + aliyun.RoleArn = "the arn of ram role"; + aliyun.RoleSessionName = "the name of the certificate"; + aliyun.Policy = "policy"; + aliyun.DurationSeconds = "expiration date"; + aliyun.ContainerName = "your aliyun container name"; + aliyun.CreateContainerIfNotExists = false; + }); }); }); ```` diff --git a/docs/zh-Hans/Blob-Storing-Aws.md b/docs/zh-Hans/Blob-Storing-Aws.md index 6d50acce9d..0ecb59bbf5 100644 --- a/docs/zh-Hans/Blob-Storing-Aws.md +++ b/docs/zh-Hans/Blob-Storing-Aws.md @@ -23,21 +23,24 @@ BLOB存储Aws提供程序可以将BLOB存储在[Amazon Simple Storage Service](h ````csharp Configure(options => { - options.Containerscontainer.UseAws(Aws => + options.Containers.ConfigureDefault(container => { - Aws.AccessKeyId = "your Aws access key id"; - Aws.SecretAccessKey = "your Aws access key secret"; - Aws.UseCredentials = "set true to use credentials"; - Aws.UseTemporaryCredentials = "set true to use temporary credentials"; - Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; - Aws.ProfileName = "the name of the profile to get credentials from"; - Aws.ProfilesLocation = "the path to the aws credentials file to look at"; - Aws.Region = "the system name of the service"; - Aws.Name = "the name of the federated user"; - Aws.Policy = "policy"; - Aws.DurationSeconds = "expiration date"; - Aws.ContainerName = "your Aws container name"; - Aws.CreateContainerIfNotExists = false; + container.UseAws(Aws => + { + Aws.AccessKeyId = "your Aws access key id"; + Aws.SecretAccessKey = "your Aws access key secret"; + Aws.UseCredentials = "set true to use credentials"; + Aws.UseTemporaryCredentials = "set true to use temporary credentials"; + Aws.UseTemporaryFederatedCredentials = "set true to use temporary federated credentials"; + Aws.ProfileName = "the name of the profile to get credentials from"; + Aws.ProfilesLocation = "the path to the aws credentials file to look at"; + Aws.Region = "the system name of the service"; + Aws.Name = "the name of the federated user"; + Aws.Policy = "policy"; + Aws.DurationSeconds = "expiration date"; + Aws.ContainerName = "your Aws container name"; + Aws.CreateContainerIfNotExists = false; + }); }); }); ```` diff --git a/docs/zh-Hans/Blob-Storing-Azure.md b/docs/zh-Hans/Blob-Storing-Azure.md index 892d299fc0..b6f54cd72c 100644 --- a/docs/zh-Hans/Blob-Storing-Azure.md +++ b/docs/zh-Hans/Blob-Storing-Azure.md @@ -23,11 +23,14 @@ BLOB存储Azure提供程序可以将BLOB存储在[Azure Blob storage](https://az ````csharp Configure(options => { - options.Containerscontainer.UseAzure(azure => + options.Containers.ConfigureDefault(container => { - azure.ConnectionString = "your azure connection string"; - azure.ContainerName = "your azure container name"; - azure.CreateContainerIfNotExists = false; + container.UseAzure(azure => + { + azure.ConnectionString = "your azure connection string"; + azure.ContainerName = "your azure container name"; + azure.CreateContainerIfNotExists = false; + }); }); }); ```` diff --git a/docs/zh-Hans/Blob-Storing-Database.md b/docs/zh-Hans/Blob-Storing-Database.md index e45c79d179..cc688e4396 100644 --- a/docs/zh-Hans/Blob-Storing-Database.md +++ b/docs/zh-Hans/Blob-Storing-Database.md @@ -27,7 +27,7 @@ abp add-module Volo.Abp.BlobStoring.Database 这里是此提供程序定义的所有包: -* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Domain.Shared) +* [Volo.Abp.BlobStoring.Database.Domain.Shared](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain.Shared) * [Volo.Abp.BlobStoring.Database.Domain](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.Domain) * [Volo.Abp.BlobStoring.Database.EntityFrameworkCore](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.EntityFrameworkCore) * [Volo.Abp.BlobStoring.Database.MongoDB](https://www.nuget.org/packages/Volo.Abp.BlobStoring.Database.MongoDB) diff --git a/docs/zh-Hans/Blob-Storing-Minio.md b/docs/zh-Hans/Blob-Storing-Minio.md index a2974e0843..3c4bec721d 100644 --- a/docs/zh-Hans/Blob-Storing-Minio.md +++ b/docs/zh-Hans/Blob-Storing-Minio.md @@ -23,12 +23,15 @@ BLOB Storing Minio提供程序帮助你存储对象到 [MinIO Object storage](ht ````csharp Configure(options => { - options.Containerscontainer.UseMinio(minio => - { - minio.EndPoint = "你的 minio endPoint"; - minio.AccessKey = "你的 minio accessKey"; - minio.SecretKey = "你的 minio secretKey"; - minio.BucketName = "你的 minio bucketName"; + options.Containers.ConfigureDefault(container => + { + container.UseMinio(minio => + { + minio.EndPoint = "你的 minio endPoint"; + minio.AccessKey = "你的 minio accessKey"; + minio.SecretKey = "你的 minio secretKey"; + minio.BucketName = "你的 minio bucketName"; + }); }); }); ```` diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index 77f5ac5cd7..876d98721b 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -3,7 +3,7 @@ 本文首先介绍[应用程序启动模板](Startup-Templates/Application.md)提供的**默认结构**,并讨论你可能希望为自己的应用程序实现的**各种场景**. -> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Index.md). +> 本文档适用于希望完全理解和自定义[应用程序启动模板](Startup-Templates/Application.md)附带的数据库结构的人员. 如果你只是想创建实体和管理代码优先(code first)迁移,只需要遵循[启动教程](Tutorials/Part-1.md). ### 源码 diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index 63759d1462..acc956be1b 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -611,7 +611,7 @@ Configure(options => 应用程序启动后如果`Index`不存在则会自动创建`Index`. `DefaultElasticClientProvider`负责创建`IElasticClient`, 默认情况下它会从`IConfiguration`中读取Elastic Search的`Url`. -如果你的IElasticClient需要其它配置请使用重写IElasticClientProvider服务并在依赖注入系统中替换它. +如果你的 `IElasticClient` 需要其它配置请使用重写 `IElasticClientProvider` 服务并在[依赖注入](../Dependency-Injection.md)系统中替换它. ``` { "ElasticSearch": { diff --git a/docs/zh-Hans/Startup-Templates/Application.md b/docs/zh-Hans/Startup-Templates/Application.md index 4e2c3f6700..44ea5adfaf 100644 --- a/docs/zh-Hans/Startup-Templates/Application.md +++ b/docs/zh-Hans/Startup-Templates/Application.md @@ -423,7 +423,11 @@ Screens 是通过在 `src/screens` 文件夹中创建将名称分开的文件夹 * [Formik](https://github.com/jaredpalmer/formik) 用于构建表单. * [Yup](https://github.com/jquense/yup) 用于表单验证. +## 社交/外部登录 + +如果你想要为你的应用程序配置社交/外部登录,请参阅[社交/外部登录](../Authentication/Social-External-Logins.md)文档. + ## 下一步是什么? -* 参阅[ASP.NET Core MVC 模板入门](../Getting-Started-AspNetCore-MVC-Template.md)创建此模板的新解决方案并运行它. -* 参阅[ASP.NET Core MVC 教程](../Tutorials/AspNetCore-Mvc/Part-I.md)学习使用此模板开发应用程序. +* [入门文档](../Getting-Started.md)介绍了如何在几分钟内创建新应用程序. +* [Web应用程序开发教程](../Tutorials/Part-1.md)逐步介绍了应用程序开发. diff --git a/docs/zh-Hans/Startup-Templates/Module.md b/docs/zh-Hans/Startup-Templates/Module.md index a6706ad783..8341670af4 100644 --- a/docs/zh-Hans/Startup-Templates/Module.md +++ b/docs/zh-Hans/Startup-Templates/Module.md @@ -56,7 +56,7 @@ abp new Acme.IssueManagement -t module --no-ui ### .Domain 项目 -解决方案的领域层. 它主要包含 [实体, 集合根](../Entities.md), [领域服务](../Domain-Services.md), [值类型](../Value-Types.md), [仓储接口](../Repositories.md) 和解决方案的其他领域对象. +解决方案的领域层. 它主要包含 [实体, 集合根](../Entities.md), [领域服务](../Domain-Services.md), 值类型, [仓储接口](../Repositories.md) 和解决方案的其他领域对象. 例如 `Issue` 实体, `IssueManager` 领域服务和 `IIssueRepository` 接口都适合放在这个项目中. diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md index bcf4e21b9f..101835559b 100644 --- a/docs/zh-Hans/Tutorials/Part-1.md +++ b/docs/zh-Hans/Tutorials/Part-1.md @@ -1,128 +1,75 @@ -## ASP.NET Core {{UI_Value}} 教程 - 第一章 +# Web应用程序开发教程 - 第一章: 创建服务端 ````json //[doc-params] { - "UI": ["MVC","NG"] + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] } ```` {{ if UI == "MVC" - DB="ef" - DB_Text="Entity Framework Core" UI_Text="mvc" else if UI == "NG" - DB="mongodb" - DB_Text="MongoDB" UI_Text="angular" else - DB ="?" UI_Text="?" end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end }} -### 关于本教程 - -在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的应用程序. **{{DB_Text}}**将用作ORM提供者,前端使用{{UI_Value}} 和 JavaScript. - -ASP.NET Core {{UI_Value}} 系列教程包括三个3个部分: - -- **Part-1: 创建项目和书籍列表页面(本章)** -- [Part-2: 创建,编辑,删除书籍](Part-2.md) -- [Part-3: 集成测试](Part-3.md) - -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). - -### 创建新项目 - -创建一个名为 `Acme.BookStore` 的新项目,其中 `Acme` 是公司名 `BookStore` 是项目名. 你可以参阅[入门](../Getting-Started?UI={{UI}}#run-the-application) 文档了解如何创建新项目. 我们将使用CLI创建新项目. - -#### 创建项目 - -使用以下命令创建一个新的ABP项目,使用 `{{DB_Text}}` 做为数据库提供者, UI选项使用 `{{UI_Value}}`. 其他CLI选项请参考[ABP CLI](https://docs.abp.io/en/abp/latest/CLI)文档. - -```bash -abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} --mobile none -``` - -![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) - -### 应用迁移 - -项目创建后,需要应用初始化迁移创建数据库. 运行 `Acme.BookStore.DbMigrator` 应用程序. 它会应用所有迁移,完成流程后你会看到以下结果,数据库已经准备好了! - -![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png) - -> 另外你也可以在 Visual Studio 包管理控制台运行 `Update-Database` 命令应用迁移. +## 关于本教程 -#### 初始化数据库表 +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: -![Initial database tables](./images/bookstore-database-tables-{{DB}}.png) +* **{{DB_Text}}** 做为ORM提供程序. +* **{{UI_Value}}** 做为UI框架. -### 运行应用程序 +本教程分为以下部分: -右键单击{{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} 项目**设置为启动项**. 使用 **CTRL+F5** 或 **F5** 运行应用程序. {{if UI == "NG"}}你会看到BookStore API的Swagger UI.{{end}} +- **Part 1: 创建服务端 (本章)** +- [Part 2: 图书列表页面](Part-2.md) +- [Part 3: 创建,更新和删除图书](Part-3.md) +- [Part 4: 集成测试](Part-4.md) +- [Part 5: 授权](Part-5.md) +- [Part 6: 作者: 领域层](Part-6.md) +- [Part 7: 作者: 数据库集成](Part-7.md) +- [Part 8: 作者: 应用服务层](Part-8.md) +- [Part 9: 作者: 用户页面](Part-9.md) +- [Part 10: 图书到作者的关系](Part-10.md) -更多信息,参阅[入门教程](../../Getting-Started?UI={{UI}})的运行应用程序部分. +## 下载源码 -![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) +本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: -{{if UI == "NG"}} - -在 `angular` 下打开命令行终端,执行 `yarn` 命令: - -```bash -yarn -``` +* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -所有的模块加载后,执行 `yarn start` 命令: +## 创建解决方案 -```bash -yarn start -``` - -默认网站从以下URL访问: - -http://localhost:4200/ - -如果你成功看到登录页面,可以按 `ctrl-c` 退出Angular托管.(我们稍后再运行). - -> 注意, Firefox不使用Windows凭据存储,你需要手动将自签名的开发人员证书导入到Firefox. 打开Firefox并导航到以下网址: -> -> https://localhost:44322/api/abp/application-configuration -> -> 如果你看到下图,单击 **Accept the Risk 和 Continue** 按钮绕过警告. -> -> ![Set as startup project](./images/mozilla-self-signed-cert-error.png) - -{{end}} +在开始开发之前,请按照[入门教程](../Getting-Started.md)创建名为 `Acme.BookStore` 的新解决方案. -默认的登录凭证: +## 创建Book实体 -* **Username**: admin -* **Password**: 1q2w3E* - -### 解决方案的结构 - -下面的图片展示了从启动模板创建的项目是如何分层的. - -![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png) - -> 你可以查看[应用程序模板文档](../startup-templates/application#solution-structure)以详细了解解决方案结构. - -### 创建Book实体 - -启动模板中的域层分为两个项目: +启动模板中的**领域层**分为两个项目: - `Acme.BookStore.Domain`包含你的[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities), [领域服务](https://docs.abp.io/zh-Hans/abp/latest/Domain-Services)和其他核心域对象. - `Acme.BookStore.Domain.Shared`包含可与客户共享的常量,枚举或其他域相关对象. -在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义[实体](https://docs.abp.io/zh-Hans/abp/latest/Entities). 该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个名为`Book`的类,如下所示: +在解决方案的**领域层**(`Acme.BookStore.Domain`项目)中定义你的实体. -````C# +该应用程序的主要实体是`Book`. 在`Acme.BookStore.Domain`项目中创建一个 `Books` 文件夹并在其中添加一个名为 `Book` 的类,如下所示: + +````csharp using System; using Volo.Abp.Domain.Entities.Auditing; -namespace Acme.BookStore +namespace Acme.BookStore.Books { public class Book : AuditedAggregateRoot { @@ -133,33 +80,22 @@ namespace Acme.BookStore public DateTime PublishDate { get; set; } public float Price { get; set; } - - protected Book() - { - } - public Book(Guid id, string name, BookType type, DateTime publishDate, float price) - :base(id) - { - Name = name; - Type = type; - PublishDate = publishDate; - Price = price; - } } } ```` -* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**域驱动设计(DDD)** 概念之一. 有关详细信息和最佳做法,请参阅[实体文档](https://docs.abp.io/zh-Hans/abp/latest/Entities). -* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). +* ABP为实体提供了两个基本的基类: `AggregateRoot`和`Entity`. **Aggregate Root**是**[领域驱动设计](./Domain-Driven-Design.md)** 概念之一. 可以视为直接查询和处理的根实体(请参阅[实体文档](./Entities.md)). +* `Book`实体继承了`AuditedAggregateRoot`,`AuditedAggregateRoot`类在`AggregateRoot`类的基础上添加了一些审计属性(`CreationTime`, `CreatorId`, `LastModificationTime` 等). ABP框架自动为你管理这些属性. * `Guid`是`Book`实体的主键类型. -* 使用 **数据注解** 为EF Core添加映射.或者你也可以使用 EF Core 自带的[fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling). -#### BookType枚举 +> 为了保持简单,本教程将实体属性保留为 **public get/set** . 如果你想了解关于DDD最佳实践,请参阅[实体文档](../Entities.md). -上面所用到的`BookType`枚举定义如下: +### BookType枚举 -````C# -namespace Acme.BookStore +上面所用到了 `BookType` 枚举,在 `Acme.BookStore.Domain.Shared` 项目创建 `BookType`. + +````csharp +namespace Acme.BookStore.Books { public enum BookType { @@ -176,69 +112,112 @@ namespace Acme.BookStore } ```` -#### 将Book实体添加到DbContext中 +最终的文件夹/文件结构应该如下所示: + +![bookstore-book-and-booktype](images/bookstore-book-and-booktype.png) + +### 将Book实体添加到DbContext中 -{{if DB == "ef"}} +{{if DB == "EF"}} EF Core需要你将实体和 `DbContext` 建立关联.最简单的做法是在`Acme.BookStore.EntityFrameworkCore`项目的`BookStoreDbContext`类中添加`DbSet`属性.如下所示: -````C# +````csharp public class BookStoreDbContext : AbpDbContext { - public DbSet Book { get; set; } - ... + public DbSet Books { get; set; } + //... } ```` {{end}} -{{if DB == "mongodb"}} +{{if DB == "Mongo"}} 添加 `IMongoCollection Book` 属性到 `Acme.BookStore.MongoDB` 项目的 `BookStoreMongoDbContext` 中. ```csharp public class BookStoreMongoDbContext : AbpMongoDbContext { - public IMongoCollection Users => Collection(); - public IMongoCollection Book => Collection();//<--added this line--> - //... + public IMongoCollection Books => Collection(); + //... } ``` {{end}} -{{if DB == "ef"}} +{{if DB == "EF"}} -#### 配置你的Book实体 +### 将Book实体映射到数据库表 -在 `Acme.BookStore.EntityFrameworkCore` 项目中打开 `BookStoreDbContextModelCreatingExtensions.cs` 文件,并将以下代码添加到 `ConfigureBookStore` 方法的末尾以配置Book实体: +在 `Acme.BookStore.EntityFrameworkCore` 项目中打开 `BookStoreDbContextModelCreatingExtensions.cs` 文件,添加 `Book` 实体的映射代码. 最终类应为: ````csharp -builder.Entity(b => +using Acme.BookStore.Books; +using Microsoft.EntityFrameworkCore; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; + +namespace Acme.BookStore.EntityFrameworkCore { - b.ToTable(BookStoreConsts.DbTablePrefix + "Book", BookStoreConsts.DbSchema); - b.ConfigureByConvention(); //auto configure for the base class props - b.Property(x => x.Name).IsRequired().HasMaxLength(128); -}); + public static class BookStoreDbContextModelCreatingExtensions + { + public static void ConfigureBookStore(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + /* Configure your own tables/entities inside here */ + + builder.Entity(b => + { + b.ToTable(BookStoreConsts.DbTablePrefix + "Books", + BookStoreConsts.DbSchema); + b.ConfigureByConvention(); //auto configure for the base class props + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + }); + } + } +} ```` -添加 `using Volo.Abp.EntityFrameworkCore.Modeling;` 以使用 `ConfigureByConvention` 扩展方法. +* `BookStoreConsts` 含有用于表的架构和表前缀的常量值. 你不必使用它,但建议在单点控制表前缀. +* `ConfigureByConvention()` 方法优雅的配置/映射继承的属性,应始终对你所有的实体使用它. -{{end}} +### 添加数据迁移 -{{if DB == "mongodb"}} +启动模板使用[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)创建和维护数据库架构. 打开菜单*工具 > NuGet包管理器*下的**程序包管理控制台 (PMC)**. + +![Open Package Manager Console](images/bookstore-open-package-manager-console.png) + +选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 做为**默认项目**然后执行以下命令: + +```bash +Add-Migration "Created_Book_Entity" +``` + +![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) + +它会在 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 项目中的 `Migrations` 文件内创建一个新的迁移类. + +在更新数据库之前,请阅读下面的部分了解如何将一些初始数据插入到数据库. + +> 如果你使用其他IDE而不是Visual Studio, 你可以使用 [`dotnet-ef]`(https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli#create-a-migration) 工具. + +{{end}} #### 添加种子数据 -添加种子数据是可选的,但第一次运行时最好将初始数据添加到数据库中. ABP提供了[数据种子系统](https://docs.abp.io/en/abp/latest/Data-Seeding). 在 `*.Domain` 项目下创建派生 `IDataSeedContributor` 的类: +> >在运行应用程序之前最好将初始数据添加到数据库中. 本节介绍ABP框架的[数据种子系统](../Data-Seeding.md). 如果你不想创建种子数据可以跳过本节,但是建议你遵循它来学习这个有用的ABP Framework功能。 + +在 `*.Domain` 项目下创建派生 `IDataSeedContributor` 的类,并且拷贝以下代码: ```csharp using System; using System.Threading.Tasks; +using Acme.BookStore.Books; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; namespace Acme.BookStore { @@ -246,102 +225,75 @@ namespace Acme.BookStore : IDataSeedContributor, ITransientDependency { private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - public BookStoreDataSeederContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) + public BookStoreDataSeederContributor(IRepository bookRepository) { _bookRepository = bookRepository; - _guidGenerator = guidGenerator; } public async Task SeedAsync(DataSeedContext context) { - if (await _bookRepository.GetCountAsync() > 0) + if (await _bookRepository.GetCountAsync() <= 0) { - return; + await _bookRepository.InsertAsync( + new Book + { + Name = "1984", + Type = BookType.Dystopia, + PublishDate = new DateTime(1949, 6, 8), + Price = 19.84f + }, + autoSave: true + ); + + await _bookRepository.InsertAsync( + new Book + { + Name = "The Hitchhiker's Guide to the Galaxy", + Type = BookType.ScienceFiction, + PublishDate = new DateTime(1995, 9, 27), + Price = 42.0f + }, + autoSave: true + ); } - - await _bookRepository.InsertAsync( - new Book( - id: _guidGenerator.Create(), - name: "1984", - type: BookType.Dystopia, - publishDate: new DateTime(1949, 6, 8), - price: 19.84f - ) - ); - - await _bookRepository.InsertAsync( - new Book( - id: _guidGenerator.Create(), - name: "The Hitchhiker's Guide to the Galaxy", - type: BookType.ScienceFiction, - publishDate: new DateTime(1995, 9, 27), - price: 42.0f - ) - ); } } } ``` -{{end}} +* 如果数据库中当前没有图书,则此代码使用 `IRepository`(默认为[repository](../Repositories.md)将两本书插入数据库. -{{if DB == "ef"}} +### 更新数据库 -#### 添加新的Migration并更新数据库 +运行 `Acme.BookStore.DbMigrator` 应用程序来更新数据库: -这个启动模板使用了[EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/)来创建并维护数据库结构.打开 **程序包管理器控制台(Package Manager Console) (PMC)** (工具/Nuget包管理器菜单) +![bookstore-dbmigrator-on-solution](images/bookstore-dbmigrator-on-solution.png) -![Open Package Manager Console](./images/bookstore-open-package-manager-console.png) +{{if DB == "EF"}} -选择 `Acme.BookStore.EntityFrameworkCore.DbMigrations`作为默认的项目然后执行下面的命令: +`.DbMigrator` 是一个控制台使用程序,可以在**开发**和**生产**环境**迁移数据库架构**和**初始化种子数据**. -```bash -Add-Migration "Created_Book_Entity" -``` - -![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) - -这样就会在 `Migrations` 文件夹中创建一个新的migration类.然后执行 `Update-Database` 命令更新数据库结构: - -````bash -Update-Database -```` - -![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png) - -#### 添加示例数据 - -`Update-Database`命令在数据库中创建了`AppBook`表. 打开数据库并输入几个示例行,以便在页面上显示它们: - -```mssql -INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES -('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84') - -INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES -('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42') +{{end}} -INSERT INTO AppBook (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES -('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7') -``` +{{if DB == "Mongo"}} -![bookstore-book-table](./images/bookstore-book-table.png) +尽管MongoDB**不需要**数据库架构迁移,但仍建议运行该应用程序,因为它可以在数据库上播种初始数据. 可以在**开发**和**生产**环境中使用. {{end}} -### 创建应用服务 +## 创建应用程序 -下一步是创建[应用服务](../Application-Services.md)来管理(创建,列出,更新,删除)书籍. 启动模板中的应用程序层分为两个项目: +应用程序层由两个分离的项目组成: -* `Acme.BookStore.Application.Contracts`主要包含你的DTO和应用程序服务接口. -* `Acme.BookStore.Application`包含应用程序服务的实现. +* `Acme.BookStore.Application.Contracts 包含你的[DTO](../Data-Transfer-Objects.md)和[应用服务](../Application-Services.md)接口. +* `Acme.BookStore.Application` 包含你的应用服务实现. -#### BookDto +在本部分中,你将创建一个应用程序服务,使用ABP Framework的 `CrudAppService` 基类来获取,创建,更新和删除书籍. -在`Acme.BookStore.Application.Contracts`项目中创建一个名为`BookDto`的DTO类: +### BookDto + +`CrudAppService` 基类需要定义实体的基本DTO. 在 `Acme.BookStore.Application.Contracts` 项目中创建一个名为 `BookDto` 的DTO类: ````C# using System; @@ -364,11 +316,12 @@ namespace Acme.BookStore * **DTO**类被用来在 **表示层** 和 **应用层** **传递数据**.查看[DTO文档](https://docs.abp.io/zh-Hans/abp/latest/Data-Transfer-Objects)查看更多信息. * 为了在页面上展示书籍信息,`BookDto`被用来将书籍数据传递到表示层. -* `BookDto`继承自 `AuditedEntityDto`.跟上面定义的`Book`类一样具有一些审计属性. +* `BookDto`继承自 `AuditedEntityDto`.跟上面定义的 `Book` 实体一样具有一些审计属性. 在将书籍返回到表示层时,需要将`Book`实体转换为`BookDto`对象. [AutoMapper](https://automapper.org)库可以在定义了正确的映射时自动执行此转换. 启动模板配置了AutoMapper,因此你只需在`Acme.BookStore.Application`项目的`BookStoreApplicationAutoMapperProfile`类中定义映射: ````csharp +using Acme.BookStore.Books; using AutoMapper; namespace Acme.BookStore @@ -383,16 +336,17 @@ namespace Acme.BookStore } ```` -#### CreateUpdateBookDto +> 参阅 [对象对对象映射](../Object-To-Object-Mapping.md) 文档了解详情. + +### CreateUpdateBookDto -在`Acme.BookStore.Application.Contracts`项目中创建一个名为`CreateUpdateBookDto`的DTO类: +在`Acme.BookStore.Application.Contracts`项目中创建一个名为 `CreateUpdateBookDto` 的DTO类: -````c# +````csharp using System; using System.ComponentModel.DataAnnotations; -using Volo.Abp.AutoMapper; -namespace Acme.BookStore +namespace Acme.BookStore.Books { public class CreateUpdateBookDto { @@ -404,7 +358,8 @@ namespace Acme.BookStore public BookType Type { get; set; } = BookType.Undefined; [Required] - public DateTime PublishDate { get; set; } + [DataType(DataType.Date)] + public DateTime PublishDate { get; set; } = DateTime.Now; [Required] public float Price { get; set; } @@ -415,9 +370,11 @@ namespace Acme.BookStore * 这个DTO类被用于在创建或更新书籍的时候从用户界面获取图书信息. * 它定义了数据注释属性(如`[Required]`)来定义属性的验证. DTO由ABP框架[自动验证](https://docs.abp.io/zh-Hans/abp/latest/Validation). -就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射: +就像上面的`BookDto`一样,创建一个从`CreateUpdateBookDto`对象到`Book`实体的映射,最终映射配置类如下: + ````csharp +using Acme.BookStore.Books; using AutoMapper; namespace Acme.BookStore @@ -427,30 +384,29 @@ namespace Acme.BookStore public BookStoreApplicationAutoMapperProfile() { CreateMap(); - CreateMap(); //<--added this line--> + CreateMap(); } } } ```` -#### IBookAppService +### IBookAppService -在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口: +下一步是为应用程序定义接口,在`Acme.BookStore.Application.Contracts`项目中定义一个名为`IBookAppService`的接口: -````C# +````csharp using System; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; -namespace Acme.BookStore +namespace Acme.BookStore.Books { public interface IBookAppService : - ICrudAppService< //定义了CRUD方法 - BookDto, //用来展示书籍 - Guid, //Book实体的主键 - PagedAndSortedResultRequestDto, //获取书籍的时候用于分页和排序 - CreateUpdateBookDto, //用于创建书籍 - CreateUpdateBookDto> //用于更新书籍 + ICrudAppService< //Defines CRUD methods + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto> //Used to create/update a book { } @@ -458,25 +414,29 @@ namespace Acme.BookStore ```` * 框架定义应用程序服务的接口**不是必需的**. 但是,它被建议作为最佳实践. -* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法. -* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定. +* `ICrudAppService`定义了常见的**CRUD**方法:`GetAsync`,`GetListAsync`,`CreateAsync`,`UpdateAsync`和`DeleteAsync`. 你可以从空的`IApplicationService`接口继承并手动定义自己的方法(将在下一部分中完成). +* `ICrudAppService`有一些变体, 你可以在每个方法中使用单独的DTO,也可以分别单独指定(例如使用不同的DTO进行创建和更新). -#### BookAppService +### BookAppService -在`Acme.BookStore.Application`项目中实现名为`BookAppService`的`IBookAppService`: +在`Acme.BookStore.Application`项目中创建名为 `BookAppService` 的 `IBookAppService` 实现: -````C# +````csharp using System; using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace Acme.BookStore +namespace Acme.BookStore.Books { public class BookAppService : - CrudAppService, - IBookAppService + CrudAppService< + Book, //The Book entity + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting + CreateUpdateBookDto>, //Used to create/update a book + IBookAppService //implement the IBookAppService { public BookAppService(IRepository repository) : base(repository) @@ -487,15 +447,17 @@ namespace Acme.BookStore } ```` -* `BookAppService`继承了`CrudAppService<...>`.它实现了上面定义的CRUD方法. +* `BookAppService`继承了`CrudAppService<...>`.它实现了 `ICrudAppService` 定义的CRUD方法. * `BookAppService`注入`IRepository `,这是`Book`实体的默认仓储. ABP自动为每个聚合根(或实体)创建默认仓储. 请参阅[仓储文档](https://docs.abp.io/zh-Hans/abp/latest/Repositories) -* `BookAppService`使用`IObjectMapper`将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 你之前定义了映射, 因此它将按预期工作. +* `BookAppService`使用[`IObjectMapper`](../Object-To-Object-Mapping.md)将`Book`对象转换为`BookDto`对象, 将`CreateUpdateBookDto`对象转换为`Book`对象. 启动模板使用[AutoMapper](http://automapper.org/)库作为对象映射提供程序. 我们之前定义了映射, 因此它将按预期工作. ### 自动生成API Controllers -你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. ABP可以[**自动**](https://docs.abp.io/zh-Hans/abp/latest/API/Auto-API-Controllers)按照惯例将你的应用程序服务配置为MVC API控制器. +你通常创建**Controller**以将应用程序服务公开为**HTTP API**端点. 因此允许浏览器或第三方客户端通过AJAX调用它们. + +ABP可以[**自动**](../API/Auto-API-Controllers.md)按照惯例将你的应用程序服务配置为MVC API控制器. -#### Swagger UI +### Swagger UI 启动模板配置为使用[Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)运行[swagger UI](https://swagger.io/tools/swagger-ui/). 运行应用程序并在浏览器中输入`https://localhost:XXXX/swagger/`(用你自己的端口替换XXXX)作为URL. @@ -503,485 +465,42 @@ namespace Acme.BookStore ![bookstore-swagger](images/bookstore-swagger.png) -Swagger有一个很好的UI来测试API. 你可以尝试执行`[GET] /api/app/book` API来获取书籍列表. - -{{if UI == "MVC"}} - -### 动态JavaScript代理 - -在Javascript端通过AJAX的方式调用HTTP API接口是很常见的,你可以使用`$.ajax`或者其他的工具来调用接口.当然,ABP中提供了更好的方式. - -ABP **自动** 为所有的API接口创建了JavaScript **代理**.因此,你可以像调用 **JavaScript function**一样调用任何接口. - -#### 在浏览器的开发者控制台中测试接口 - -你可以使用你钟爱的浏览器的 **开发者控制台** 中轻松测试JavaScript代理.运行程序,并打开浏览器的 **开发者工具**(快捷键:F12),切换到 **Console** 标签,输入下面的代码并回车: - -````js -acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); -```` - -* `acme.bookStore`是`BookAppService`的命名空间,转换成了[驼峰命名](https://en.wikipedia.org/wiki/Camel_case). -* `book`是`BookAppService`转换后的名字(去除了AppService后缀并转成了驼峰命名). -* `getList`是定义在`AsyncCrudAppService`基类中的`GetListAsync`方法转换后的名字(去除了Async后缀并转成了驼峰命名). -* `{}`参数用于将空对象发送到`GetListAsync`方法,该方法通常需要一个类型为`PagedAndSortedResultRequestDto`的对象,用于向服务器发送分页和排序选项(所有属性都是可选的,所以你可以发送一个空对象). -* `getList`方法返回了一个`promise`.因此,你可以传递一个回调函数到`done`(或者`then`)方法中来获取服务返回的结果. - -运行这段代码会产生下面的输出: - -![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png) - -你可以看到服务器返回的 **book list**.你还可以切换到开发者工具的 **network** 查看客户端到服务器端的通讯信息: - -![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png) - -我们使用`create`方法 **创建一本新书**: - -````js -acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); -```` - -你会看到控制台会显示类似这样的输出: - -````text -successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 -```` - -检查数据库中的`Book`表以查看新书. 你可以自己尝试`get`,`update`和`delete`功能. - -### 创建书籍页面 - -现在我们来创建一些可见和可用的东西,取代经典的MVC,我们使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start). +Swagger有一个很好的UI来测试API. -在 `Acme.BookStore.Web`项目的`Pages`文件夹下创建一个新的文件夹叫`Book`并添加一个名为`Index.cshtml`的Razor Page. +你可以尝试执行`[GET] /api/app/book` API来获取书籍列表, 服务端会返回以下JSON结果: -![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) - -打开`Index.cshtml`并把内容修改成下面这样: - -**Index.cshtml:** - -````html -@page -@using Acme.BookStore.Web.Pages.Book -@model IndexModel - -

    Book

    -```` - -* 确保`IndexModel`(Index.cshtml.cs)具有`Acme.BookStore.Web.Pages.Book`命名空间,或者在`Index.cshtml`中更新它. - -**Index.cshtml.cs:** - -```csharp -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Acme.BookStore.Web.Pages.Book +````json { - public class IndexModel : PageModel + "totalCount": 2, + "items": [ { - public void OnGet() - { - - } - } -} -``` - -#### 将Book页面添加到主菜单 - -打开`Menus`文件夹中的 `BookStoreMenuContributor` 类,在`ConfigureMainMenuAsync`方法的底部添加如下代码: - -````csharp -//... -namespace Acme.BookStore.Web.Menus -{ - public class BookStoreMenuContributor : IMenuContributor + "name": "The Hitchhiker's Guide to the Galaxy", + "type": 7, + "publishDate": "1995-09-27T00:00:00", + "price": 42, + "lastModificationTime": null, + "lastModifierId": null, + "creationTime": "2020-07-03T21:04:18.4607218", + "creatorId": null, + "id": "86100bb6-cbc1-25be-6643-39f62806969c" + }, { - private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) - { - //<-- added the below code - context.Menu.AddItem( - new ApplicationMenuItem("BookStore", l["Menu:BookStore"]) - .AddItem( - new ApplicationMenuItem("BookStore.Book", l["Menu:Book"], url: "/Book") - ) - ); - //--> - } + "name": "1984", + "type": 3, + "publishDate": "1949-06-08T00:00:00", + "price": 19.84, + "lastModificationTime": null, + "lastModifierId": null, + "creationTime": "2020-07-03T21:04:18.3174016", + "creatorId": null, + "id": "41055277-cce8-37d7-bb37-39f62806960b" } + ] } ```` -{{end}} - -#### 本地化菜单 - -本地化文本位于`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下: - -![bookstore-localization-files](./images/bookstore-localization-files-v2.png) - -打开`en.json`文件,将`Menu:BookStore`和`Menu:Book`键的本地化文本添加到文件末尾: - -````json -{ - "Culture": "en", - "Texts": { - "Menu:Home": "Home", - "Welcome": "Welcome", - "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", - - "Menu:BookStore": "Book Store", - "Menu:Book": "Book", - "Actions": "Actions", - "Edit": "Edit", - "PublishDate": "Publish date", - "NewBook": "New book", - "Name": "Name", - "Type": "Type", - "Price": "Price", - "CreationTime": "Creation time", - "AreYouSureToDelete": "Are you sure you want to delete this item?" - } -} -```` - -* ABP的本地化功能建立在[ASP.NET Core's standard localization]((https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization))之上并增加了一些扩展.查看[本地化文档](https://docs.abp.io/zh-Hans/abp/latest/Localization). -* 本地化key是任意的. 你可以设置任何名称. 我们更喜欢为菜单项添加`Menu:`前缀以区别于其他文本. 如果未在本地化文件中定义文本,则它将**返回**到本地化的key(ASP.NET Core的标准行为). - -运行该应用程序,看到新菜单项已添加到顶部栏: - -![bookstore-menu-items](./images/bookstore-new-menu-item.png) - -点击BookStore下Book子菜单项就会跳转到新增的书籍页面. - -#### 书籍列表 - -我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验. Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,而需要在页面中引用样式和脚本文件. - -##### Index.cshtml - -将`Pages/Book/Index.cshtml`改成下面的样子: - -````html -@page -@model Acme.BookStore.Web.Pages.Book.IndexModel -@section scripts -{ - -} - - -

    @L["Book"]

    -
    - - - - - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - - -
    -```` - -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Bundling-Minification)获取更多信息. -* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.1/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](https://docs.abp.io/zh-Hans/abp/latest/UI/AspNetCore/Tag-Helpers/Index). -* 你可以像上面本地化菜单一样 **本地化** 列名. - -#### 添加脚本文件 - -在`Pages/Book/`文件夹中创建 `index.js`文件 - -![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png) - -`index.js`的内容如下: - -````js -$(function () { - var l = abp.localization.getResource('BookStore'); - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); -}); -```` - -* `abp.libs.datatables.createAjax`是帮助ABP的动态JavaScript API代理跟[Datatable](https://datatables.net/)的格式相适应的辅助方法. -* `abp.libs.datatables.normalizeConfiguration`是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置. -* `acme.bookStore.book.getList`是获取书籍列表的方法(上面已经介绍过了) -* 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项. - -最终的页面如下: - -![Book list](./images/bookstore-book-list-2.png) - -{{end}} - -{{if UI == "NG"}} - -### Angular 开发 - -#### 创建book页面 - -是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: - -- [Angular CLI](https://angular.io/cli) 用于创建模块,组件和服务. -- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. -- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库. -- [Visual Studio Code](https://code.visualstudio.com/) 用做代码编辑器 (你可以选择自己喜欢的编辑器). - -#### 安装 NPM 包 - -在 `angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包: - -```bash -yarn -``` - -#### BookModule - -运行以下命令创建一个名为 `BookModule` 的新模块: - -```bash -yarn ng generate module book --routing true -``` - -![Generating book module](./images/bookstore-creating-book-module-terminal.png) - -#### 路由 - -打开位于 `src\app` 目录下的 `app-routing.module.ts` 文件. 添加新的路由: - -```js -const routes: Routes = [ -// ... -// added a new route to the routes array - { - path: 'books', - loadChildren: () => import('./book/book.module').then(m => m.BookModule) - } -] -``` - -* 我们添加了一个懒加载路由. 参阅 [嬾加載功能模块](https://angular.io/guide/lazy-loading-ngmodules#lazy-loading-feature-modules). - -打开位于 `src\app` 目录下的 `route.provider.ts` 文件,用以下内容替换它: - -```js -import { RoutesService, eLayoutType } from '@abp/ng.core'; -import { APP_INITIALIZER } from '@angular/core'; - -export const APP_ROUTE_PROVIDER = [ - { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, -]; - -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - //... - // added below element - { - path: '/books', - name: '::Menu:Books', - iconClass: 'fas fa-book', - order: 101, - layout: eLayoutType.application, - }, - ]); - }; -} -``` - -* 我们添加了一个新的路由元素在菜单上显示为 "Books" 的导航元素. - * `path` 路由的URL. - * `name` 菜单项的名称,可以使用本地化Key. - * `iconClass` 菜单项的图标. - * `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面. - * `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account` 或 `eLayoutType.empty`. - -更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice). - -#### Book 列表组件 - -在命令行运行以下命令,生成名为 book-list 的新组件: - -```bash -yarn ng generate component book/book-list -``` - -![Creating book list](./images/bookstore-creating-book-list-terminal.png) - -打开 `app\book` 目录下的 `book.module.ts` 文件,使用以下内容替换它: - -```js -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { BookRoutingModule } from './book-routing.module'; -import { BookListComponent } from './book-list/book-list.component'; -import { SharedModule } from '../shared/shared.module'; //<== added this line ==> - -@NgModule({ - declarations: [BookListComponent], - imports: [ - CommonModule, - BookRoutingModule, - SharedModule, //<== added this line ==> - ], -}) -export class BookModule {} -``` - -* 我们导入了 `SharedModule` 并添加到 `imports` 数组. - -打开 `app\book` 目录下的 `book-routing.module.ts` 文件用以下内容替换它: - -```js -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; -import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==> - -// <== replaced routes ==> -const routes: Routes = [ - { - path: '', - component: BookListComponent, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class BookRoutingModule { } -``` - -* 我们导入了 `BookListComponent` 并替换 `routes` 常量. - -运行 `yarn start`,等待Angular启动服务: - -```bash -yarn start -``` - -我们将看到book页面的 **book-list works!**: - -![Initial book list page](./images/bookstore-initial-book-list-page.png) - -#### 生成代理 - -ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md). - -在 `angular` 文件夹下运行以下命令: - -```bash -abp generate-proxy --module app -``` - -![Generate proxy command](./images/generate-proxy-command.png) - -生成的文件如下: - -![Generated files](./images/generated-proxies.png) - -#### BookListComponent - -打开 `app\book\book-list` 目录下的 `book-list.component.ts` 用以下内容替换它: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from '../models'; -import { BookService } from '../services'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], - providers: [ListService], -}) -export class BookListComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - booksType = BookType; - - constructor(public readonly list: ListService, private bookService: BookService) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getListByInput(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } -} -``` - -* 我们注入了生成的 `BookService`. -* 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索. - -打开 `app\book\book-list` 目录下的 `book-list.component.html` 用以下内容替换它: - -```html -
    -
    -
    -
    -
    - {%{{{ '::Menu:Books' | abpLocalization }}}%} -
    -
    -
    -
    -
    -
    - - - - - {%{{{ booksType[row.type] }}}%} - - - - - {%{{{ row.publishDate | date }}}%} - - - - - {%{{{ row.price | currency }}}%} - - - -
    -
    -``` - -* 我们添加了图书列表页面的HTML代码. - -现在你可以在浏览器看到最终结果: - -![Book list final result](./images/bookstore-book-list.png) - -项目的文件系统结构: - -![Book list final result](./images/bookstore-angular-file-tree.png) - -在本教程中我们遵循了官方的[Angular风格指南](https://angular.io/guide/styleguide#file-tree). - -{{end}} +这很酷,因为我们没有编写任何代码来创建API控制器,但是现在我们有了一个可以正常使用的REST API! -### 下一章 +## 下一章 -参阅[第二章](part-2.md)了解创建,更新和删除图书. +参阅教程的[下一章](part-2.md). diff --git a/docs/zh-Hans/Tutorials/Part-10.md b/docs/zh-Hans/Tutorials/Part-10.md new file mode 100644 index 0000000000..602306c484 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-10.md @@ -0,0 +1 @@ +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 751bd85ece..decddfebff 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -1,8 +1,9 @@ -## ASP.NET Core {{UI_Value}} 教程 - 第二章 +# Web应用程序开发教程 - 第二章: 图书列表页面 ````json //[doc-params] { - "UI": ["MVC","NG"] + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] } ```` @@ -21,904 +22,476 @@ else end }} -### 关于本教程 +## 关于本教程 -这是ASP.NET Core{{UI_Value}}系列教程的第二章. 共有三章: +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: -- [Part-1: 创建项目和书籍列表页面](Part-1.md) -- **Part 2: 创建,编辑,删除书籍(本章)** -- [Part-3: 集成测试](Part-3.md) +* **{{DB_Text}}** 做为ORM提供程序. +* **{{UI_Value}}** 做为UI框架. -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). +本教程分为以下部分: -{{if UI == "MVC"}} - -### 新增 Book 实体 +- [Part 1: 创建服务端](Part-1.md) +- **Part 2: 图书列表页面**(本章) +- [Part 3: 创建,更新和删除图书](Part-3.md) +- [Part 4: 集成测试](Part-4.md) +- [Part 5: 授权](Part-5.md) +- [Part 6: 作者: 领域层](Part-6.md) +- [Part 7: 作者: 数据库集成](Part-7.md) +- [Part 8: 作者: 应用服务层](Part-8.md) +- [Part 9: 作者: 用户页面](Part-9.md) +- [Part 10: 图书到作者的关系](Part-10.md) -通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: +## 下载源码 -![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) +本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: -#### 新建 modal form +* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: +{{if UI == "MVC"}} -![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) +## 动态JavaScript代理 -##### CreateModal.cshtml.cs +通常在 **JavaScript** 端通过AJAX调用HTTP API端点. 你可以使用 `$.ajax` 或其他工具来调用端点. 但是ABP提供了更好的方法. -打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: +ABP**动态**为所有API端点创建 **[JavaScript代理](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md)**. 所以你可以像调用**Javascript本地方法**一样使用任何**端点**. -````C# -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +### 在开发者控制台中进行测试 -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } +你可以在自己喜欢的浏览器的**开发者控制台**轻松的测试JavaScript代理. 运行应用程序,打开浏览器的**开发者人员工具**(*快捷键通常是F12*),切换到**控制台**选项卡,输入以下代码然后按回车: - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} +````js +acme.bookStore.books.book.getList({}).done(function (result) { console.log(result); }); ```` -* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. -* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. -* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. +* `acme.bookStore.books` 是 `BookAppService` 的命令空间转换成[小驼峰](https://en.wikipedia.org/wiki/Camel_case)形式. +* `book` 是 `BookAppService` 的约定名称(删除`AppService`后缀并且转换为小驼峰). +* `getList` 是 `CrudAppService` 基类定义的 `GetListAsync` 方法的约定名称(删除`Async`后缀并且转换为小驼峰). +* `{}` 参数将空对象发送到 `GetListAsync` 方法,该方法通常需要一个类型为 `PagedAndSortedResultRequestDto` 的对象,该对象用于将分页和排序选项发送到服务器(所有属性都是可选的,具有默认值. 因此你可以发送一个空对象). +* `getList` 函数返回一个 `promise`. 你可以传递一个回调到 `then`(或`done`)函数来获取从服务器返回的结果. -##### CreateModal.cshtml +运行该代码会产生以下输出: -打开 `CreateModal.cshtml` 文件并粘贴如下代码: +![bookstore-javascript-proxy-console](images/bookstore-javascript-proxy-console.png) -````html -@page -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model Acme.BookStore.Web.Pages.Books.CreateModalModel -@{ - Layout = null; -} - - - - - - - - - -```` +你可以看到服务端返回的 **图书列表**. 你也可以在开发者人员工具的 **网络** 选项卡查看客户端到服务端的通信: -* 这个 modal 使用 `abp-dynamic-form` Tag Helper 根据 `CreateBookViewModel` 类自动构建了表单. - * `abp-model` 指定了 `Book` 属性为模型对象. - * `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. - * `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). +![bookstore-getlist-result-network](images/bookstore-getlist-result-network.png) -#### 添加 "New book" 按钮 +Let's **create a new book** using the `create` function: -打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : +让我们使用 `create` 函数**创建一本书**: -````html - - - -

    @L["Books"]

    -
    - - - -
    -
    +````js +acme.bookStore.books.book.create({ + name: 'Foundation', + type: 7, + publishDate: '1951-05-24', + price: 21.5 + }).then(function (result) { + console.log('successfully created the book with id: ' + result.id); + }); ```` -如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: +您应该在控制台中看到类似以下的消息: -![bookstore-new-book-button](./images/bookstore-new-book-button.png) +````text +successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 +```` -打开 `Pages/book/index.js` 在 `datatable` 配置代码后面添加如下代码: +检查数据库中的 `Books` 表你会看到新的一行. 你可以自己尝试使用 `get`, `update` 和 `delete` 函数. -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); +我们将利用这些动态代理功能在接下来的章节来与服务器通信. -createModal.onResult(function () { - dataTable.ajax.reload(); -}); +{{end}} -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` +## 本地化 -* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. +开始的UI开发之前,我们首先要准备本地化的文本(这是你通常在开发应用程序时需要做的). -现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. +本地化文本位于 `Acme.BookStore.Domain.Shared` 项目的 `Localization/BookStore` 文件夹下: -### 编辑更新已存在的 Book 实体 +![bookstore-localization-files](./images/bookstore-localization-files-v2.png) -在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: +打开 `en.json` (*英文翻译*)文件并更改内容,如下所示: -![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) +````json +{ + "Culture": "en", + "Texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", + "Menu:BookStore": "Book Store", + "Menu:Books": "Books", + "Actions": "Actions", + "Close": "Close", + "Delete": "Delete", + "Edit": "Edit", + "PublishDate": "Publish date", + "NewBook": "New book", + "Name": "Name", + "Type": "Type", + "Price": "Price", + "CreationTime": "Creation time", + "AreYouSure": "Are you sure?", + "AreYouSureToDelete": "Are you sure you want to delete this item?", + "Enum:BookType:0": "Undefined", + "Enum:BookType:1": "Adventure", + "Enum:BookType:2": "Biography", + "Enum:BookType:3": "Dystopia", + "Enum:BookType:4": "Fantastic", + "Enum:BookType:5": "Horror", + "Enum:BookType:6": "Science", + "Enum:BookType:7": "Science fiction", + "Enum:BookType:8": "Poetry" + } +} +```` -#### EditModal.cshtml.cs +* 本地化关键字名称是任意的. 你可以设置任何名称. 对于特定的文本类型,我们更喜欢遵循一些约定: + * 为按钮项添加 `Menu:` 前缀. + * 使用 `Enum::` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化. -展开 `EditModal.cshtml`,打开 `EditModal.cshtml.cs` 文件( `EditModalModel` 类) 并替换成以下代码: +如果未在本地化文件中定义文本,则文本将**回退**到本地化键(作为ASP.NET Core的标准行为). -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; +> ABP本地化系统建立在[ASP.NET Core标准本地化](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization)系统之上,并以多种方式进行了扩展. 有关详细信息请参见[本地化文档](../Localization.md). -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } +{{if UI == "MVC"}} - [BindProperty] - public CreateUpdateBookDto Book { get; set; } +## 创建图书页面 - private readonly IBookAppService _bookAppService; +是时候创建可见的和可用的东西了! 代替经典的MVC,我们将使用微软推荐的[Razor Pages UI](https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/razor-pages/razor-pages-start). - public EditModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } +在 `Acme.BookStore.Web` 项目的 `Pages` 文件夹下创建一个名为新的 `Books` 的文件夹. 然后在文件夹右键选择 **添加 > Razor Page** 菜单. 输入名称 `Index`: - public async Task OnGetAsync() - { - var bookDto = await _bookAppService.GetAsync(Id); - Book = ObjectMapper.Map(bookDto); - } +![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -```` +打开 `Index.cshtml` 并把内容修改成下面这样: -* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. -* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. -* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. +````html +@page +@using Acme.BookStore.Web.Pages.Books +@model IndexModel -#### BookDto到CreateUpdateBookDto对象映射 +

    Books

    +```` -为了执行`BookDto`到`CreateUpdateBookDto`对象映射,请打开`Acme.BookStore.Web`项目中的`BookStoreWebAutoMapperProfile.cs`并更改它,如下所示: +`Index.cshtml.cs` 内容应该是: -````csharp -using AutoMapper; +```csharp +using Microsoft.AspNetCore.Mvc.RazorPages; -namespace Acme.BookStore.Web +namespace Acme.BookStore.Web.Pages.Books { - public class BookStoreWebAutoMapperProfile : Profile + public class IndexModel : PageModel { - public BookStoreWebAutoMapperProfile() + public void OnGet() { - CreateMap(); + } } } -```` - -* 刚刚添加了`CreateMap();`作为映射定义. +``` -#### EditModal.cshtml +### 将Book页面添加到主菜单 -将 `EditModal.cshtml` 页面内容替换成如下代码: +打开 `Menus` 文件夹中的 `BookStoreMenuContributor` 类,在 `ConfigureMainMenuAsync` 方法的底部添加如下代码: -````html -@page -@using Acme.BookStore.Web.Pages.Books -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@{ - Layout = null; -} - - - - - - - - - - +````csharp +context.Menu.AddItem( + new ApplicationMenuItem( + "BooksStore", + l["Menu:BookStore"], + icon: "fa fa-book" + ).AddItem( + new ApplicationMenuItem( + "BooksStore.Books", + l["Menu:Books"], + url: "/Books" + ) + ) +); ```` -这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: +运行项目,使用用户名 `admin` 和密码 `1q2w3E*` 登录到应用程序. 看到新菜单项已添加到顶部栏: -* 它包含`id`属性的`abp-input`, 用于存储编辑书的 `id` (它是隐藏的Input) -* 此页面指定的post地址是`Books/EditModal`, 并用文本 *Update* 作为 modal 标题. +![bookstore-menu-items](./images/bookstore-new-menu-item.png) -#### 为表格添加 "操作(Actions)" 下拉菜单 - -我们将为表格每行添加下拉按钮 ("Actions") . 最终效果如下: - -![bookstore-book-table-actions](images/bookstore-book-table-actions.png) - -打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: - -````html - - - - @L["Actions"] - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - -```` +点击BookStore下的Books子菜单项就会跳转到空的图书页面. -* 只是为"Actions"增加了一个 `th` 标签. +### 图书列表 -打开 `Pages/book/index.js` 并用以下内容进行替换: +We will use the [Datatables.net](https://datatables.net/) jQuery library to show the book list. Datatables library completely work via AJAX, it is fast, popular and provides a good user experience. -````js -$(function () { +我们将使用[Datatables.net](https://datatables.net/)JQuery插件来显示页面上的表格列表. [Datatables](https://datatables.net/)可以完全通过AJAX工作,速度快,并提供良好的用户体验. - var l = abp.localization.getResource('BookStore'); +> Datatables插件在启动模板中配置,因此你可以直接在任何页面中使用它,无需在页面中引用样式和脚本文件. - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); +#### Index.cshtml - editModal.onResult(function () { - dataTable.ajax.reload(); - }); +将 `Pages/Book/Index.cshtml` 改成下面的样子: - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Books +@using Microsoft.Extensions.Localization +@model IndexModel +@inject IStringLocalizer L +@section scripts +{ + +} + + +

    @L["Books"]

    +
    + + + +
    ```` -* 通过 `abp.localization.getResource('BookStore')` 可以在客户端使用服务器端定义的相同的本地化语言文本. -* 添加了一个名为 `createModal` 的新的 `ModalManager` 来打开创建用的 modal 对话框. -* 添加了一个名为 `editModal` 的新的 `ModalManager` 来打开编辑用的 modal 对话框. -* 在 `columnDefs` 起始处新增一列用于显示 "Actions" 下拉按钮. -* "New Book"动作只需调用`createModal.open`来打开创建对话框. -* "Edit" 操作只是简单调用 `editModal.open` 来打开编辑对话框. +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro)用于将外部的 **脚本** 添加到页面中.它比标准的`script`标签多了很多额外的功能.它可以处理 **最小化**和 **版本**.查看[捆绑 & 压缩文档](../UI/AspNetCore/Bundling-Minification.md)获取更多信息. +* `abp-card` 和 `abp-table` 是为Twitter Bootstrap的[card component](http://getbootstrap.com/docs/4.5/components/card/)封装的 **tag helpers**.ABP中有很多tag helpers,可以很方便的使用大多数[bootstrap](https://getbootstrap.com/)组件.你也可以使用原生的HTML标签代替tag helpers.使用tag helper可以通过智能提示和编译时类型检查减少HTML代码并防止错误.查看[tag helpers 文档](../UI/AspNetCore/Tag-Helpers/Index.md). -现在,你可以运行程序,通过编辑操作来更新任一个book实体. +#### Index.js -### 删除一个已有的Book实体 +在 `Pages/Books/` 文件夹中创建 `index.js`文件 -打开 `Pages/book/index.js` 文件,在 `rowAction` `items` 下新增一项: +![bookstore-index-js-file](./images/bookstore-index-js-file-v3.png) -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. -* 通过javascript代理方法 `acme.bookStore.book.delete` 执行一个AJAX请求来删除一个book实体. -* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. - -最终的 `index.js` 文件内容如下所示: +`index.js` 的内容如下: ````js $(function () { - var l = abp.localization.getResource('BookStore'); - var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); - - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } - } - ] + var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + scrollX: true, + ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), + columnDefs: [ + { + title: l('Name'), + data: "name" + }, + { + title: l('Type'), + data: "type", + render: function (data) { + return l('Enum:BookType:' + data); + } + }, + { + title: l('PublishDate'), + data: "publishDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + }, + { + title: l('Price'), + data: "price" + }, + { + title: l('CreationTime'), data: "creationTime", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(luxon.DateTime.DATETIME_SHORT); + } } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); + ] + }) + ); }); ```` -打开`Acme.BookStore.Domain.Shared`项目中的`en.json`并添加以下行: - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", -"SuccessfullyDeleted": "Successfully deleted" -```` - -运行程序并尝试删除一个book实体. - -{{end}} - -{{if UI == "NG"}} - -### 新增 Book 实体 - -下面的章节中,你将学习到如何创建一个新的模态对话框来新增Book实体. - -#### 添加 modal 到 BookListComponent - - -Open `book-list.component.ts` file in `app\book\book-list` folder and replace the content as below: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from '../models'; -import { BookService } from '../services'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], - providers: [ListService], -}) -export class BookListComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - booksType = BookType; - - isModalOpen = false; // <== added this line ==> - - constructor(public readonly list: ListService, private bookService: BookService) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getListByInput(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - // added createBook method - createBook() { - this.isModalOpen = true; - } -} -``` - -* 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法. +* `abp.localization.getResource` 获取一个函数,该函数用于使用服务器端定义的相同JSON文件对文本进行本地化. 通过这种方式你可以与客户端共享本地化值. +* `abp.libs.datatables.normalizeConfiguration`是另一个辅助方法.不是必须的, 但是它通过为缺少的选项提供常规值来简化数据表配置. +* `abp.libs.datatables.createAjax`是帮助ABP的动态JavaScript API代理跟[Datatable](https://datatables.net/)的格式相适应的辅助方法. +* `acme.bookStore.books.book.getList` 是动态JavaScript代理函数(上面已经介绍过了) +* [luxon](https://moment.github.io/luxon/) 库也是该解决方案中预先配置的标准库,你可以轻松地执行日期/时间操作. -打开 `app\book\book-list` 文件夹内的 `book-list.component.html` 文件,使用以下内容替换它: +> 查看 [Datatable文档](https://datatables.net/manual/) 了解更多配置项. -```html -
    -
    -
    -
    -
    {%{{{ '::Menu:Books' | abpLocalization }}}%}
    -
    - -
    -
    - -
    -
    -
    -
    -
    - - - - - {%{{{ booksType[row.type] }}}%} - - - - - {%{{{ row.publishDate | date }}}%} - - - - - {%{{{ row.price | currency }}}%} - - - -
    -
    - - - - -

    {%{{{ '::NewBook' | abpLocalization }}}%}

    -
    - - - - - - -
    -``` +## 运行最终应用程序 -* 我们添加了 `abp-modal` 渲染模态框,允许用户创建新书. -* `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. -* 我们添加了 `New book` 按钮到 `AbpContentToolbar`. - -你可以打开浏览器,点击**New book**按钮看到模态框. - -![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) - -#### 添加响应式表单 - -[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. - -打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from '../models'; -import { BookService } from '../services'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // <== added this line ==> - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], - providers: [ListService], -}) -export class BookListComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; +你可以运行应用程序!该部分的最终用户界面如下所示: - booksType = BookType; +![Book list](images/bookstore-book-list-3.png) - isModalOpen = false; +这是一个完全正常工作的服务端分页,排序和本地化的图书列表. - form: FormGroup; // <== added this line ==> +{{end}} - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder // <== injected FormBuilder ==> - ) {} +{{if UI == "NG"}} - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getListByInput(query); +## 安装NPM包 - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } +>注意: 本教程基于ABP Framework v3.0.3+. 如果你的项目版本较旧,请升级您的解决方案. 如果要升级现有的v2.x项目,请参阅[迁移指南](../UI/Angular/Migration-Guide-v3.md). - createBook() { - this.buildForm(); // <== added this line ==> - this.isModalOpen = true; - } +在 `angular` 目录下打开命令行窗口,选择 `yarn` 命令安装NPM包: - // added buildForm method - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } -} +```bash +yarn ``` -* 我们导入了 `FormGroup, FormBuilder and Validators`. -* 我们添加了 `form: FormGroup` 变量. -* 我们注入了 `fb: FormBuilder` 服务到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 服务为生成控件提供了方便的方法. 它减少了构建复杂表单所需的样板文件的数量. -* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法. 该方法创建一个响应式表单去创建新书. - * `FormBuilder` 内的 `fb.group` 方法创建一个 `FormGroup`. - * 添加了 `Validators.required` 静态方法用于验证表单元素. +## 创建图书页面 -#### 创建表单的DOM元素 +是时候创建可见和可用的东西了!开发ABP Angular前端应用程序时,需要使用一些工具: -打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 ` `: +- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) 用做UI组件库. +- [ngx-datatable](https://swimlane.gitbook.io/ngx-datatable/) 用做 datatable 类库. -```html - -
    -
    - * - -
    +### BookModule -
    - * - -
    - -
    - * - -
    +运行以下命令创建一个名为 `BookModule` 的新模块: -
    - * - -
    -
    -
    +```bash +yarn ng generate module book --module app --routing --route books ``` -- 模板创建了带有 `Name`, `Price`, `Type` 和 `Publish` 时间字段的表单. -- 我们在组件中使用了 [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview). +该命令应该产生以下的输出: + +````bash +> yarn ng generate module book --module app --routing --route books + +yarn run v1.19.1 +$ ng generate module book --module app --routing --route books +CREATE src/app/book/book-routing.module.ts (336 bytes) +CREATE src/app/book/book.module.ts (335 bytes) +CREATE src/app/book/book.component.html (19 bytes) +CREATE src/app/book/book.component.spec.ts (614 bytes) +CREATE src/app/book/book.component.ts (268 bytes) +CREATE src/app/book/book.component.scss (0 bytes) +UPDATE src/app/app-routing.module.ts (1289 bytes) +Done in 3.88s. +```` -#### Datepicker 要求 +### BookModule -打开 `app\book` 文件夹下的 `book.module.ts` 文件,使用以下内容替换它: +打开 `/src/app/book/book.module.ts` 并使用以下内容替换: -```js +````js import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { BooksRoutingModule } from './book-routing.module'; -import { BooksComponent } from './book.component'; -import { BookListComponent } from './book-list/book-list.component'; import { SharedModule } from '../shared/shared.module'; -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> +import { BookRoutingModule } from './book-routing.module'; +import { BookComponent } from './book.component'; @NgModule({ - declarations: [BooksComponent, BookListComponent], + declarations: [BookComponent], imports: [ - CommonModule, - BooksRoutingModule, - SharedModule, - NgbDatepickerModule //<== added this line ==> + BookRoutingModule, + SharedModule ] }) -export class BooksModule { } -``` - -* 我们导入了 `NgbDatepickerModule` 来使用日期选择器. - -打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from '../models'; -import { BookService } from '../services'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; // <== added this line ==> - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], // <== added a provide ==> -}) -export class BookListComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; +export class BookModule { } - booksType = BookType; - - // <== added bookTypes array ==> - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' - ); +```` - isModalOpen = false; +* 添加了 `SharedModule`. `SharedModule` 导出了一些创建用户界面所需的通用模块. +* `SharedModule` 已经导出了 `CommonModule`,所以我们删除了 `CommonModule`. - form: FormGroup; +### 路由 - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} +生成的代码将新的路由定义放在 `src/app/app-routing.module.ts` 文件中,如下所示: - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getListByInput(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } +````js +const routes: Routes = [ + // other route definitions... + { path: 'books', loadChildren: () => import('./book/book.module').then(m => m.BookModule) }, +]; +```` - createBook() { - this.buildForm(); - this.isModalOpen = true; - } +现在打开 `src/app/route.provider.ts` 以下替换 `configureRoutes` 函数: - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } +```js +function configureRoutes(routes: RoutesService) { + return () => { + routes.add([ + { + path: '/', + name: '::Menu:Home', + iconClass: 'fas fa-home', + order: 1, + layout: eLayoutType.application, + }, + { + path: '/book-store', + name: '::Menu:BookStore', + iconClass: 'fas fa-book', + order: 2, + layout: eLayoutType.application, + }, + { + path: '/books', + name: '::Menu:Books', + parentName: '::Menu:BookStore', + layout: eLayoutType.application, + }, + ]); + }; } ``` -* 我们导入了 ` NgbDateNativeAdapter, NgbDateAdapter` - -* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为Date类型. 有关更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview). - -* 我们添加了 `bookTypes` 数组,以便能够在combobox值中使用它. `bookTypes` 包含 `BookType` 枚举的字段. 得到的数组如下所示: - - ```js - ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] - ``` - - 在先前的表单模板中 用 `ngFor` 使用这个数组. - -现在你可以打开浏览器看到以下变化: - -![New book modal](./images/bookstore-new-book-form.png) - -#### 保存图书 - -打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,使用以下内容替换它: - -```js -import { ListService, PagedResultDto } from '@abp/ng.core'; -import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from '../models'; -import { BookService } from '../services'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookListComponent implements OnInit { - book = { items: [], totalCount: 0 } as PagedResultDto; - - booksType = BookType; - - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' - ); - - isModalOpen = false; - - form: FormGroup; - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} - - ngOnInit() { - const bookStreamCreator = (query) => this.bookService.getListByInput(query); - - this.list.hookToQuery(bookStreamCreator).subscribe((response) => { - this.book = response; - }); - } - - createBook() { - this.buildForm(); - this.isModalOpen = true; - } +`RoutesService` 是ABP框架提供的用于配置主菜单和路由的服务. - buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); - } - - // <== added save ==> - save() { - if (this.form.invalid) { - return; - } - - this.bookService.createByInput(this.form.value).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` +* `path` 路由的URL. +* `name` 菜单项的名称(参阅[本地化文档](../UI/Angular/Localization.md)了解更多). +* `iconClass` 菜单项的图标(你可以使用默认的[Font Awesome](https://fontawesome.com/)图标). +* `order` 菜单项的排序.我们定义了101,它显示在 "Administration" 项的后面. +* `layout` BooksModule路由的布局. 可以定义 `eLayoutType.application`, `eLayoutType.account` 或 `eLayoutType.empty`. -* 我们添加了 `save` 方法. +更多信息请参阅[RoutesService 文档](https://docs.abp.io/en/abp/latest/UI/Angular/Modifying-the-Menu.md#via-routesservice). -打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件, 找到 `` 元素,使用下面元素替换它: +### 生成代理 -```html - - - - - - -``` +ABP CLI提供了 `generate-proxy` 命令为你的服务HTTP API生成客户端代理简化客户端使用服务的成本. 运行 `generate-proxy` 命令前你的host必须正在运行. 参阅 [CLI 文档](../CLI.md). -使用以下内容替换 `
    ` 标签: +在 `angular` 文件夹下运行以下命令: -```html - +```bash +abp generate-proxy ``` -* 我们添加了 `(ngSubmit)="save()"` 到 `` 元素,当按下enter时保存图书. -* 我们在模态框的底部添加了 `abp-button` 来保存图书. +生成的文件如下: -模态框最终看起来像这样: +![Generated files](./images/generated-proxies-2.png) -![Save button to the modal](./images/bookstore-new-book-form-v2.png) +### BookComponent -### 更新图书 - -打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件并且添加名为 `selectedBook` 的变量. +打开 `/src/app/book/book.component.ts` 用以下内容替换它: ```js import { ListService, PagedResultDto } from '@abp/ng.core'; import { Component, OnInit } from '@angular/core'; -import { BookDto, BookType } from '../models'; -import { BookService } from '../services'; -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { BookDto } from './models'; +import { BookService } from './services'; @Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], - providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], + selector: 'app-book', + templateUrl: './book.component.html', + styleUrls: ['./book.component.scss'], + providers: [ListService], }) -export class BookListComponent implements OnInit { +export class BookComponent implements OnInit { book = { items: [], totalCount: 0 } as PagedResultDto; - booksType = BookType; - - bookTypes = Object.keys(BookType).filter( - (bookType) => typeof this.booksType[bookType] === 'number' - ); - - isModalOpen = false; - - form: FormGroup; - - selectedBook = {} as BookDto; // <== declared selectedBook ==> - - constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder - ) {} + constructor(public readonly list: ListService, private bookService: BookService) {} ngOnInit() { const bookStreamCreator = (query) => this.bookService.getListByInput(query); @@ -927,196 +500,55 @@ export class BookListComponent implements OnInit { this.book = response; }); } - - // <== this method is replaced ==> - createBook() { - this.selectedBook = {} as BookDto; // <== added ==> - this.buildForm(); - this.isModalOpen = true; - } - - // <== added editBook method ==> - editBook(id: string) { - this.bookService.getById(id).subscribe((book) => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } - - // <== this method is replaced ==> - buildForm() { - this.form = this.fb.group({ - name: [this.selectedBook.name || '', Validators.required], - type: [this.selectedBook.type || null, Validators.required], - publishDate: [ - this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - Validators.required, - ], - price: [this.selectedBook.price || null, Validators.required], - }); - } - - // <== this method is replaced ==> - save() { - if (this.form.invalid) { - return; - } - - // <== added request ==> - const request = this.selectedBook.id - ? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id) - : this.bookService.createByInput(this.form.value); - - request.subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - this.list.get(); - }); - } -} -``` - -* 我们声明了类型为 `BookDto` 的 `selectedBook` 变量. -* 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象. -* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. -* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. -* 我们替换了 `save` 方法. - -#### 添加 "Actions" 下拉框到表格 - -打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `
    ` 标签: - -```html -
    - - - - -
    - -
    - -
    -
    -
    -
    - - - - {%{{{ booksType[row.type] }}}%} - - - - - {%{{{ row.publishDate | date }}}%} - - - - - {%{{{ row.price | currency }}}%} - - -
    -
    -``` - -- 我们添加了 "Actions" 栏的 `th`. -- 我们为 "Actions" 栏添加了 `ngx-datatable-column`. -- 我们添加了带有 `ngbDropdownToggle` 的 `button`,在点击按钮时打开操作. -- 我们习惯于将[NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples)用于操作的下拉菜单. - -UI最终看起来像这样: - -![Action buttons](./images/bookstore-actions-buttons.png) - -打开 `app\book\book-list` 文件夹下的 `book-list.component.html` 文件,使用以下内容替换 `` 标签: - -```html - -

    {%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}

    -
    -``` - -* **Edit** 文本做为编辑记录操作的标题, **New Book** 做为添加记录操作的标题. - -### 删除图书 - -#### 删除确认弹层 - -打开 `app\book\book-list` 文件夹下的 `book-list.component.ts` 文件,注入 `ConfirmationService`. - -替换构造函数: - -```js -import { ConfirmationService } from '@abp/ng.theme.shared'; -//... - -constructor( - public readonly list: ListService, - private bookService: BookService, - private fb: FormBuilder, - private confirmation: ConfirmationService // <== added this line ==> -) {} -``` - -* We imported `ConfirmationService`. -* We injected `ConfirmationService` to the constructor. - -See the [Confirmation Popup documentation](https://docs.abp.io/en/abp/latest/UI/Angular/Confirmation-Service) - -In the `book-list.component.ts` add a delete method: - -```js -import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== imported Confirmation namespace ==> - -//... - -delete(id: string) { - this.confirmation.warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure').subscribe((status) => { - if (status === Confirmation.Status.confirm) { - this.bookService.deleteById(id).subscribe(() => this.list.get()); - } - }); } ``` -`delete` 方法会显示一个确认弹层并订阅用户响应. 只在用户点击 `Yes` 按钮时调用 `BookService` 的 `deleteById` 方法. 确认弹层看起来如下: - -![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) +* 我们注入了生成的 `BookService`. +* 我们实现了 [ListService](https://docs.abp.io/en/abp/latest/UI/Angular/List-Service),它是一个公用服务,提供了简单的分页,排序和搜索. -#### 添加删除按钮 - -打开 `app\book\book-list` 文件夹下的 `app\book\book-list` 文件,修改 `ngbDropdownMenu` 添加删除按钮: +打开 `/src/app/book/book.component.html` 用以下内容替换它: ```html -
    - - +
    +
    +
    +
    +
    + {%{{{ '::Menu:Books' | abpLocalization }}}%} +
    +
    +
    +
    +
    +
    + + + + + {%{{{ '::Enum:BookType:' + row.type | abpLocalization }}}%} + + + + + {%{{{ row.publishDate | date }}}%} + + + + + {%{{{ row.price | currency }}}%} + + + +
    ``` -最终操作下拉框UI看起来如下: +现在你可以在浏览器看到最终结果: -![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) +![Book list final result](./images/bookstore-book-list.png) {{end}} -### 下一章 +## 下一章 -查看本教程的 [下一章](Part-3.md) . +查看本教程的[下一章](Part-3.md). diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md index c9eb3bac78..7ad96a0465 100644 --- a/docs/zh-Hans/Tutorials/Part-3.md +++ b/docs/zh-Hans/Tutorials/Part-3.md @@ -1,198 +1,1187 @@ -## ASP.NET Core {{UI_Value}} 教程 - 第三章 +# Web应用程序开发教程 - 第三章: 创建,更新和删除图书 ````json //[doc-params] { - "UI": ["MVC","NG"] + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] } ```` - {{ if UI == "MVC" - DB="ef" - DB_Text="Entity Framework Core" UI_Text="mvc" else if UI == "NG" - DB="mongodb" - DB_Text="MongoDB" UI_Text="angular" -else - DB ="?" +else UI_Text="?" end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end }} -### 关于本教程 +## 关于本教程 -这是ASP.NET Core{{UI_Value}}系列教程的第三章. 共有三章: +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: -- [Part-1: 创建项目和书籍列表页面](Part-1.md) -- [Part 2: 创建,编辑,删除书籍](Part-2.md) -- **Part-3: 集成测试(本章)** +* **{{DB_Text}}** 做为ORM提供程序. +* **{{UI_Value}}** 做为UI框架. -> 你也可以观看由ABP社区成员为本教程录制的[视频课程](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application). +本教程分为以下部分: -### 解决方案中的测试项目 +- [Part 1: 创建服务端](Part-1.md) +- [Part 2: 图书列表页面](Part-2.md) +- **Part 3: 创建,更新和删除图书**(本章) +- [Part 4: 集成测试](Part-4.md) +- [Part 5: 授权](Part-5.md) +- [Part 6: 作者: 领域层](Part-6.md) +- [Part 7: 作者: 数据库集成](Part-7.md) +- [Part 8: 作者: 应用服务层](Part-8.md) +- [Part 9: 作者: 用户页面](Part-9.md) +- [Part 10: 图书到作者的关系](Part-10.md) -解决方案中有多个测试项目: +## 下载源码 -![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png) +本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: -每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: +* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) -* [xunit](https://xunit.github.io/) 作为主测试框架. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. -* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. +{{if UI == "MVC"}} -### 添加测试数据 +## 创建新书籍 -启动模板包含`Acme.BookStore.TestBase`项目中的`BookStoreTestDataSeedContributor`类,它创建一些数据来运行测试. -更改`BookStoreTestDataSeedContributor`类如下所示: +通过本节, 你将会了解如何创建一个 modal form 来实现新增书籍的功能. 最终成果如下图所示: -````csharp -using System; +![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) + +### 创建 modal form + +在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个 `CreateModal.cshtml` Razor页面: + +![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) + +#### CreateModal.cshtml.cs + +打开 `CreateModal.cshtml.cs` 代码文件,用如下代码替换 `CreateModalModel` 类的实现: + +````C# using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; +using Acme.BookStore.Books; +using Microsoft.AspNetCore.Mvc; -namespace Acme.BookStore +namespace Acme.BookStore.Web.Pages.Books { - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency + public class CreateModalModel : BookStorePageModel { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) + public void OnGet() { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; + Book = new CreateUpdateBookDto(); } - public async Task SeedAsync(DataSeedContext context) + public async Task OnPostAsync() { - await _bookRepository.InsertAsync( - new Book(id: _guidGenerator.Create(), - name: "Test book 1", - type: BookType.Fantastic, - publishDate: new DateTime(2015, 05, 24), - price: 21 - ) - ); - - await _bookRepository.InsertAsync( - new Book(id: _guidGenerator.Create(), - name: "Test book 2", - type: BookType.Science, - publishDate: new DateTime(2014, 02, 11), - price: 15 - ) - ); + await _bookAppService.CreateAsync(Book); + return NoContent(); } } } ```` -* 注入`IRepository`并在`SeedAsync`中使用它来创建两个书实体作为测试数据. -* 使用`IGuidGenerator`服务创建GUID. 虽然`Guid.NewGuid()`非常适合测试,但`IGuidGenerator`在使用真实数据库时还有其他特别重要的功能(参见[Guid生成文档](../Guid-Generation.md)了解更多信息). +* 该类派生于 `BookStorePageModel` 而非默认的 `PageModel`. `BookStorePageModel` 继承了 `PageModel` 并且添加了一些可以被你的page model类使用的通用属性和方法. +* `Book` 属性上的 `[BindProperty]` 特性将post请求提交上来的数据绑定到该属性上. +* 该类通过构造函数注入了 `IBookAppService` 应用服务,并且在 `OnPostAsync` 处理程序中调用了服务的 `CreateAsync` 方法. +* 它在 `OnGet` 方法中创建一个新的 `CreateUpdateBookDto` 对象。 ASP.NET Core不需要像这样创建一个新实例就可以正常工作. 但是它不会为你创建实例,并且如果你的类在类构造函数中具有一些默认值分配或代码执行,它们将无法工作. 对于这种情况,我们为某些 `CreateUpdateBookDto` 属性设置了默认值. + +#### CreateModal.cshtml + +打开 `CreateModal.cshtml` 文件并粘贴如下代码: + +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Books +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model CreateModalModel +@inject IStringLocalizer L +@{ + Layout = null; +} + + + + + + + + + +```` + +* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateBookViewModel` 类自动构建了表单. +* `abp-model` 指定了 `Book` 属性为模型对象. +* `data-ajaxForm` 设置了表单通过AJAX提交,而不是经典的页面回发. +* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要). + +> 提示: 就像在本示例中一样,`Layout` 应该为 `null`,因为当通过AJAX加载模态时,我们不希望包括所有布局. + +### 添加 "New book" 按钮 -### 测试 BookAppService +打开 `Pages/Books/Index.cshtml` 并按如下代码修改 `abp-card-header` : -在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: +````html + + + + @L["Books"] + + + + + + +```` + +`Index.cshtml` 的内容最终如下所示: + +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Books +@using Microsoft.Extensions.Localization +@model IndexModel +@inject IStringLocalizer L +@section scripts +{ + +} + + + + + + @L["Books"] + + + + + + + + + + +```` + +如下图所示,只是在表格 **右上方** 添加了 **New book** 按钮: + +![bookstore-new-book-button](./images/bookstore-new-book-button-2.png) + +打开 `Pages/book/index.js` 在 `datatable` 配置代码后面添加如下代码: + +````js +var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + +createModal.onResult(function () { + dataTable.ajax.reload(); +}); + +$('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); +}); +```` + +* `abp.ModalManager` 是一个在客户端打开和管理modal的辅助类.它基于Twitter Bootstrap的标准modal组件通过简化的API抽象隐藏了许多细节. +* `createModal.onResult(...)` 用于在创建书籍后刷新数据表格. +* `createModal.open();` 用于打开模态创建新书籍. + +`Index.js` 的内容最终如下所示: + +````js +$(function () { + var l = abp.localization.getResource('BookStore'); + + var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + scrollX: true, + ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), + columnDefs: [ + { + title: l('Name'), + data: "name" + }, + { + title: l('Type'), + data: "type", + render: function (data) { + return l('Enum:BookType:' + data); + } + }, + { + title: l('PublishDate'), + data: "publishDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + }, + { + title: l('Price'), + data: "price" + }, + { + title: l('CreationTime'), data: "creationTime", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(luxon.DateTime.DATETIME_SHORT); + } + } + ] + }) + ); + + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +现在,你可以 **运行程序** 通过新的 modal form 来创建书籍了. + +## 更新书籍 + +在 `Acme.BookStore.Web` 项目的 `Pages/Books` 目录下新建一个名叫 `EditModal.cshtml` 的Razor页面: + +![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) + +### EditModal.cshtml.cs + +打开 `EditModal.cshtml.cs` 文件(`EditModalModel`类) 并替换成以下代码: ````csharp using System; -using System.Linq; using System.Threading.Tasks; -using Xunit; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Validation; -using Microsoft.EntityFrameworkCore.Internal; +using Acme.BookStore.Books; +using Microsoft.AspNetCore.Mvc; -namespace Acme.BookStore +namespace Acme.BookStore.Web.Pages.Books { - public class BookAppService_Tests : BookStoreApplicationTestBase + public class EditModalModel : BookStorePageModel { + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + private readonly IBookAppService _bookAppService; - public BookAppService_Tests() + public EditModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnGetAsync() { - _bookAppService = GetRequiredService(); + var bookDto = await _bookAppService.GetAsync(Id); + Book = ObjectMapper.Map(bookDto); } - [Fact] - public async Task Should_Get_List_Of_Books() + public async Task OnPostAsync() { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); + await _bookAppService.UpdateAsync(Id, Book); + return NoContent(); } } } ```` -* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. +* `[HiddenInput]` 和 `[BindProperty]` 是标准的 ASP.NET Core MVC 特性.这里启用 `SupportsGet` 从Http请求的查询字符串中获取Id的值. +* 在 `OnGetAsync` 方法中,将 `BookAppService.GetAsync` 方法返回的 `BookDto` 映射成 `CreateUpdateBookDto` 并赋值给Book属性. +* `OnPostAsync` 方法直接使用 `BookAppService.UpdateAsync` 来更新实体. -新增测试方法,用以测试创建一个合法book实体的场景: +### BookDto 到 CreateUpdateBookDto 对象映射 -````C# -[Fact] -public async Task Should_Create_A_Valid_Book() +为了执行 `BookDto` 到 `CreateUpdateBookDto` 对象映射,请打开 `Acme.BookStore.Web` 项目中的 `BookStoreWebAutoMapperProfile.cs` 并更改它,如下所示: + +````csharp +using AutoMapper; + +namespace Acme.BookStore.Web { - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto + public class BookStoreWebAutoMapperProfile : Profile + { + public BookStoreWebAutoMapperProfile() { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction + CreateMap(); } - ); + } +} +```` + +* 我们添加了 `CreateMap();` 作为映射定义. + +> 请注意,我们在Web层中进行映射定义是一种最佳实践,因为仅在该层中需要它. - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); +### EditModal.cshtml + +将 `EditModal.cshtml` 页面内容替换成如下代码: + +````html +@page +@using Acme.BookStore.Localization +@using Acme.BookStore.Web.Pages.Books +@using Microsoft.Extensions.Localization +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@inject IStringLocalizer L +@{ + Layout = null; } + + + + + + + + + + ```` -新增测试方法,用以测试创建一个非法book实体失败的场景: +这个页面内容和 `CreateModal.cshtml` 非常相似,除了以下几点: -````csharp -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() +* 它包含`id`属性的`abp-input`, 用于存储编辑书的 `id` (它是隐藏的Input) +* 此页面指定的post地址是`Books/EditModal`. + +### 为表格添加 "操作(Actions)" 下拉菜单 + +我们将为表格每行添加下拉按钮 ("Actions"): + +打开 `Pages/Books/Index.cshtml` 页面,并按下方所示修改表格部分的代码: + +````js +$(function () { + var l = abp.localization.getResource('BookStore'); + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + scrollX: true, + ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + } + ] + } + }, + { + title: l('Name'), + data: "name" + }, + { + title: l('Type'), + data: "type", + render: function (data) { + return l('Enum:BookType:' + data); + } + }, + { + title: l('PublishDate'), + data: "publishDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + }, + { + title: l('Price'), + data: "price" + }, + { + title: l('CreationTime'), data: "creationTime", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(luxon.DateTime.DATETIME_SHORT); + } + } + ] + }) + ); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +* 增加了一个新的 `ModalManager` 名为 `editModal` 打开编辑模态框. +* 在 `columnDefs` 部分的开头添加了一个新列,用于"*Actions*"下拉按钮. +* "*Edit*" 动作简单地调用 `editModal.open()` 打开编辑模态框. +* `editModal.onResult(...)` 当你关闭编程模态框时进行回调刷新数据表格. + +你可以运行应用程序,并通过选择一本书的编辑操作编辑任何一本书. + +最终的UI看起来如下: + +![bookstore-books-table-actions](./images/bookstore-edit-button-2.png) + +## 删除书籍 + +打开 `Pages/book/index.js` 文件,在 `rowAction` `items` 下新增一项: + +````js { - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.books.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } } ```` -* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. +* `confirmMessage` 用来在实际执行 `action` 之前向用户进行确认. +* 通过javascript代理方法 `acme.bookStore.books.book.delete(...)` 执行一个AJAX请求来删除一个book实体. +* `abp.notify.info` 用来在执行删除操作后显示一个toastr通知信息. + +由于我们使用了两个新的本地化文本(`BookDeletionConfirmationMessage`和`SuccesslyDeleted`),因此你需要将它们添加到本地化文件(`Acme.BookStore.Domain.Shared`项目的`Localization/BookStore`文件夹下的`en.json`): + +````json +"BookDeletionConfirmationMessage": "Are you sure to delete the book '{0}'?", +"SuccessfullyDeleted": "Successfully deleted!" +```` + +`Index.js` 的内容最终如下所示: + +````js +$(function () { + var l = abp.localization.getResource('BookStore'); + var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal'); + + var dataTable = $('#BooksTable').DataTable( + abp.libs.datatables.normalizeConfiguration({ + serverSide: true, + paging: true, + order: [[1, "asc"]], + searching: false, + scrollX: true, + ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList), + columnDefs: [ + { + title: l('Actions'), + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return l( + 'BookDeletionConfirmationMessage', + data.record.name + ); + }, + action: function (data) { + acme.bookStore.books.book + .delete(data.record.id) + .then(function() { + abp.notify.info( + l('SuccessfullyDeleted') + ); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { + title: l('Name'), + data: "name" + }, + { + title: l('Type'), + data: "type", + render: function (data) { + return l('Enum:BookType:' + data); + } + }, + { + title: l('PublishDate'), + data: "publishDate", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(); + } + }, + { + title: l('Price'), + data: "price" + }, + { + title: l('CreationTime'), data: "creationTime", + render: function (data) { + return luxon + .DateTime + .fromISO(data, { + locale: abp.localization.currentCulture.name + }).toLocaleString(luxon.DateTime.DATETIME_SHORT); + } + } + ] + }) + ); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +你可以运行程序并尝试删除一本书. + +{{end}} + +{{if UI == "NG"}} + +## 创建新书籍 + +下面的章节中,你将学习到如何创建一个新的模态对话框来新增书籍. + +### BookComponent + +打开 `/src/app/book/book.component.ts` 使用以下内容替换: + +```js +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { BookDto } from './models'; +import { BookService } from './services'; + +@Component({ + selector: 'app-book', + templateUrl: './book.component.html', + styleUrls: ['./book.component.scss'], + providers: [ListService], +}) +export class BookComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; + + isModalOpen = false; // add this line + + constructor(public readonly list: ListService, private bookService: BookService) {} + + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getListByInput(query); + + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); + } + + // add new method + createBook() { + this.isModalOpen = true; + } +} +``` + +* 我们定义了一个名为 `isModalOpen` 的变量和 `createBook` 方法. + +打开 `/src/app/book/book.component.html` 做以下更改: + +```html +
    +
    +
    +
    +
    {%{{{ '::Menu:Books' | abpLocalization }}}%}
    +
    +
    + + +
    + +
    + +
    +
    +
    +
    + +
    +
    + + + + +

    {%{{{ '::NewBook' | abpLocalization }}}%}

    +
    + + + + + + +
    +``` + +* 添加了 `New book` 按钮到卡片头部. +* 添加了 `abp-modal` 渲染模态框,允许用户创建新书. `abp-modal` 是显示模态框的预构建组件. 你也可以使用其它方法显示模态框,但 `abp-modal` 提供了一些附加的好处. + +你可以打开浏览器,点击**New book**按钮看到模态框. + +![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) + +### 添加响应式表单 + +[响应式表单](https://angular.io/guide/reactive-forms) 提供一种模型驱动的方法来处理其值随时间变化的表单输入. + +打开 `/src/app/book/book.component.ts` 使用以下内容替换: + +```js +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { BookDto, BookType } from './models'; // add BookType +import { BookService } from './services'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; // add this + +@Component({ + selector: 'app-book', + templateUrl: './book.component.html', + styleUrls: ['./book.component.scss'], + providers: [ListService], +}) +export class BookComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; + + form: FormGroup; // add this line + + bookType = BookType; // add this line + + // add bookTypes as a list of BookType enum members + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' + ); + + isModalOpen = false; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder // inject FormBuilder + ) {} + + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getListByInput(query); + + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); + } + + createBook() { + this.buildForm(); // add this line + this.isModalOpen = true; + } + + // add buildForm method + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + // add save method + save() { + if (this.form.invalid) { + return; + } + + this.bookService.createByInput(this.form.value).subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); + } +} +``` + +* 导入了 `FormGroup, FormBuilder and Validators`. +* 添加了 `form: FormGroup` 变量. +* 添加了 `bookType` 属性,你可以从模板中获取 `BookType` 枚举成员. +* 添加了 `bookTypes` 属性作为 `BookType` 枚举成员列表. 将在表单选项中使用. +* 我们注入了 `fb: FormBuilder` 服务到构造函数. [FormBuilder](https://angular.io/api/forms/FormBuilder) 服务为生成控件提供了方便的方法. 它减少了构建复杂表单所需的样板文件的数量. +* 我们添加了 `buildForm` 方法到文件末尾, 在 `createBook` 方法调用 `buildForm()` 方法. 该方法创建一个响应式表单去创建新书. +* 添加了`save` 方法. + +打开 `/src/app/book/book.component.html`,使用以下内容替换 ` `: + +```html + + +
    + * + +
    + +
    + * + +
    + +
    + * + +
    + +
    + * + +
    + +
    +``` + +同时使用下面的代码部分替换 ` `: + +````html + + + + + + +```` + +### Datepicker + +我们在这个组件中使用了[NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview). 因此需要添加与此组件相关的依赖项. + +打开 `/src/app/book/book.module.ts` 使用以下内容替换: + +```js +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { BookRoutingModule } from './book-routing.module'; +import { BookComponent } from './book.component'; +import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; // add this line + +@NgModule({ + declarations: [BookComponent], + imports: [ + BookRoutingModule, + SharedModule, + NgbDatepickerModule, // add this line + ] +}) +export class BookModule { } +``` + +* 我们导入了 `NgbDatepickerModule` 来使用日期选择器. + +打开 `/src/app/book/book.component.ts` 使用以内内容替换: + +```js +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { BookDto, BookType } from './models'; +import { BookService } from './services'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; + +// added this line +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-book', + templateUrl: './book.component.html', + styleUrls: ['./book.component.scss'], + providers: [ + ListService, + { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter } // add this line + ], +}) +export class BookComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; + + form: FormGroup; + + bookType = BookType; + + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' + ); + + isModalOpen = false; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} + + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getListByInput(query); + + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + save() { + if (this.form.invalid) { + return; + } + + this.bookService.createByInput(this.form.value).subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); + } +} +``` + +* 导入了 `NgbDateNativeAdapter` 和 `NgbDateAdapter`. +* 我们添加了一个新的 `NgbDateAdapter` 提供程序,它将Datepicker值转换为 `Date` 类型. 有关更多详细信息,请参见[datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview). + +现在你可以打开浏览器看到以下变化: + +![Save button to the modal](./images/bookstore-new-book-form-v2.png) + +## 更新书籍 + +打开 `/src/app/book/book.component.ts` 使用以下内容替换: + +```js +import { ListService, PagedResultDto } from '@abp/ng.core'; +import { Component, OnInit } from '@angular/core'; +import { BookDto, BookType, CreateUpdateBookDto } from './models'; +import { BookService } from './services'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-book', + templateUrl: './book.component.html', + styleUrls: ['./book.component.scss'], + providers: [ListService, { provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], +}) +export class BookComponent implements OnInit { + book = { items: [], totalCount: 0 } as PagedResultDto; + + selectedBook = new BookDto(); // declare selectedBook + + form: FormGroup; + + bookType = BookType; + + bookTypes = Object.keys(this.bookType).filter( + (key) => typeof this.bookType[key] === 'number' + ); + + isModalOpen = false; + + constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder + ) {} + + ngOnInit() { + const bookStreamCreator = (query) => this.bookService.getListByInput(query); + + this.list.hookToQuery(bookStreamCreator).subscribe((response) => { + this.book = response; + }); + } + + createBook() { + this.selectedBook = new BookDto(); // reset the selected book + this.buildForm(); + this.isModalOpen = true; + } + + // Add editBook method + editBook(id: string) { + this.bookService.getById(id).subscribe((book) => { + this.selectedBook = book; + this.buildForm(); + this.isModalOpen = true; + }); + } + + buildForm() { + this.form = this.fb.group({ + name: [this.selectedBook.name || '', Validators.required], + type: [this.selectedBook.type || null, Validators.required], + publishDate: [ + this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, + Validators.required, + ], + price: [this.selectedBook.price || null, Validators.required], + }); + } + + // change the save method + save() { + if (this.form.invalid) { + return; + } + + const request = this.selectedBook.id + ? this.bookService.updateByIdAndInput(this.form.value, this.selectedBook.id) + : this.bookService.createByInput(this.form.value); + + request.subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.list.get(); + }); + } +} +``` + +* 我们声明了类型为 `BookDto` 的 `selectedBook` 变量. +* 我们添加了 `editBook` 方法, 根据给定图书 `Id` 设置 `selectedBook` 对象. +* 我们替换了 `buildForm` 方法使用 `selectedBook` 数据创建表单. +* 我们替换了 `createBook` 方法,设置 `selectedBook` 为空对象. +* 我们替换了 `save` 方法. + +### 添加 "Actions" 下拉框到表格 + +打开 `/src/app/book/book.component.html` 在 `ngx-datatable` 第一列添加 `ngx-datatable-column` 定义: + +```html + + +
    + +
    + +
    +
    +
    +
    +``` + +在表格的第一列添加了一个 "Actions" 下拉菜单,如下图所示: + +![Action buttons](./images/bookstore-actions-buttons.png) + +同时如下所示更改 `ng-template #abpHeader` 部分: + +```html + +

    {%{{{ (selectedBook.id ? '::Edit' : '::NewBook' ) | abpLocalization }}}%}

    +
    +``` + +模板将在标题中显示 **Edit** 文本用于编辑记录操作, **New Book** 用于添加新记录操作. + +## 删除书籍 + +打开 `/src/app/book/book.component.ts` 注入 `ConfirmationService`. + +所示替换构造函数: + +```js +// ... + +// add new imports +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; + +//change the constructor +constructor( + public readonly list: ListService, + private bookService: BookService, + private fb: FormBuilder, + private confirmation: ConfirmationService // inject the ConfirmationService +) {} + +// Add a delete method +delete(id: string) { + this.confirmation.warn('::AreYouSureToDelete', '::AreYouSure').subscribe((status) => { + if (status === Confirmation.Status.confirm) { + this.bookService.deleteById(id).subscribe(() => this.list.get()); + } + }); +} +``` + +* 我们注入了 `ConfirmationService`. +* 我们注入了 `ConfirmationService` 到构造函数. +* 添加了 `delete` 方法. + +> 参阅[确认弹层文档](../UI/Angular/Confirmation-Service)了解该服务的更多信息. + +### 添加删除按钮: + +打开 `/src/app/book/book.component.html` 修改 `ngbDropdownMenu` 添加删除按钮: + +```html +
    + + +
    +``` + +最终操作下拉框UI看起来如下: + +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) + +点击 `delete` 动作调用 `delete` 方法,然后无法显示一个确认弹层如下图所示. + +![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) -打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: +{{end}} -![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) +## 下一章 -恭喜, 绿色图标表示测试已成功通过! +查看本教程的[下一章](Part-4.md). diff --git a/docs/zh-Hans/Tutorials/Part-4.md b/docs/zh-Hans/Tutorials/Part-4.md new file mode 100644 index 0000000000..151c7aab90 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-4.md @@ -0,0 +1,255 @@ +# Web应用程序开发教程 - 第三章: 集成测试 +````json +//[doc-params] +{ + "UI": ["MVC","NG"], + "DB": ["EF","Mongo"] +} +```` +{{ +if UI == "MVC" + UI_Text="mvc" +else if UI == "NG" + UI_Text="angular" +else + UI_Text="?" +end +if DB == "EF" + DB_Text="Entity Framework Core" +else if DB == "Mongo" + DB_Text="MongoDB" +else + DB_Text="?" +end +}} + +## 关于本教程 + +在本系列教程中, 你将构建一个名为 `Acme.BookStore` 的用于管理书籍及其作者列表的基于ABP的应用程序. 它是使用以下技术开发的: + +* **{{DB_Text}}** 做为ORM提供程序. +* **{{UI_Value}}** 做为UI框架. + +本教程分为以下部分: + +- [Part 1: 创建服务端](Part-1.md) +- [Part 2: 图书列表页面](Part-2.md) +- [Part 3: 创建,更新和删除图书](Part-3.md) +- **Part 4: 集成测试**(本章) +- [Part 5: 授权](Part-5.md) +- [Part 6: 作者: 领域层](Part-6.md) +- [Part 7: 作者: 数据库集成](Part-7.md) +- [Part 8: 作者: 应用服务层](Part-8.md) +- [Part 9: 作者: 用户页面](Part-9.md) +- [Part 10: 图书到作者的关系](Part-10.md) + +## 下载源码 + +本教程根据你的**UI** 和 **Database**偏好有多个版,我们准备了两种可供下载的源码组合: + +* [MVC (Razor Pages) UI 与 EF Core](https://github.com/abpframework/abp-samples/tree/master/BookStore-Mvc-EfCore) +* [Angular UI 与 MongoDB](https://github.com/abpframework/abp-samples/tree/master/BookStore-Angular-MongoDb) + +## 解决方案中的测试项目 + +这一部分涵盖了 **服务器端** 测试. 解决方案中有多个测试项目: + +![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png) + +每个项目用于测试相关的应用程序项目.测试项目使用以下库进行测试: + +* [xunit](https://xunit.github.io/) 作为主测试框架. +* [Shoudly](http://shouldly.readthedocs.io/en/latest/) 作为断言库. +* [NSubstitute](http://nsubstitute.github.io/) 作为模拟库. + +{{if DB=="EF"}} + +> 测试项目配置为使用 **SQLite内存** 作为数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. + +{{else if DB=="Mongo"}} + +> **[Mongo2Go](https://github.com/Mongo2Go/Mongo2Go)**库用于模拟MongoDB数据库. 创建一个单独的数据库实例并使用数据种子系统进行初始化种子数据,为每个测试准备一个新的数据库. + +{{end}} + +## 添加测试数据 + +如果你已经按照[第一部分](Part-1.md)中的描述创建了数据种子贡献者,则相同的数据也在测试中可用. 因此你可以跳过此部分. 如果你尚未创建种子贡献者,可以使用 `BookStoreTestDataSeedContributor` 来为要在以下测试中使用的相同数据提供种子. + +## 测试 BookAppService + +在 `Acme.BookStore.Application.Tests` 项目中创建一个名叫 `BookAppService_Tests` 的测试类: + +````csharp +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Xunit; + +namespace Acme.BookStore.Books +{ {{if DB=="Mongo"}} + [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "1984"); + } + } +} +```` + +* 测试方法 `Should_Get_List_Of_Books` 直接使用 `BookAppService.GetListAsync` 方法来获取用户列表,并执行检查. +* 我们可以安全地检查 "1984" 这本书的名称,因为我们知道这本书可以在数据库中找到,我们已将其添加到种子数据中. + +新增测试方法,用以测试创建一个**合法**book实体的场景: + +````csharp +[Fact] +public async Task Should_Create_A_Valid_Book() +{ + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); +} +```` + +新增测试方法,用以测试创建一个非法book实体失败的场景: + +````csharp +[Fact] +public async Task Should_Not_Create_A_Book_Without_Name() +{ + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); +} +```` + +* 由于 `Name` 是空值, ABP 抛出一个 `AbpValidationException` 异常. + +最终的测试类如下所示: + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; +using Xunit; + +namespace Acme.BookStore.Books +{ {{if DB=="Mongo"}} + [Collection(BookStoreTestConsts.CollectionDefinitionName)]{{end}} + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "1984"); + } + + [Fact] + public async Task Should_Create_A_Valid_Book() + { + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); + } + + [Fact] + public async Task Should_Not_Create_A_Book_Without_Name() + { + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); + } + } +} +```` + +打开**测试资源管理器**(测试 -> Windows -> 测试资源管理器)并**执行**所有测试: + +![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) + +恭喜你, **绿色图标**表示测试已成功通过! + +## 下一章 + +查看本教程的[下一章](Part-5.md). \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-5.md b/docs/zh-Hans/Tutorials/Part-5.md new file mode 100644 index 0000000000..602306c484 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-5.md @@ -0,0 +1 @@ +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-6.md b/docs/zh-Hans/Tutorials/Part-6.md new file mode 100644 index 0000000000..602306c484 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-6.md @@ -0,0 +1 @@ +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-7.md b/docs/zh-Hans/Tutorials/Part-7.md new file mode 100644 index 0000000000..602306c484 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-7.md @@ -0,0 +1 @@ +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-8.md b/docs/zh-Hans/Tutorials/Part-8.md new file mode 100644 index 0000000000..602306c484 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-8.md @@ -0,0 +1 @@ +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/Part-9.md b/docs/zh-Hans/Tutorials/Part-9.md new file mode 100644 index 0000000000..602306c484 --- /dev/null +++ b/docs/zh-Hans/Tutorials/Part-9.md @@ -0,0 +1 @@ +TODO.. \ No newline at end of file diff --git a/docs/zh-Hans/Tutorials/images/bookstore-add-migration-authors.png b/docs/zh-Hans/Tutorials/images/bookstore-add-migration-authors.png new file mode 100644 index 0000000000..c24e33c628 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-add-migration-authors.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list-angular.png b/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list-angular.png new file mode 100644 index 0000000000..9fba94d5bf Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list-angular.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list.png new file mode 100644 index 0000000000..f318ddfd31 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-added-author-to-book-list.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-added-authors-to-modals.png b/docs/zh-Hans/Tutorials/images/bookstore-added-authors-to-modals.png new file mode 100644 index 0000000000..5fe4046794 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-added-authors-to-modals.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-authors-page.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-authors-page.png new file mode 100644 index 0000000000..bbd865e44b Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-angular-authors-page.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-angular-service-proxy-author.png b/docs/zh-Hans/Tutorials/images/bookstore-angular-service-proxy-author.png new file mode 100644 index 0000000000..ac231ef7a8 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-angular-service-proxy-author.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-author-domain-layer.png b/docs/zh-Hans/Tutorials/images/bookstore-author-domain-layer.png new file mode 100644 index 0000000000..fde53834bb Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-author-domain-layer.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-author-permissions.png b/docs/zh-Hans/Tutorials/images/bookstore-author-permissions.png new file mode 100644 index 0000000000..a20093d2c4 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-author-permissions.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-authors-page.png b/docs/zh-Hans/Tutorials/images/bookstore-authors-page.png new file mode 100644 index 0000000000..6b8959df54 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-authors-page.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-and-booktype.png b/docs/zh-Hans/Tutorials/images/bookstore-book-and-booktype.png new file mode 100644 index 0000000000..1620ebf150 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-book-and-booktype.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list-3.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list-3.png new file mode 100644 index 0000000000..cbd7ec4a6a Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-book-list-3.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png index ecdb87c737..4982032e64 100644 Binary files a/docs/zh-Hans/Tutorials/images/bookstore-book-list.png and b/docs/zh-Hans/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-dbmigrator-on-solution.png b/docs/zh-Hans/Tutorials/images/bookstore-dbmigrator-on-solution.png new file mode 100644 index 0000000000..0b81900307 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-dbmigrator-on-solution.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-button-2.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-button-2.png new file mode 100644 index 0000000000..507bdd9406 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-edit-button-2.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-edit-delete-actions.png b/docs/zh-Hans/Tutorials/images/bookstore-edit-delete-actions.png new file mode 100644 index 0000000000..9c275c6d15 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-edit-delete-actions.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-getlist-result-network.png b/docs/zh-Hans/Tutorials/images/bookstore-getlist-result-network.png new file mode 100644 index 0000000000..141f9d14ee Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-getlist-result-network.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v3.png b/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v3.png new file mode 100644 index 0000000000..add95788a8 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-index-js-file-v3.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-javascript-proxy-console.png b/docs/zh-Hans/Tutorials/images/bookstore-javascript-proxy-console.png new file mode 100644 index 0000000000..f393c47875 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-javascript-proxy-console.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-author-modal.png b/docs/zh-Hans/Tutorials/images/bookstore-new-author-modal.png new file mode 100644 index 0000000000..dda0dab4c1 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-author-modal.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-2.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-2.png new file mode 100644 index 0000000000..12f1819688 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-2.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-small.png b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-small.png new file mode 100644 index 0000000000..13f7da9285 Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-new-book-button-small.png differ diff --git a/docs/zh-Hans/Tutorials/images/bookstore-permissions-ui.png b/docs/zh-Hans/Tutorials/images/bookstore-permissions-ui.png new file mode 100644 index 0000000000..749f7a013f Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/bookstore-permissions-ui.png differ diff --git a/docs/zh-Hans/Tutorials/images/generated-proxies-2.png b/docs/zh-Hans/Tutorials/images/generated-proxies-2.png new file mode 100644 index 0000000000..adad417c3d Binary files /dev/null and b/docs/zh-Hans/Tutorials/images/generated-proxies-2.png differ diff --git a/docs/zh-Hans/UI/Angular/Custom-Setting-Page.md b/docs/zh-Hans/UI/Angular/Custom-Setting-Page.md index f7854ac6bf..2b888e8d75 100644 --- a/docs/zh-Hans/UI/Angular/Custom-Setting-Page.md +++ b/docs/zh-Hans/UI/Angular/Custom-Setting-Page.md @@ -2,38 +2,33 @@ 不同的模块提供它们的设置选项卡. 你可以通过3个步骤在项目中自定义设置页面. -1. 创建一个组件 +1. 使用以下命令创建一个组件 -```js -import { Select } from '@ngxs/store'; -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-your-custom-settings', - template: ` - custom-settings works! - `, -}) -export class YourCustomSettingsComponent { - // Your component logic -} +```bash +yarn ng generate component my-settings ``` -2. 添加 `YourCustomSettingsComponent` 到 `AppModule` 中的 `declarations`和 `entryComponents` 数组中. - -3. 打开 `app.component.ts` 在 `ngOnInit` 添加以下内容: +2. 打开 `app.component.ts` 做以下修改: ```js -import { addSettingTab } from '@abp/ng.theme.shared'; -// ... - -ngOnInit() { - addSettingTab({ - component: YourCustomSettingsComponent, - name: 'Type here the setting tab title (you can type a localization key, e.g: AbpAccount::Login', - order: 4, - requiredPolicy: 'type here a policy key' - }); +import { Component } from '@angular/core'; +import { SettingTabsService } from '@abp/ng.core'; // imported SettingTabsService +import { MySettingsComponent } from './my-settings/my-settings.component'; // imported MySettingsComponent + +@Component(/* component metadata */) +export class AppComponent { + constructor(private settingTabs: SettingTabsService) // injected MySettingsComponent + { + // added below + settingTabs.add([ + { + name: 'MySettings', + order: 1, + requiredPolicy: 'policy key here', + component: MySettingsComponent, + }, + ]); + } } ``` diff --git a/docs/zh-Hans/UI/Angular/Localization.md b/docs/zh-Hans/UI/Angular/Localization.md index 60af06a976..9ddb414234 100644 --- a/docs/zh-Hans/UI/Angular/Localization.md +++ b/docs/zh-Hans/UI/Angular/Localization.md @@ -146,16 +146,6 @@ this.store.selectSnapshot( "build": { "options": { "styles": [ - { - "input": "node_modules/@abp/ng.theme.shared/styles/bootstrap-rtl.min.css", - "inject": false, - "bundleName": "bootstrap-rtl.min" - }, - { - "input": "node_modules/bootstrap/dist/css/bootstrap.min.css", - "inject": true, - "bundleName": "bootstrap-ltr.min" - }, { "input": "node_modules/@fortawesome/fontawesome-free/css/all.min.css", "inject": true, @@ -166,6 +156,16 @@ this.store.selectSnapshot( "inject": true, "bundleName": "fontawesome-v4-shims.min" }, + { + "input": "node_modules/@abp/ng.theme.shared/styles/bootstrap-rtl.min.css", + "inject": false, + "bundleName": "bootstrap-rtl.min" + }, + { + "input": "node_modules/bootstrap/dist/css/bootstrap.min.css", + "inject": true, + "bundleName": "bootstrap-ltr.min" + }, "apps/dev-app/src/styles.scss" ], } @@ -174,6 +174,7 @@ this.store.selectSnapshot( } } } +``` #### 步骤 2. 清除AppComponent中延迟加载的Fontawesome @@ -192,6 +193,32 @@ import { Component } from '@angular/core'; export class AppComponent {} ``` +## 文化名称到语言环境文件名的映射 + +.NET中定义的某些文化名称与Angular语言环境不匹配. 在这种情况下Angular应用程序在运行时会引发如下错误: + +![locale-error](./images/locale-error.png) + +如果你看到这样的错误,你应该像下面这样传递 `cultureNameLocaleFileMap` 属性到CoreModule的forRoot静态方法. + +```js +// app.module.ts + +@NgModule({ + imports: [ + // other imports + CoreModule.forRoot({ + // other options + cultureNameLocaleFileMap: { + "DotnetCultureName": "AngularLocaleFileName", + "pt-BR": "pt" // example + } + }) + //... +``` + +查看 [Angular中所有的语言环境文件](https://github.com/angular/angular/tree/master/packages/common/locales). + ## 另请参阅 * [ASP.NET Core中的本地化](../../Localization.md) diff --git a/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md b/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md index 36afffa54f..eb16c21e03 100644 --- a/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md +++ b/docs/zh-Hans/UI/Angular/Migration-Guide-v3.md @@ -18,7 +18,7 @@ yarn ng update @angular/cli @angular/core --force - 更新你的package.json并安装新的软件包 - 修改tsconfig.json文件创建一个"Solution Style"配置 -- 重命名 `browserlist` 为 `.browserlistrc` +- 重命名 `browserslist` 为 `.browserslistrc` 另一方面,如果你单独使用 `yarn ng update` 命令检查首先要更新哪些包会更好. Angular会给你一个要更新的包列表. @@ -144,42 +144,33 @@ export class AppModule {} AppRoutingModule: ```js -import { DynamicLayoutComponent } from '@abp/ng.core'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: '', - component: DynamicLayoutComponent, - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('./home/home.module') - .then(m => m.HomeModule), - }, - { - path: 'account', - loadChildren: () => import('@abp/ng.account') - .then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), - }, - { - path: 'identity', - loadChildren: () => import('@abp/ng.identity') - .then(m => m.IdentityModule.forLazy()), - }, - { - path: 'tenant-management', - loadChildren: () => import('@abp/ng.tenant-management') - .then(m => m.TenantManagementModule.forLazy()), - }, - { - path: 'setting-management', - loadChildren: () => import('@abp/ng.setting-management') - .then(m => m.SettingManagementModule.forLazy()), - }, - ], + pathMatch: 'full', + loadChildren: () => import('./home/home.module').then(m => m.HomeModule), + }, + { + path: 'account', + loadChildren: () => + import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })), + }, + { + path: 'identity', + loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()), + }, + { + path: 'tenant-management', + loadChildren: () => + import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()), + }, + { + path: 'setting-management', + loadChildren: () => + import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()), }, ]; @@ -190,7 +181,7 @@ const routes: Routes = [ export class AppRoutingModule {} ``` -> 你可能已经注意到我们在top级别路由组件上使用了 `DynamicLayoutComponent`. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情. +> 你可能已经注意到我们在AppComponent模板中使用了 `` 而不是 ``. 我们这样做是为了避免不必要的渲染和闪烁. 这不是强制的,但是我们建议在你的应用程序路由中做同样的事情. #### 如何迁移? @@ -200,7 +191,7 @@ export class AppRoutingModule {} - 调用 `ThemeBasicModule` 的静态 `forRoot` 方法(或商业上的 `ThemeLeptonModule`),并从导入中删除 `SharedModule`(除非已在其中添加了根模块所需的任何内容). - 在app路由模块中直接导入延迟ABP模块 (如 `() => import('@abp/ng.identity').then(...)`). - 在所有延迟模块 `then` 中调用的静态 `forLazy` 方法,即使配置没有被传递. -- [可选]使用 `DynamicLayoutComponent` 添加空的父路由,获得更好的性能和UX. +- [可选]添加 `` 到AppComponent模板并且删除 ``,获得更好的性能和UX. ### RoutesService @@ -233,9 +224,10 @@ export class AppRoutingModule {} 从ABP v3开始,我们已切换到经过严格测试,执行良好的数据表格:[ngx-datatable](https://github.com/swimlane/ngx-datatable). 所有的ABP模块都已经实现了ngx-datatable. `ThemeSharedModule` 已经导出了 `NgxDatatableModule`. 因此如果你在终端运行 `yarn add @swimlane/ngx-datatable` 来安装这个包,它将在你的应用的所有模块中可用. -为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容: +为了正确设置样式,你需要在angular.json文件的样式部分中添加以下内容(在其他所有元素之上): ```json +"styles": [ { "input": "node_modules/@swimlane/ngx-datatable/index.css", "inject": true, @@ -250,7 +242,9 @@ export class AppRoutingModule {} "input": "node_modules/@swimlane/ngx-datatable/themes/material.css", "inject": true, "bundleName": "ngx-datatable-material" - } + }, + // other styles +] ``` 由于尚未删除 `abp-table`, 因此以前由ABP v2.x构建的模块不会突然丢失所有. 但是它们的外观与内置ABP v3模块有所不同, 因此你可能希望将这些模块中的表转换为ngx-datatable. 为了减少将abp-table转换为ngx-datatable所需的工作量,我们修改了 `ListService` 以使其与 `ngx-datatable` 一起很好地工作,并引入了两个新指令: `NgxDatatableListDirective` 和 `NgxDatatableDefaultDirective`. @@ -348,7 +342,103 @@ export class SomeComponent { - 如果可以的话,根据上面的例子更新你的模. - 如果你稍后需要这样做,并且打算保留abp-table一段时间,请确保根据此处描述的[破坏性更改](List-Service.md)更新分页. -**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable。 +**重要说明:**abp-table没有被删除,但已被弃用并在以后的版本中删除. 请考虑切换到ngx-datatable. + +### 扩展系统[商业版] + +扩展程序系统现在是开源的, 可以从 `@abp/ng.theme.shared/extensions` 而不是从 `@volo/abp.commercial.ng.ui` 中获取. 同样,根据config软件包的新结构,如上所述通过 `forLazy` 静态方法进行配置. + +#### 如何迁移? + +如果你以前从未使用过扩展系统,则无需执行任何操作. 否则请再次检查文档以查看更改. 扩展系统本身的工作原理与以前相同,唯一的变化是你从中导入的包,静态方法以及您将贡献者传递给的模块. + + +### Lepton 主题Logo [商业版] + +在ABP v2.x中,Lepton每个颜色主题都有一个亮徽标和一个暗徽标. 我们意识到我们可以使它仅使用一个浅色和一个深色徽标. 因此我们更改了Lepton查找徽标图像的方式,现在你只需要在项目中包含 `logo-light.png` 和 `logo-dark.png`. + +#### 如何迁移? + +如果你之前已切换模板徽标PNG,则更改很简单: + +- 转到 `/assets/images/logo` 目录. +- 重命名 `theme1.png` 为 `logo-light.png` 并且重命名 `theme1-reverse.png` 为 `logo-dark.png`. +- 删除所有其他 `theme*.png` 文件. + +如果你更换了徽标组件,则更改有些不同,但仍然很简单. `LayoutStateService` 有两个新成员: `primaryLogoColor` 和 `secondaryLogoColor`. 它们有 `'light'` 和 `'dark'` 设置值做为可观察流. 你可以使用 `async` 管道在自定义徽标组件模板中使用它们的值. 这是一个完整的示例,其中涵盖了主要和辅助(帐户)布局徽标. + +```js +import { AddReplaceableComponent } from '@abp/ng.core'; +import { CommonModule } from '@angular/common'; +import { APP_INITIALIZER, Component, Injector, NgModule } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { eAccountComponents } from '@volo/abp.ng.account'; +import { + AccountLayoutComponent, + eThemeLeptonComponents, + LayoutStateService, +} from '@volo/abp.ng.theme.lepton'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Component({ + template: ` + + + + + + `, +}) +export class LogoComponent { + isAccount: boolean; + + logoColor: Observable<'dark' | 'light'>; + + get logoUrl() { + return this.logoColor.pipe(map(color => `url(/assets/images/logo/logo-${color}.png)`)); + } + + constructor(injector: Injector) { + const layout = injector.get(LayoutStateService); + this.isAccount = Boolean(injector.get(AccountLayoutComponent, false)); + this.logoColor = this.isAccount ? layout.secondaryLogoColor : layout.primaryLogoColor; + } +} + +@NgModule({ + imports: [CommonModule], + declarations: [LogoComponent], + exports: [LogoComponent], +}) +export class LogoModule {} + +export const APP_LOGO_PROVIDER = [ + { provide: APP_INITIALIZER, useFactory: switchLogos, multi: true, deps: [Store] }, +]; + +export function switchLogos(store: Store) { + return () => { + store.dispatch( + new AddReplaceableComponent({ + component: LogoComponent, + key: eThemeLeptonComponents.Logo, + }), + ); + + store.dispatch( + new AddReplaceableComponent({ + component: LogoComponent, + key: eAccountComponents.Logo, + }), + ); + }; +} +``` + +只要将 `APP_LOGO_PROVIDER` 添加到根模块的提供程序(通常是 `AppModule` ),你就会有一个调整主题颜色的自定义徽标组件. ### 过时的接口 diff --git a/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md b/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md index 1f4e626bd3..82c1fe41ad 100644 --- a/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md +++ b/docs/zh-Hans/UI/Angular/Modifying-the-Menu.md @@ -2,8 +2,6 @@ 菜单在 @abp/ng.theme.basic包 `ApplicationLayoutComponent` 内部. 有几种修改菜单的方法,本文档介绍了这些方法. 如果你想完全替换菜单,请参考[组件替换文档]了解如何替换布局. - - ## 如何添加Logo 环境变量中的 `logoUrl` 是logo的url. @@ -212,51 +210,65 @@ this.routes.remove(['Your navigation']); ## 如何在菜单的右侧添加元素 -右侧的元素存储在 @abp/ng.theme.basic 包的 `LayoutState` 中. - -`LayoutStateService` 的 `dispatchAddNavigationElement` 方法添加元素到右侧的菜单. - -你可以通过将模板添加到 `app.component` 调用 `dispatchAddNavigationElement` 方法来插入元素: +你可以通过调用 `NavItemsService` 的 `addItems` 方法将元素添加到菜单的右侧. 这是一个单例服务,即以根身份提供. 因此你可以立即注入并使用它. ```js -import { Layout, LayoutStateService } from '@abp/ng.theme.basic'; // added this line +import { NavItemsService } from '@abp/ng.theme.shared'; +import { Component } from '@angular/core'; @Component({ - selector: 'app-root', template: ` - - - + `, }) -export class AppComponent { - // Added ViewChild - @ViewChild('search', { static: false, read: TemplateRef }) searchElementRef: TemplateRef; - - constructor(private layout: LayoutStateService) {} // injected LayoutStateService +export class MySearchInputComponent {} - // Added ngAfterViewInit - ngAfterViewInit() { - const newElement = { - name: 'Search', - element: this.searchElementRef, - order: 1, - } as Layout.NavigationElement; - this.layout.dispatchAddNavigationElement(newElement); +@Component(/* component metadata */) +export class AppComponent { + constructor(private navItems: NavItemsService) { + navItems.addItems([ + { + id: 'MySearchInput', + order: 1, + component: MySearchInputComponent, + }, + { + id: 'SignOutIcon', + html: '', + action: () => console.log('Clicked the sign out icon'), + order: 101, // puts as last element + }, + ]); } } ``` -上面我们在菜单添加了一个搜索输入,最终UI如下: +上面我们在菜单添加了一个搜索输入和退出登录图标,最终UI如下: ![navigation-menu-search-input](./images/navigation-menu-search-input.png) -## 如何删除右侧菜单元素 +> 默认元素的排序为 `100`. 如果要将自定义元素放在默认值之前,请指定一个排序,最高为 `99`. 如果要将自定义元素放在默认值之后,请指定排序从 `101` 开始. 最后如果必须在默认值之间放置一个项目,请按如下所述修补默认元素顺序. 但有一个警告:我们将来可能会添加另一个默认元素,排序也为 `100`. + +## 如何修补或删除右侧部分元素 + +`NavItemsService` 的 `patchItem` 方法通过 `id` 查找元素,并将配置替换为第二个参数传递的新配置. `removeItem` 方法会找到一个元素并删除. + +```js +export class AppComponent { + constructor(private navItems: NavItemsService) { + navItems.patchItem(eThemeBasicComponents.Languages, { + requiredPolicy: 'new policy here', + order: 1, + }); + + navItems.removeItem(eThemeBasicComponents.CurrentUser); + } +} +``` -TODO +* 使用新的 `requiredPolicy` 和新的 `order` 修补了语言下拉菜单元素. +* 删除了当前用户的下拉菜单元素. ## 下一步是什么? diff --git a/docs/zh-Hans/UI/Angular/Permission-Management.md b/docs/zh-Hans/UI/Angular/Permission-Management.md index 5c1ba5b4fb..7f28035105 100644 --- a/docs/zh-Hans/UI/Angular/Permission-Management.md +++ b/docs/zh-Hans/UI/Angular/Permission-Management.md @@ -55,7 +55,9 @@ export class YourComponent { 如果你想要在导航过程中控制经过身份验证的用户对路由的访问权限,可以使用 `PermissionGuard`. -将 `requiredPolicy` 添加到路由模块中的 `routes`属性. +* 从@abp/ng.core导入PermissionGuard. +* 添加 `canActivate: [PermissionGuard]` 到你的路由对象. +* 添加 `requiredPolicy` 到路由模块路由的 `data` 属性. ```js import { PermissionGuard } from '@abp/ng.core'; @@ -66,9 +68,7 @@ const routes: Routes = [ component: YourComponent, canActivate: [PermissionGuard], data: { - routes: { - requiredPolicy: 'AbpIdentity.Roles.Create', - }, + requiredPolicy: 'YourProjectName.YourComponent', // policy key for your component }, }, ]; diff --git a/docs/zh-Hans/UI/Angular/Service-Proxies.md b/docs/zh-Hans/UI/Angular/Service-Proxies.md index 29aafebdae..2824bc44a7 100644 --- a/docs/zh-Hans/UI/Angular/Service-Proxies.md +++ b/docs/zh-Hans/UI/Angular/Service-Proxies.md @@ -33,7 +33,7 @@ abp generate-proxy 服务的 `providerIn` 属性定义为 `'root'`. 因此无需将服务作为提供程序添加到模块. 你可以通过将服务注入到构造函数中来使用它,如下所示: ```js -import { AbpApplicationConfigurationService } from '../app/shared/services'; +import { AbpApplicationConfigurationService } from '../abp/applicationconfiguration/services'; //... export class HomeComponent{ @@ -49,14 +49,14 @@ Angular编译器会从最终输出中删除那些没有被注入的服务. 参 ### Models -生成的模型与后端中的dto匹配. 每个模型在 `src/app/*/shared/models` 文件夹生成一个类. +生成的模型与后端中的dto匹配. 每个模型在 `src/app/*/models` 文件夹生成一个类. `@abp/ng.core` 包有一些[基类](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/dtos.ts). 一些模型扩展了这些类. 可以如下所示创建一个类的实例: ```js -import { IdentityRoleCreateDto } from '../identity/shared/models'; +import { IdentityRoleCreateDto } from '../identity/role/models' //... const instance = new IdentityRoleCreateDto({name: 'Role 1', isDefault: false, isPublic: true}) ``` diff --git a/docs/zh-Hans/UI/Angular/Subscription-Service.md b/docs/zh-Hans/UI/Angular/Subscription-Service.md new file mode 100644 index 0000000000..fb248d4d95 --- /dev/null +++ b/docs/zh-Hans/UI/Angular/Subscription-Service.md @@ -0,0 +1,205 @@ +# 轻松取消将你的观察取消订阅 + +`SubscriptionService` 是一个实用工具服务,它提供了一个简单的取消订阅Angular组件和指令中的RxJS可观察对象的功能. 请参见[为什么在实例销毁时要取消订阅可观察对象](https://angular.io/guide/lifecycle-hooks#cleaning-up-on-instance-destruction). + +## 入门 + +你必须在组件或指令级别提供 `SubscriptionService`,因为它没有在**根中提供**,而且它与组件/指令的生命周期同步. 只有在此之后,您才能注入并开始使用它。 + +```js +import { SubscriptionService } from '@abp/ng.core'; + +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent { + count$ = interval(1000); + + constructor(private subscription: SubscriptionService) { + this.subscription.addOne(this.count$, console.log); + } +} +``` + +由 `count$` 发出的值将被记录下来,直到组件被销毁. 你不必手动退订. + +> 请不要尝试使用单例 `SubscriptionService`. 这是行不通的. + +## 用法 + +## 如何订阅可观察对象 + +你可以传递 `next` 函数和 `error` 函数. + + + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + const source$ = interval(1000); + const nextFn = value => console.log(value * 2); + const errorFn = error => { + console.error(error); + return of(null); + }; + + this.subscription.addOne(source$, nextFn, errorFn); + } +} +``` + +或者,你可以传递一个观察者. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + const source$ = interval(1000); + const observer = { + next: value => console.log(value * 2), + complete: () => console.log('DONE'), + }; + + this.subscription.addOne(source$, observer); + } +} +``` + +`addOne` 方法返回单个订阅以便你以后使用它. 有关详细信息,请参见下面的主题. + +### 实例销毁之前如何退订 + +有两种方法可以做到这一点. 如果你不想再次订阅.可以使用 `closeAll` 方法. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.subscription.addOne(interval(1000), console.log); + } + + onSomeEvent() { + this.subscription.closeAll(); + } +} +``` + +这将清除所有订阅,你将无法再次订阅. 如果你打算添加另一个订阅,可以使用`reset`方法. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.subscription.addOne(interval(1000), console.log); + } + + onSomeEvent() { + this.subscription.reset(); + this.subscription.addOne(interval(1000), console.warn); + } +} +``` + +### 如何取消单个订阅 + +有时你可能需要取消订阅特定的订阅,但保留其他订阅. 在这种情况下,你可以使用 `closeOne` 方法. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + countSubscription: Subscription; + + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.countSubscription = this.subscription.addOne( + interval(1000), + console.log + ); + } + + onSomeEvent() { + this.subscription.closeOne(this.countSubscription); + console.log(this.countSubscription.closed); // true + } +} +``` + +### 如何从跟踪的订阅中删除单个订阅 + +你可能需要控制特定的订阅. 在这种情况下你可以使用 `removeOne` 方法将其从跟踪的订阅中删除. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + countSubscription: Subscription; + + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.countSubscription = this.subscription.addOne( + interval(1000), + console.log + ); + } + + onSomeEvent() { + this.subscription.removeOne(this.countSubscription); + console.log(this.countSubscription.closed); // false + } +} +``` + +### 如何检查是否全部取消订阅 + +使用 `isClosed` 检查 `closeAll` 是否被调用. + +```js +@Component({ + /* class metadata here */ + providers: [SubscriptionService], +}) +class DemoComponent implements OnInit { + constructor(private subscription: SubscriptionService) {} + + ngOnInit() { + this.subscription.addOne(interval(1000), console.log); + } + + onSomeEvent() { + console.log(this.subscription.isClosed); // false + } +} +``` + +## 下一步是什么? + +- [ListService](./List-Service.md) diff --git a/docs/zh-Hans/UI/Angular/Track-By-Service.md b/docs/zh-Hans/UI/Angular/Track-By-Service.md index 53c9dcbf1c..f941b1f0ca 100644 --- a/docs/zh-Hans/UI/Angular/Track-By-Service.md +++ b/docs/zh-Hans/UI/Angular/Track-By-Service.md @@ -99,4 +99,4 @@ class DemoComponent { ## 下一步是什么? -- [ListService](./List-Service.md) \ No newline at end of file +- [SubscriptionService](./Subscription-Service.md) \ No newline at end of file diff --git a/docs/zh-Hans/UI/Angular/images/custom-settings.png b/docs/zh-Hans/UI/Angular/images/custom-settings.png index 05dd3f695a..dc40ad3fa9 100644 Binary files a/docs/zh-Hans/UI/Angular/images/custom-settings.png and b/docs/zh-Hans/UI/Angular/images/custom-settings.png differ diff --git a/docs/zh-Hans/UI/Angular/images/generated-files-via-generate-proxy.png b/docs/zh-Hans/UI/Angular/images/generated-files-via-generate-proxy.png index 77a120a35d..0d062cc2d3 100644 Binary files a/docs/zh-Hans/UI/Angular/images/generated-files-via-generate-proxy.png and b/docs/zh-Hans/UI/Angular/images/generated-files-via-generate-proxy.png differ diff --git a/docs/zh-Hans/UI/Angular/images/locale-error.png b/docs/zh-Hans/UI/Angular/images/locale-error.png new file mode 100644 index 0000000000..de385a59c0 Binary files /dev/null and b/docs/zh-Hans/UI/Angular/images/locale-error.png differ diff --git a/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png b/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png index ebdc05e3e0..e93b7927d1 100644 Binary files a/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png and b/docs/zh-Hans/UI/Angular/images/navigation-menu-search-input.png differ diff --git a/docs/zh-Hans/UI/AspNetCore/Dynamic-JavaScript-Proxies.md b/docs/zh-Hans/UI/AspNetCore/Dynamic-JavaScript-Proxies.md new file mode 100644 index 0000000000..391a63a910 --- /dev/null +++ b/docs/zh-Hans/UI/AspNetCore/Dynamic-JavaScript-Proxies.md @@ -0,0 +1,3 @@ +# Dynamic JavaScript HTTP API Proxies + +TODO \ No newline at end of file diff --git a/docs/zh-Hans/Unit-Of-Work.md b/docs/zh-Hans/Unit-Of-Work.md index 0fbf5762f0..b937c86ac0 100644 --- a/docs/zh-Hans/Unit-Of-Work.md +++ b/docs/zh-Hans/Unit-Of-Work.md @@ -21,7 +21,7 @@ ABP的UOW系统是; UOW自动针对这些方法开始,除非**周围已经有一个(环境)**UOW在运行.示例; -* 如果你调用一个[仓储]方法(Repositories.md),但还没有启动UOW,它将自动**启动一个新的事务UOW**,其中包括在仓储方法中完成的所有操作,如果仓储方法没有抛出任何异常,则**提交事务**. 仓储方法根本不知道UOW或事务. 它只在一个常规的数据库对象上工作(例如用于[EF Core](Entity-Framework-Core.md)的`DbContext`),而UOW由ABP框架处理. +* 如果你调用一个[仓储](Repositories.md)方法,但还没有启动UOW,它将自动**启动一个新的事务UOW**,其中包括在仓储方法中完成的所有操作,如果仓储方法没有抛出任何异常,则**提交事务**. 仓储方法根本不知道UOW或事务. 它只在一个常规的数据库对象上工作(例如用于[EF Core](Entity-Framework-Core.md)的`DbContext`),而UOW由ABP框架处理. * 如果调用[应用服务](Application-Services.md)方法,则相同的UOW系统将按上述说明工作. 如果应用服务方法使用某些仓储,这些仓储**不会开始新的UOW**,而是**参与由ABP框架为应用程序服务方法启动的当前工作单元中**. * ASP.NET Core控制器操作也是如此. 如果操作以控制器action开始,**UOW范围是控制器action的方法主体**. @@ -352,4 +352,4 @@ public async Task CreateAsync(string name) ````csharp app.UseUnitOfWork(); app.UseConfiguredEndpoints(); -```` \ No newline at end of file +```` diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index 683bf750c0..26a526dd10 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -80,6 +80,15 @@ "text": "CLI", "path": "CLI.md" }, + { + "text": "认证", + "items": [ + { + "text": "社交/外部登录", + "path": "Authentication/Social-External-Logins.md" + } + ] + }, { "text": "基础知识", "items": [ @@ -440,6 +449,10 @@ "text": "TrackByService", "path": "UI/Angular/Track-By-Service.md" }, + { + "text": "SubscriptionService", + "path": "UI/Angular/Subscription-Service.md" + }, { "text": "ListService", "path": "UI/Angular/List-Service.md" diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj index af317c263c..1d30366e67 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj index 8ce2077235..111a6a898a 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Demo.csproj @@ -24,11 +24,11 @@ - + - + - + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj index 47899625d4..e190116b2f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj @@ -26,9 +26,9 @@ - + - + diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index b5c5699757..617650d0e2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -92,7 +92,9 @@ namespace Volo.Abp.AspNetCore.Mvc var uniqueMethodName = GetUniqueActionName(method); if (controllerModel.Actions.ContainsKey(uniqueMethodName)) { - Logger.LogWarning($"Controller '{controllerModel.ControllerName}' contains more than one action with name '{uniqueMethodName}' for module '{moduleModel.RootPath}'. Ignored: " + method); + Logger.LogWarning( + $"Controller '{controllerModel.ControllerName}' contains more than one action with name '{uniqueMethodName}' for module '{moduleModel.RootPath}'. Ignored: " + + method); return; } @@ -119,11 +121,14 @@ namespace Volo.Abp.AspNetCore.Mvc private static string CalculateControllerName(Type controllerType, ConventionalControllerSetting setting) { - var controllerName = controllerType.Name.RemovePostFix("Controller").RemovePostFix(ApplicationService.CommonPostfixes); + var controllerName = controllerType.Name.RemovePostFix("Controller") + .RemovePostFix(ApplicationService.CommonPostfixes); if (setting?.UrlControllerNameNormalizer != null) { - controllerName = setting.UrlControllerNameNormalizer(new UrlControllerNameNormalizerContext(setting.RootPath, controllerName)); + controllerName = + setting.UrlControllerNameNormalizer( + new UrlControllerNameNormalizerContext(setting.RootPath, controllerName)); } return controllerName; @@ -152,7 +157,8 @@ namespace Volo.Abp.AspNetCore.Mvc return methodNameBuilder.ToString(); } - private static List GetSupportedVersions(Type controllerType, MethodInfo method, ConventionalControllerSetting setting) + private static List GetSupportedVersions(Type controllerType, MethodInfo method, + ConventionalControllerSetting setting) { var supportedVersions = new List(); @@ -185,13 +191,19 @@ namespace Volo.Abp.AspNetCore.Mvc AddCustomTypesToModel(applicationModel, method.ReturnType); } - private static void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, [CanBeNull] Type type) + private static void AddCustomTypesToModel(ApplicationApiDescriptionModel applicationModel, + [CanBeNull] Type type) { if (type == null) { return; } + if (type.IsGenericParameter) + { + return; + } + type = AsyncHelper.UnwrapTask(type); if (type == typeof(object) || @@ -216,28 +228,54 @@ namespace Volo.Abp.AspNetCore.Mvc return; } - /* TODO: Add interfaces - */ + if (type.IsGenericType && !type.IsGenericTypeDefinition) + { + var genericTypeDefinition = type.GetGenericTypeDefinition(); + + AddCustomTypesToModel(applicationModel, genericTypeDefinition); + + foreach (var genericArgument in type.GetGenericArguments()) + { + AddCustomTypesToModel(applicationModel, genericArgument); + } - var typeName = TypeHelper.GetFullNameHandlingNullableAndGenerics(type); + return; + } + var typeName = CalculateTypeName(type); if (applicationModel.Types.ContainsKey(typeName)) { return; } - var typeModel = TypeApiDescriptionModel.Create(type); - applicationModel.Types[typeName] = typeModel; + applicationModel.Types[typeName] = TypeApiDescriptionModel.Create(type); AddCustomTypesToModel(applicationModel, type.BaseType); - foreach (var propertyInfo in type.GetProperties()) + foreach (var propertyInfo in type.GetProperties().Where(p => p.DeclaringType == type)) { AddCustomTypesToModel(applicationModel, propertyInfo.PropertyType); } } - private void AddParameterDescriptionsToModel(ActionApiDescriptionModel actionModel, MethodInfo method, ApiDescription apiDescription) + private static string CalculateTypeName(Type type) + { + if (!type.IsGenericTypeDefinition) + { + return TypeHelper.GetFullNameHandlingNullableAndGenerics(type); + } + + var i = 0; + var argumentList = type + .GetGenericArguments() + .Select(_ => "T" + i++) + .JoinAsString(","); + + return $"{type.FullName.Left(type.FullName.IndexOf('`'))}<{argumentList}>"; + } + + private void AddParameterDescriptionsToModel(ActionApiDescriptionModel actionModel, MethodInfo method, + ApiDescription apiDescription) { if (!apiDescription.ParameterDescriptions.Any()) { @@ -253,8 +291,8 @@ namespace Volo.Abp.AspNetCore.Mvc { var parameterDescription = apiDescription.ParameterDescriptions[i]; var matchedMethodParamName = matchedMethodParamNames.Length > i - ? matchedMethodParamNames[i] - : parameterDescription.Name; + ? matchedMethodParamNames[i] + : parameterDescription.Name; actionModel.AddParameter(ParameterApiDescriptionModel.Create( parameterDescription.Name, @@ -286,7 +324,8 @@ namespace Volo.Abp.AspNetCore.Mvc return modelNameProvider.Name ?? parameterInfo.Name; } - private static string GetRootPath([NotNull] Type controllerType, [CanBeNull] ConventionalControllerSetting setting) + private static string GetRootPath([NotNull] Type controllerType, + [CanBeNull] ConventionalControllerSetting setting) { if (setting != null) { @@ -309,7 +348,8 @@ namespace Volo.Abp.AspNetCore.Mvc return setting.RemoteServiceName; } - var remoteServiceAttr = controllerType.GetCustomAttributes().OfType().FirstOrDefault(); + var remoteServiceAttr = + controllerType.GetCustomAttributes().OfType().FirstOrDefault(); if (remoteServiceAttr?.Name != null) { return remoteServiceAttr.Name; diff --git a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj index a4dcd1c8c5..ce02e1f0a0 100644 --- a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj +++ b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj @@ -24,7 +24,7 @@ - + diff --git a/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj b/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj index b8ff62572b..a7e76714c2 100644 --- a/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj +++ b/framework/src/Volo.Abp.Authorization/Volo.Abp.Authorization.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj index 3c5c275117..85a8541e26 100644 --- a/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj +++ b/framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj @@ -17,7 +17,7 @@ - + diff --git a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj index c362c16bff..95a15ddc8e 100644 --- a/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj +++ b/framework/src/Volo.Abp.Caching.StackExchangeRedis/Volo.Abp.Caching.StackExchangeRedis.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj index 5ee096a493..19d5b456ec 100644 --- a/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj +++ b/framework/src/Volo.Abp.Caching/Volo.Abp.Caching.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmApplicationType.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmApplicationType.cs index 9f58aa52e9..0e05a1ffb0 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmApplicationType.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmApplicationType.cs @@ -5,6 +5,7 @@ namespace Volo.Abp.Cli.ProjectModification [Flags] public enum NpmApplicationType { - Mvc = 1 + Mvc = 1, + Angular = 2 } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs index acf4231520..a19af6f52c 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs @@ -39,7 +39,7 @@ namespace Volo.Abp.Cli.ProjectModification EfCoreMigrationAdder efCoreMigrationAdder, DerivedClassFinder derivedClassFinder, ProjectNpmPackageAdder projectNpmPackageAdder, - NpmGlobalPackagesChecker npmGlobalPackagesChecker, + NpmGlobalPackagesChecker npmGlobalPackagesChecker, IRemoteServiceExceptionHandler remoteServiceExceptionHandler, SourceCodeDownloadService sourceCodeDownloadService, SolutionFileModifier solutionFileModifier, @@ -159,7 +159,7 @@ namespace Volo.Abp.Cli.ProjectModification { Directory.Delete(demoFolder, true); } - + var hostFolder = Path.Combine(targetModuleFolder, "host"); if (Directory.Exists(hostFolder)) { @@ -186,7 +186,9 @@ namespace Volo.Abp.Cli.ProjectModification await ProjectNugetPackageAdder.AddAsync(targetProjectFile, nugetPackage); } - if (!module.NpmPackages.IsNullOrEmpty()) + var mvcNpmPackages = module.NpmPackages?.Where(p => p.ApplicationType.HasFlag(NpmApplicationType.Mvc)).ToList(); + + if (!mvcNpmPackages.IsNullOrEmpty()) { var targetProjects = ProjectFinder.FindNpmTargetProjectFile(projectFiles); if (targetProjects.Any()) @@ -195,8 +197,7 @@ namespace Volo.Abp.Cli.ProjectModification foreach (var targetProject in targetProjects) { - foreach (var npmPackage in module.NpmPackages.Where(p => - p.ApplicationType.HasFlag(NpmApplicationType.Mvc))) + foreach (var npmPackage in mvcNpmPackages) { await ProjectNpmPackageAdder.AddAsync(Path.GetDirectoryName(targetProject), npmPackage); } @@ -222,7 +223,7 @@ namespace Volo.Abp.Cli.ProjectModification } var dbMigrationsProject = projectFiles.FirstOrDefault(p => p.EndsWith(".DbMigrations.csproj")); - + if (dbMigrationsProject == null) { Logger.LogDebug("Solution doesn't have a \".DbMigrations\" project."); @@ -241,7 +242,7 @@ namespace Volo.Abp.Cli.ProjectModification if (addedNewBuilder && !skipDbMigrations) { - EfCoreMigrationAdder.AddMigration(dbMigrationsProject, module.Name, startupProject); + EfCoreMigrationAdder.AddMigration(dbMigrationsProject, module.Name, startupProject); } } @@ -274,4 +275,4 @@ namespace Volo.Abp.Cli.ProjectModification .Any(p => p.EndsWith(".IdentityServer") || p.EndsWith(".HttpApi.Host")); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj b/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj index 424e09b115..489ea99ac5 100644 --- a/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj +++ b/framework/src/Volo.Abp.Cli/Volo.Abp.Cli.csproj @@ -18,12 +18,12 @@ - - + + - \ No newline at end of file + diff --git a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs index ec7a640a39..ac71649793 100644 --- a/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/Collections/Generic/AbpListExtensions.cs @@ -9,6 +9,14 @@ namespace System.Collections.Generic /// public static class AbpListExtensions { + public static void InsertRange(this IList source, int index, IEnumerable items) + { + foreach (var item in items) + { + source.Insert(index++, item); + } + } + public static int FindIndex(this IList source, Predicate selector) { for (var i = 0; i < source.Count; ++i) @@ -182,7 +190,7 @@ namespace System.Collections.Generic /// Function to resolve the dependencies /// /// Returns a new list ordered by dependencies. - /// If A depends on B, then B will come before than A in the resulting list. + /// If A depends on B, then B will come before than A in the resulting list. /// public static List SortByDependencies(this IEnumerable source, Func> getDependencies) { @@ -202,14 +210,15 @@ namespace System.Collections.Generic } /// - /// + /// /// /// The type of the members of values. /// Item to resolve /// Function to resolve the dependencies /// List with the sortet items /// Dictionary with the visited items - private static void SortByDependenciesVisit(T item, Func> getDependencies, List sorted, Dictionary visited) + private static void SortByDependenciesVisit(T item, Func> getDependencies, List sorted, + Dictionary visited) { bool inProcess; var alreadyVisited = visited.TryGetValue(item, out inProcess); diff --git a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj index 000433c83d..e1842d0447 100644 --- a/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj +++ b/framework/src/Volo.Abp.Core/Volo.Abp.Core.csproj @@ -16,15 +16,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 83c2c4c38e..cf3805249d 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -71,7 +71,7 @@ namespace Volo.Abp.Reflection return true; } - if (includeNullables && IsNullable(type)) + if (includeNullables && IsNullable(type) && type.GenericTypeArguments.Any()) { return IsPrimitiveExtendedInternal(type.GenericTypeArguments[0], includeEnums); } @@ -197,7 +197,7 @@ namespace Volo.Abp.Reflection return $"{genericType.FullName.Left(genericType.FullName.IndexOf('`'))}<{type.GenericTypeArguments.Select(GetFullNameHandlingNullableAndGenerics).JoinAsString(",")}>"; } - return type.FullName; + return type.FullName ?? type.Name; } public static string GetSimplifiedName([NotNull] Type type) @@ -295,8 +295,12 @@ namespace Volo.Abp.Reflection { return "number"; } + else if (type == typeof(object)) + { + return "object"; + } - return type.FullName; + return type.FullName ?? type.Name; } public static object ConvertFromString(string value) diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ar.json b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ar.json index 23b8e152d7..a4bcaee0a2 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ar.json +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Localization/ar.json @@ -1,4 +1,4 @@ -{ +{ "culture": "ar", "texts": { "DisplayName:Abp.Mailing.DefaultFromAddress": "العنوان الإفتراضي", @@ -18,6 +18,8 @@ "Description:Abp.Mailing.Smtp.Password": "كلمة المرور لاسم المستخدم المرتبط ببيانات الاعتماد.", "Description:Abp.Mailing.Smtp.Domain": "اسم المجال أو الكمبيوتر الذي يتحقق من بيانات الاعتماد.", "Description:Abp.Mailing.Smtp.EnableSsl": "ما إذا كان SmtpClient يستخدم (SSL) لتشفير الاتصال.", - "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "إرسال الصلاحيات الافتراضية مع الطلب." + "Description:Abp.Mailing.Smtp.UseDefaultCredentials": "إرسال الصلاحيات الافتراضية مع الطلب.", + "TextTemplate:StandardEmailTemplates.Layout": "نموذج تخطيط إفتراضي للبريد الإلكتروني", + "TextTemplate:StandardEmailTemplates.Message": "نموذج بسيط للرسائل الإلكترونية" } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj index 988c3688ee..366958b213 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.SqlServer/Volo.Abp.EntityFrameworkCore.SqlServer.csproj @@ -19,7 +19,7 @@ - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj index ce5c9a1716..6a8fd26db2 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore.Sqlite/Volo.Abp.EntityFrameworkCore.Sqlite.csproj @@ -2,7 +2,7 @@ - + netstandard2.0 Volo.Abp.EntityFrameworkCore.Sqlite @@ -13,11 +13,11 @@ false - + - + - + diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj b/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj index da97fb5977..4c6fb7fbbc 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo.Abp.EntityFrameworkCore.csproj @@ -20,8 +20,8 @@ - - + + 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 796c6e20fb..50de4f1b56 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 @@ -242,7 +242,7 @@ namespace Volo.Abp.EntityFrameworkCore.EntityHistory } } - if (IsBaseAuditProperty(propertyInfo, entityType)) + if (propertyInfo != null && IsBaseAuditProperty(propertyInfo, entityType)) { return false; } diff --git a/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj b/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj index 4441e0bc17..75dbce3b03 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj +++ b/framework/src/Volo.Abp.Http.Client/Volo.Abp.Http.Client.csproj @@ -15,7 +15,7 @@ - + diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs index 533b76aa6c..af9eb59b7f 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs @@ -17,7 +17,6 @@ using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; using Volo.Abp.MultiTenancy; -using Volo.Abp.Reflection; using Volo.Abp.Threading; using Volo.Abp.Tracing; diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApiTypeNameHelper.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApiTypeNameHelper.cs new file mode 100644 index 0000000000..46eb9e903d --- /dev/null +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ApiTypeNameHelper.cs @@ -0,0 +1,38 @@ +using System; +using Volo.Abp.Reflection; + +namespace Volo.Abp.Http.Modeling +{ + public static class ApiTypeNameHelper + { + public static string GetTypeName(Type type) + { + if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) + { + return $"{{{GetTypeName(keyType)}:{GetTypeName(valueType)}}}"; + } + + if (TypeHelper.IsEnumerable(type, out var itemType, includePrimitives: false)) + { + return $"[{GetTypeName(itemType)}]"; + } + + return TypeHelper.GetFullNameHandlingNullableAndGenerics(type); + } + + public static string GetSimpleTypeName(Type type) + { + if (TypeHelper.IsDictionary(type, out var keyType, out var valueType)) + { + return $"{{{GetSimpleTypeName(keyType)}:{GetSimpleTypeName(valueType)}}}"; + } + + if (TypeHelper.IsEnumerable(type, out var itemType, includePrimitives: false)) + { + return $"[{GetSimpleTypeName(itemType)}]"; + } + + return TypeHelper.GetSimplifiedName(type); + } + } +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs index 603d3994e0..b4e272c2f3 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/MethodParameterApiDescriptionModel.cs @@ -31,7 +31,7 @@ namespace Volo.Abp.Http.Modeling Name = parameterInfo.Name, TypeAsString = parameterInfo.ParameterType.GetFullNameWithAssemblyName(), Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(parameterInfo.ParameterType), - TypeSimple = TypeHelper.GetSimplifiedName(parameterInfo.ParameterType), + TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(parameterInfo.ParameterType), IsOptional = parameterInfo.IsOptional, DefaultValue = parameterInfo.HasDefaultValue ? parameterInfo.DefaultValue : null }; diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs index 0450861a41..675ba8bf54 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ParameterApiDescriptionModel.cs @@ -26,7 +26,7 @@ namespace Volo.Abp.Http.Modeling private ParameterApiDescriptionModel() { - + } public static ParameterApiDescriptionModel Create(string name, string nameOnMethod, Type type, bool isOptional = false, object defaultValue = null, string[] constraintTypes = null, string bindingSourceId = null, string descriptorName = null) @@ -36,7 +36,7 @@ namespace Volo.Abp.Http.Modeling Name = name, NameOnMethod = nameOnMethod, Type = type != null ? TypeHelper.GetFullNameHandlingNullableAndGenerics(type) : null, - TypeSimple = type != null ? TypeHelper.GetSimplifiedName(type) : null, + TypeSimple = type != null ? ApiTypeNameHelper.GetSimpleTypeName(type) : null, IsOptional = isOptional, DefaultValue = defaultValue, ConstraintTypes = constraintTypes, @@ -45,4 +45,4 @@ namespace Volo.Abp.Http.Modeling }; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs index 80a3cee91a..610b6bd347 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/PropertyApiDescriptionModel.cs @@ -1,6 +1,5 @@ using System; using System.Reflection; -using Volo.Abp.Reflection; namespace Volo.Abp.Http.Modeling { @@ -16,30 +15,11 @@ namespace Volo.Abp.Http.Modeling //TODO: Validation rules for this property public static PropertyApiDescriptionModel Create(PropertyInfo propertyInfo) { - string typeName; - string simpleTypeName; - - if (TypeHelper.IsDictionary(propertyInfo.PropertyType, out var keyType, out var valueType)) - { - typeName = $"{{{TypeHelper.GetFullNameHandlingNullableAndGenerics(keyType)}:{TypeHelper.GetFullNameHandlingNullableAndGenerics(valueType)}}}"; - simpleTypeName = $"{{{TypeHelper.GetSimplifiedName(keyType)}:{TypeHelper.GetSimplifiedName(valueType)}}}"; - } - else if (TypeHelper.IsEnumerable(propertyInfo.PropertyType, out var itemType, includePrimitives: false)) - { - typeName = $"[{TypeHelper.GetFullNameHandlingNullableAndGenerics(itemType)}]"; - simpleTypeName = $"[{TypeHelper.GetSimplifiedName(itemType)}]"; - } - else - { - typeName = TypeHelper.GetFullNameHandlingNullableAndGenerics(propertyInfo.PropertyType); - simpleTypeName = TypeHelper.GetSimplifiedName(propertyInfo.PropertyType); - } - return new PropertyApiDescriptionModel { Name = propertyInfo.Name, - Type = typeName, - TypeSimple = simpleTypeName + Type = ApiTypeNameHelper.GetTypeName(propertyInfo.PropertyType), + TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(propertyInfo.PropertyType) }; } } diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs index 2e0862ccce..83fa4bb6be 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ReturnValueApiDescriptionModel.cs @@ -23,8 +23,8 @@ namespace Volo.Abp.Http.Modeling return new ReturnValueApiDescriptionModel { Type = TypeHelper.GetFullNameHandlingNullableAndGenerics(unwrappedType), - TypeSimple = TypeHelper.GetSimplifiedName(unwrappedType) + TypeSimple = ApiTypeNameHelper.GetSimpleTypeName(unwrappedType) }; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs index 161584279b..4b0b0b870c 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/TypeApiDescriptionModel.cs @@ -15,11 +15,13 @@ namespace Volo.Abp.Http.Modeling public object[] EnumValues { get; set; } + public string[] GenericArguments { get; set; } + public PropertyApiDescriptionModel[] Properties { get; set; } private TypeApiDescriptionModel() { - + } public static TypeApiDescriptionModel Create(Type type) @@ -45,11 +47,17 @@ namespace Volo.Abp.Http.Modeling { typeModel.Properties = type .GetProperties() + .Where(p => p.DeclaringType == type) .Select(PropertyApiDescriptionModel.Create) .ToArray(); + + if (type.IsGenericTypeDefinition) + { + typeModel.GenericArguments = type.GetGenericArguments().Select(a => a.Name).ToArray(); + } } return typeModel; } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj index 0143db8265..d128363491 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj +++ b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj @@ -16,7 +16,7 @@ - + diff --git a/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs b/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs index dcb8f28dff..c3ea13da43 100644 --- a/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs +++ b/framework/src/Volo.Abp.Json/Volo/Abp/Json/AbpJsonModule.cs @@ -1,4 +1,5 @@ -using Volo.Abp.Modularity; +using Volo.Abp.Json.Newtonsoft; +using Volo.Abp.Modularity; using Volo.Abp.Timing; namespace Volo.Abp.Json @@ -6,6 +7,12 @@ namespace Volo.Abp.Json [DependsOn(typeof(AbpTimingModule))] public class AbpJsonModule : AbpModule { - + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Converters.Add(); + }); + } } } diff --git a/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/AbpNewtonsoftJsonSerializerOptions.cs b/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/AbpNewtonsoftJsonSerializerOptions.cs new file mode 100644 index 0000000000..cb1c3d9f65 --- /dev/null +++ b/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/AbpNewtonsoftJsonSerializerOptions.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using Volo.Abp.Collections; + +namespace Volo.Abp.Json.Newtonsoft +{ + public class AbpNewtonsoftJsonSerializerOptions + { + public ITypeList Converters { get; } + + public AbpNewtonsoftJsonSerializerOptions() + { + Converters = new TypeList(); + } + } +} diff --git a/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/NewtonsoftJsonSerializer.cs b/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/NewtonsoftJsonSerializer.cs index 99252b8448..20e49ac817 100644 --- a/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/NewtonsoftJsonSerializer.cs +++ b/framework/src/Volo.Abp.Json/Volo/Abp/Json/Newtonsoft/NewtonsoftJsonSerializer.cs @@ -1,4 +1,8 @@ using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Volo.Abp.DependencyInjection; @@ -7,14 +11,19 @@ namespace Volo.Abp.Json.Newtonsoft { public class NewtonsoftJsonSerializer : IJsonSerializer, ITransientDependency { - private readonly AbpJsonIsoDateTimeConverter _dateTimeConverter; - private static readonly CamelCaseExceptDictionaryKeysResolver SharedCamelCaseExceptDictionaryKeysResolver = new CamelCaseExceptDictionaryKeysResolver(); - public NewtonsoftJsonSerializer(AbpJsonIsoDateTimeConverter dateTimeConverter) + protected List Converters { get; } + + public NewtonsoftJsonSerializer( + IOptions options, + IServiceProvider serviceProvider) { - _dateTimeConverter = dateTimeConverter; + Converters = options.Value + .Converters + .Select(c => (JsonConverter) serviceProvider.GetRequiredService(c)) + .ToList(); } public string Serialize(object obj, bool camelCase = true, bool indented = false) @@ -36,8 +45,8 @@ namespace Volo.Abp.Json.Newtonsoft { var settings = new JsonSerializerSettings(); - settings.Converters.Insert(0, _dateTimeConverter); - + settings.Converters.InsertRange(0, Converters); + if (camelCase) { settings.ContractResolver = SharedCamelCaseExceptDictionaryKeysResolver; @@ -47,7 +56,7 @@ namespace Volo.Abp.Json.Newtonsoft { settings.Formatting = Formatting.Indented; } - + return settings; } @@ -63,4 +72,4 @@ namespace Volo.Abp.Json.Newtonsoft } } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/ar.json b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/ar.json new file mode 100644 index 0000000000..7b8ddf6919 --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/Localization/ar.json @@ -0,0 +1,7 @@ +{ + "culture": "ar", + "texts": { + "DisplayName:Abp.Timing.Timezone": "المنطقة زمنية", + "Description:Abp.Timing.Timezone": "المنطقة الزمنية للتطبيق" + } +} \ No newline at end of file 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 ea35628dc7..ec51f85510 100644 --- a/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json +++ b/framework/src/Volo.Abp.UI/Localization/Resources/AbpUi/ar.json @@ -1,4 +1,4 @@ -{ +{ "culture": "ar", "texts": { "InternalServerErrorMessage": "حدث خطأ داخلي أثناء طلبك!", @@ -47,6 +47,8 @@ "PagerInfoEmpty": "عرض 0 الي 0 من 0 إدخالات", "PagerInfoFiltered": "(تمت تصفيته من _MAX_ مجموع الإدخالات)", "NoDataAvailableInDatatable": "لا توجد بيانات متاحة في الجدول", + "Total": "مجموع", + "Selected": "مختارة", "PagerShowMenuEntries": "عرض _MENU_ إدخالات", "DatatableActionDropdownDefaultText": "أجراءات", "ChangePassword": "تغيير كلمة السر", @@ -61,4 +63,4 @@ "GoBack": "عد", "Search": "بحث" } -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ar.json b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ar.json index 1ef7e01b34..2cdb6999b5 100644 --- a/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ar.json +++ b/framework/src/Volo.Abp.Validation/Volo/Abp/Validation/Localization/ar.json @@ -1,34 +1,34 @@ { "culture": "ar", "texts": { - "'{0}' and '{1}' do not match.": "'{0}' and '{1}' do not match.", - "The {0} field is not a valid credit card number.": "The {0} field is not a valid credit card number.", - "{0} is not valid.": "{0} is not valid.", - "The {0} field is not a valid e-mail address.": "The {0} field is not a valid e-mail address.", - "The {0} field only accepts files with the following extensions: {1}": "The {0} field only accepts files with the following extensions: {1}", - "The field {0} must be a string or array type with a maximum length of '{1}'.": "The field {0} must be a string or array type with a maximum length of '{1}'.", - "The field {0} must be a string or array type with a minimum length of '{1}'.": "The field {0} must be a string or array type with a minimum length of '{1}'.", - "The {0} field is not a valid phone number.": "The {0} field is not a valid phone number.", - "The field {0} must be between {1} and {2}.": "The field {0} must be between {1} and {2}.", - "The field {0} must match the regular expression '{1}'.": "The field {0} must match the regular expression '{1}'.", - "The {0} field is required.": "The {0} field is required.", - "The field {0} must be a string with a maximum length of {1}.": "The field {0} must be a string with a maximum length of {1}.", - "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.", - "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "The {0} field is not a valid fully-qualified http, https, or ftp URL.", - "The field {0} is invalid.": "The field {0} is invalid.", - "ThisFieldIsNotAValidCreditCardNumber.": "This field is not a valid credit card number.", - "ThisFieldIsNotValid.": "This field is not valid.", - "ThisFieldIsNotAValidEmailAddress.": "This field is not a valid e-mail address.", - "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "This field only accepts files with the following extensions: {0}", - "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthoOf{0}": "This field must be a string or array type with a maximum length of '{0}'.", - "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": "This field must be a string or array type with a minimum length of '{0}'.", - "ThisFieldIsNotAValidPhoneNumber.": "This field is not a valid phone number.", - "ThisFieldMustBeBetween{0}And{1}": "This field must be between {0} and {1}.", - "ThisFieldMustMatchTheRegularExpression{0}": "This field must match the regular expression '{0}'.", - "ThisFieldIsRequired.": "This field is required.", - "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "This field must be a string with a maximum length of {0}.", - "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "This field must be a string with a minimum length of {1} and a maximum length of {0}.", - "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "This field is not a valid fully-qualified http, https, or ftp URL.", - "ThisFieldIsInvalid.": "This field is invalid." + "'{0}' and '{1}' do not match.": "'{0}' و '{1}' غير متطابقان", + "The {0} field is not a valid credit card number.": "الحقل {0} لا يمثل رقم بطاقة ائتمان صالح.", + "{0} is not valid.": "{0} غير صالح.", + "The {0} field is not a valid e-mail address.": "الحقل {0} لا يمثل عنوان بريد إلكتروني صالح.", + "The {0} field only accepts files with the following extensions: {1}": "الحقل {0} يقبل الملفات ذات الامتدادات التالية فقط: {1}", + "The field {0} must be a string or array type with a maximum length of '{1}'.": "يجب أن يكون الحقل {0} عبارة عن سلسلة أو صفيف طوله '{1}' كحد أقصى.", + "The field {0} must be a string or array type with a minimum length of '{1}'.": "يجب أن يكون الحقل {0} عبارة عن سلسلة أو صفيف طوله كحد أدنى '{1}'.", + "The {0} field is not a valid phone number.": "الحقل {0} لا يمثل رقم هاتف صالح.", + "The field {0} must be between {1} and {2}.": "يجب أن يكون الحقل {0} بين {1} و {2}.", + "The field {0} must match the regular expression '{1}'.": "يجب أن يتطابق الحقل {0} مع التعبير النمطي '{1}'.", + "The {0} field is required.": "الحقل {0} إجباري.", + "The field {0} must be a string with a maximum length of {1}.": "يجب أن يكون الحقل {0} سلسلة أحرف طولها كحد أقصى {1}.", + "The field {0} must be a string with a minimum length of {2} and a maximum length of {1}.": "يجب أن يكون الحقل {0} سلسلة أحرف طولها {1} كحد أدنى و {2} كحد أقصى.", + "The {0} field is not a valid fully-qualified http, https, or ftp URL.": "الحقل {0} ليس عنوانا URL صالحًا مؤهلاً بالكامل سواء كان عنوان http أو https أو ftp", + "The field {0} is invalid.": "الحقل {0} غير صالح.", + "ThisFieldIsNotAValidCreditCardNumber.": "هذا الحقل لا يمثل رقم بطاقة ائتمان صالح.", + "ThisFieldIsNotValid.": "هذا الحقل غير صالح.", + "ThisFieldIsNotAValidEmailAddress.": "هذا الحقل لا يمثل عنوان بريد إلكتروني صالح.", + "ThisFieldOnlyAcceptsFilesWithTheFollowingExtensions:{0}": "هذا الحقل يقبل الملفات ذات الامتدادات التالية فقط: {0}", + "ThisFieldMustBeAStringOrArrayTypeWithAMaximumLengthOf{0}": "هذا الحقل يجب أن يكون سلسلة أحرف أو صفيف لا يتعدى طوله '{0}'.", + "ThisFieldMustBeAStringOrArrayTypeWithAMinimumLengthOf{0}": " هذا الحقل يجب أن يكون عبارة عن سلسلة أو صفيف طوله كحد أدنى '{0}'.", + "ThisFieldIsNotAValidPhoneNumber.": "هذا الحقل لا يمثل رقم هاتف صالح.", + "ThisFieldMustBeBetween{0}And{1}": "هذا الحقل يجب أن يكون بين {0} و {1}.", + "ThisFieldMustMatchTheRegularExpression{0}": "هذا الحقل يجب أن يتطابق مع التعبير النمطي '{0}'.", + "ThisFieldIsRequired.": "هذا الحقل إجباري.", + "ThisFieldMustBeAStringWithAMaximumLengthOf{0}": "هذا الحقل يجب أن يكون سلسلة أحرف طولها كحد أقصى {0}.", + "ThisFieldMustBeAStringWithAMinimumLengthOf{1}AndAMaximumLengthOf{0}": "هذا الحقل يجب أن يكون سلسلة أحرف طولها {0} كحد أدنى و {1} كحد أقصى.", + "ThisFieldIsNotAValidFullyQualifiedHttpHttpsOrFtpUrl": "هذا الحقل ليس عنوانا URL صالحًا مؤهلاً بالكامل سواء كان عنوان http أو https أو ftp", + "ThisFieldIsInvalid.": "هذا الحقل غير صالح." } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj b/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj index b4aa55b1c0..09af40a2c7 100644 --- a/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj +++ b/framework/src/Volo.Abp.VirtualFileSystem/Volo.Abp.VirtualFileSystem.csproj @@ -15,9 +15,9 @@ - - - + + + diff --git a/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj b/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj index 49c80c56db..4edbb748b9 100644 --- a/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj +++ b/framework/test/SimpleConsoleDemo/SimpleConsoleDemo.csproj @@ -18,7 +18,7 @@ - + diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/PeopleAppService_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/PeopleAppService_Tests.cs index 96a79d8971..aae7345908 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/PeopleAppService_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/PeopleAppService_Tests.cs @@ -83,7 +83,6 @@ namespace Volo.Abp.AspNetCore.Mvc (await _personRepository.FindAsync(resultDto.Id)).ShouldNotBeNull(); } - [Fact] public async Task Update_Test() { @@ -170,4 +169,4 @@ namespace Volo.Abp.AspNetCore.Mvc douglas.Phones.Any(p => p.Number == firstPhone.Number).ShouldBeFalse(); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json index 16b087d9d9..9838b50651 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/package.json @@ -3,7 +3,7 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.shared": "^3.0.4", + "@abp/aspnetcore.mvc.ui.theme.shared": "^3.0.5", "highlight.js": "^9.13.1" }, "devDependencies": {} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock index 72471a0d99..3399733e18 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.Demo/yarn.lock @@ -2,30 +2,30 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.shared@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.4.tgz#50db612b6492d36c63dd9042000a9fb00fecec54" - integrity sha512-imd2T5XXkNvGQyXV/A9hA80qSeurKaUcW6HDkte8Y2fs4QfvkjgzWXi7neqF6F+HVbYYPg3rIBWC6QF1OXGn2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~3.0.4" - "@abp/bootstrap" "~3.0.4" - "@abp/bootstrap-datepicker" "~3.0.4" - "@abp/datatables.net-bs4" "~3.0.4" - "@abp/font-awesome" "~3.0.4" - "@abp/jquery-form" "~3.0.4" - "@abp/jquery-validation-unobtrusive" "~3.0.4" - "@abp/lodash" "~3.0.4" - "@abp/luxon" "~3.0.4" - "@abp/malihu-custom-scrollbar-plugin" "~3.0.4" - "@abp/select2" "~3.0.4" - "@abp/sweetalert" "~3.0.4" - "@abp/timeago" "~3.0.4" - "@abp/toastr" "~3.0.4" - -"@abp/aspnetcore.mvc.ui@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.4.tgz#4e4344d95a98a60c53037b351cb915d8ff7a7dcb" - integrity sha512-p7KI5CKkpD4EYzG4fCtygrtZhwMu7Z+IZFGbdqXelLbI5ACkbJyrn+N+9trPfS9papymeXY4p1E3v6xOH7rSHQ== +"@abp/aspnetcore.mvc.ui.theme.shared@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76" + integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg== + dependencies: + "@abp/aspnetcore.mvc.ui" "~3.0.5" + "@abp/bootstrap" "~3.0.5" + "@abp/bootstrap-datepicker" "~3.0.5" + "@abp/datatables.net-bs4" "~3.0.5" + "@abp/font-awesome" "~3.0.5" + "@abp/jquery-form" "~3.0.5" + "@abp/jquery-validation-unobtrusive" "~3.0.5" + "@abp/lodash" "~3.0.5" + "@abp/luxon" "~3.0.5" + "@abp/malihu-custom-scrollbar-plugin" "~3.0.5" + "@abp/select2" "~3.0.5" + "@abp/sweetalert" "~3.0.5" + "@abp/timeago" "~3.0.5" + "@abp/toastr" "~3.0.5" + +"@abp/aspnetcore.mvc.ui@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0" + integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg== dependencies: ansi-colors "^4.1.1" extend-object "^1.0.0" @@ -34,144 +34,144 @@ path "^0.12.7" rimraf "^3.0.2" -"@abp/bootstrap-datepicker@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.4.tgz#d21963f158a4d348a0acdc46209e67378aae8e55" - integrity sha512-25e9qm39dj9AijIchQIdLlBiBM5/fhASkEuLKNt6M8S/e3qwKLwbRhX90DKMFvOyKdxxcK1gX7tDuGHyG9A8mA== +"@abp/bootstrap-datepicker@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540" + integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA== dependencies: bootstrap-datepicker "^1.9.0" -"@abp/bootstrap@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.4.tgz#3b0e2b74dc174a04ce669de1b140df833eee6031" - integrity sha512-DwxL1fGYOwChPwmMpwUxCKiIdzTfRf/YDkOLUiwI8eRrpABBeuE2sSHKfks6Rqh9Wqo/nlWXvfccgDRqweqhsA== +"@abp/bootstrap@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880" + integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" bootstrap "^4.5.0" bootstrap-v4-rtl "4.4.1-2" -"@abp/core@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.4.tgz#281940525e280e9e3a1aa4cb4e8f64548ea026a3" - integrity sha512-ZPEH1fa5bf/f1ek9ZLQHx7u78iF6kRZ8Z2NBI36cNL13RyQbx485qJF+KXyqNlJoJAh0CAs0NsAJADU3utcDNA== +"@abp/core@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8" + integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA== dependencies: - "@abp/utils" "^3.0.4" + "@abp/utils" "^3.0.5" -"@abp/datatables.net-bs4@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.4.tgz#e488336bd2c199146ea60464e0ba75aeb134957e" - integrity sha512-ID1XWKPxG9pv6mxBkku7JWvgXjTxJ1uShxmYv5kFYVuwOGMSvj8QVZInTHFcMdECopV56yjGPNOIFLeQj4Mw8Q== +"@abp/datatables.net-bs4@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a" + integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ== dependencies: - "@abp/datatables.net" "~3.0.4" + "@abp/datatables.net" "~3.0.5" datatables.net-bs4 "^1.10.21" -"@abp/datatables.net@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.4.tgz#7ef143b83143ad8d769be25296411a812a17b255" - integrity sha512-gdQwRuxO5fQWhG/DiUaf40DQ8dWIVGhOCyt6SZ8LQzq4uSaQqa0qrtTTTW2/aiee4MgJu2vJWYF5YCszIR/HhA== +"@abp/datatables.net@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491" + integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" datatables.net "^1.10.21" -"@abp/font-awesome@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.4.tgz#98f83d0bf8b5a2a4d988cb07f5cceda2e1bc1a09" - integrity sha512-AKhKCUXx8vdHIgyXUfbSLitfYor5dJbo4qMRRGqIJVSj+cyMkpgYbGjqr24GGijgZxdmOGL+s1b9iCUbIPgLNQ== +"@abp/font-awesome@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412" + integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" "@fortawesome/fontawesome-free" "^5.13.0" -"@abp/jquery-form@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.4.tgz#022eebbfe58b55e328984c0f119884c9464bc767" - integrity sha512-xDkk4rY0AmXBJE4qztzr2hZz+OVucVNWeagOEgeQHXJSKU/T+0kj0a1RxEHpyI24xBvEXAbe8PbByTbu0QXYew== +"@abp/jquery-form@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78" + integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.4.tgz#6cc942aadcdde33893e7a45c84aa022e033e4e8b" - integrity sha512-N9sza91HCyPvJo+d0RAfq4skae3TgAYg7KXhlwzRE2hqUQS+EICVARURWyxJW7s7Zu6+gxDTkXhBZRTW56deZA== +"@abp/jquery-validation-unobtrusive@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f" + integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw== dependencies: - "@abp/jquery-validation" "~3.0.4" + "@abp/jquery-validation" "~3.0.5" jquery-validation-unobtrusive "^3.2.11" -"@abp/jquery-validation@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.4.tgz#3f96d1f83c75a7fc47e5e5e12c51dbc8da7582b1" - integrity sha512-2ljqas4YKU0PcwB0wQW2qhXejDHjW+N1XCxZDyCZ2wF9UxGlKBR8rFt76TJByXSotKE7n2hJRZaOh9ICZUow2g== +"@abp/jquery-validation@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279" + integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-validation "^1.19.2" -"@abp/jquery@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.4.tgz#5f236d95cfae7f65012cd8778fd74490b7542442" - integrity sha512-g8N8Te5vf+8djdksfZoZ3tXaqlBC4Ra39pAWU/XerlLl64u+XmAX1s6yKRfC2kS8lf292GgiFYuNOkb77J6k4A== +"@abp/jquery@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754" + integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" jquery "~3.5.1" -"@abp/lodash@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.4.tgz#ee60bc99f98b59fd9fc5ff4bbcf914a13faf209d" - integrity sha512-jxvv74Sm/fUKDBMylK4Ne/8GKm7GuHAdcLFtkoJQptdO+RjZpKJ86ULT+X5vbKskZaTj2fEmOR6l4hmfnBULTA== +"@abp/lodash@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4" + integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" lodash "^4.17.15" -"@abp/luxon@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.4.tgz#aa1bf6a93d6254433101e50a205b276759969414" - integrity sha512-gX9dQJt9C6M1wmCSUIBoNr8CAt0jkUZFQ7LKKO/BoADGM/XFvY5vDbhL7zcwbwitgCOzmucoCxs/6rIxSOVlrg== +"@abp/luxon@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688" + integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw== dependencies: luxon "^1.24.1" -"@abp/malihu-custom-scrollbar-plugin@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.4.tgz#c54824b98363f4190cc6a37fcd421c2bfdc1e201" - integrity sha512-6LdSGctOr4ACtdohdSlb19vrFNJ5C0hy1VdbBEafHOBspXckvRVx4uJl2KfHmAC+bMg9apoK/wcHjVNdBHThYQ== +"@abp/malihu-custom-scrollbar-plugin@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39" + integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/select2@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.4.tgz#49db0cfbc9b08f6ae4949b2cb8bdef92d7d308dd" - integrity sha512-zKEgkWBFdGU2KiAMUKhucHqBfzDy2KNSefyqxpOiUd1L3PEKu+9o23HZRWzDupYltop+SO1Jw33CNoVAkKm4+A== +"@abp/select2@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350" + integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" select2 "^4.0.13" -"@abp/sweetalert@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.4.tgz#f85d11c5aabc6ceb509af1b8fd8d75bc3c74bd27" - integrity sha512-hO0qXUWxeMy/lfy3sb+5HvOmIPst6UvYJJGy3ZQR6DKEdkOktBX9sEDb7m6ij6xAIHqWIx1xLveul0oGteOG0Q== +"@abp/sweetalert@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab" + integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" sweetalert "^2.1.2" -"@abp/timeago@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.4.tgz#82d90a04f703a7db9c0acedcd2f3cea684e7ae18" - integrity sha512-7SbbqScTKCix25xWy5oLT0slJqUQsW6YgJgI0N41C3AsByRBXZeh5Gcep033gprTPmpgNY8JOWcEcCXySnYvfA== +"@abp/timeago@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31" + integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" timeago "^1.6.7" -"@abp/toastr@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.4.tgz#884ab2343ddd59981c291c632e3b0630532f7e9c" - integrity sha512-NG0pVD8bdIXGpjPW9cozCzmyuAdnSLE4vDYLUReP1pZFp9B8SMJqpgPk9eFg0Y06aaHTVMKYBKRoq9xXS4w9Xg== +"@abp/toastr@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc" + integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" toastr "^2.1.4" -"@abp/utils@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.4.tgz#38068fa8f735e1889cf2f226128366825948814d" - integrity sha512-TnXRZIZcxv5+EctEPsZEi2cyRXbpl3A679wzQa6BACv+pM45cjGO1JR8P0VswKN4TVL0aQRMv26N6U/uie8sEQ== +"@abp/utils@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49" + integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg== dependencies: just-compare "^1.3.0" diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json index 1cfcdc4382..8f9f972442 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/package.json @@ -3,7 +3,7 @@ "name": "asp.net", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.5" }, "devDependencies": {} } \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock index 2b6d5dd3e7..b30d4d2f76 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Demo/yarn.lock @@ -2,37 +2,37 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.4.tgz#c58e3e06ba78279875cf2b5f96d34b83ce09f6df" - integrity sha512-tKKqVRKpwN7P4Mn+he3bcz4Qbt8bkZ++wk6iOujd6HLTkBNa0uKyM4L6+iCn1Hi62S6HeK2Xytx+b+IYeIlsuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.4.tgz#50db612b6492d36c63dd9042000a9fb00fecec54" - integrity sha512-imd2T5XXkNvGQyXV/A9hA80qSeurKaUcW6HDkte8Y2fs4QfvkjgzWXi7neqF6F+HVbYYPg3rIBWC6QF1OXGn2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~3.0.4" - "@abp/bootstrap" "~3.0.4" - "@abp/bootstrap-datepicker" "~3.0.4" - "@abp/datatables.net-bs4" "~3.0.4" - "@abp/font-awesome" "~3.0.4" - "@abp/jquery-form" "~3.0.4" - "@abp/jquery-validation-unobtrusive" "~3.0.4" - "@abp/lodash" "~3.0.4" - "@abp/luxon" "~3.0.4" - "@abp/malihu-custom-scrollbar-plugin" "~3.0.4" - "@abp/select2" "~3.0.4" - "@abp/sweetalert" "~3.0.4" - "@abp/timeago" "~3.0.4" - "@abp/toastr" "~3.0.4" - -"@abp/aspnetcore.mvc.ui@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.4.tgz#4e4344d95a98a60c53037b351cb915d8ff7a7dcb" - integrity sha512-p7KI5CKkpD4EYzG4fCtygrtZhwMu7Z+IZFGbdqXelLbI5ACkbJyrn+N+9trPfS9papymeXY4p1E3v6xOH7rSHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.5.tgz#c146f20226522f0305cc0fa31cfcf9d39be78901" + integrity sha512-ptNhD4xV2wSLW7vQ5KUgaRiH5Ov1OscZW2ZEoivnt3kqgtIC3DGw+SiE9vjFCvyY83fjRkIlW2PIfQxSySbteQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.5" + +"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76" + integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg== + dependencies: + "@abp/aspnetcore.mvc.ui" "~3.0.5" + "@abp/bootstrap" "~3.0.5" + "@abp/bootstrap-datepicker" "~3.0.5" + "@abp/datatables.net-bs4" "~3.0.5" + "@abp/font-awesome" "~3.0.5" + "@abp/jquery-form" "~3.0.5" + "@abp/jquery-validation-unobtrusive" "~3.0.5" + "@abp/lodash" "~3.0.5" + "@abp/luxon" "~3.0.5" + "@abp/malihu-custom-scrollbar-plugin" "~3.0.5" + "@abp/select2" "~3.0.5" + "@abp/sweetalert" "~3.0.5" + "@abp/timeago" "~3.0.5" + "@abp/toastr" "~3.0.5" + +"@abp/aspnetcore.mvc.ui@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0" + integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg== dependencies: ansi-colors "^4.1.1" extend-object "^1.0.0" @@ -41,144 +41,144 @@ path "^0.12.7" rimraf "^3.0.2" -"@abp/bootstrap-datepicker@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.4.tgz#d21963f158a4d348a0acdc46209e67378aae8e55" - integrity sha512-25e9qm39dj9AijIchQIdLlBiBM5/fhASkEuLKNt6M8S/e3qwKLwbRhX90DKMFvOyKdxxcK1gX7tDuGHyG9A8mA== +"@abp/bootstrap-datepicker@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540" + integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA== dependencies: bootstrap-datepicker "^1.9.0" -"@abp/bootstrap@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.4.tgz#3b0e2b74dc174a04ce669de1b140df833eee6031" - integrity sha512-DwxL1fGYOwChPwmMpwUxCKiIdzTfRf/YDkOLUiwI8eRrpABBeuE2sSHKfks6Rqh9Wqo/nlWXvfccgDRqweqhsA== +"@abp/bootstrap@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880" + integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" bootstrap "^4.5.0" bootstrap-v4-rtl "4.4.1-2" -"@abp/core@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.4.tgz#281940525e280e9e3a1aa4cb4e8f64548ea026a3" - integrity sha512-ZPEH1fa5bf/f1ek9ZLQHx7u78iF6kRZ8Z2NBI36cNL13RyQbx485qJF+KXyqNlJoJAh0CAs0NsAJADU3utcDNA== +"@abp/core@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8" + integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA== dependencies: - "@abp/utils" "^3.0.4" + "@abp/utils" "^3.0.5" -"@abp/datatables.net-bs4@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.4.tgz#e488336bd2c199146ea60464e0ba75aeb134957e" - integrity sha512-ID1XWKPxG9pv6mxBkku7JWvgXjTxJ1uShxmYv5kFYVuwOGMSvj8QVZInTHFcMdECopV56yjGPNOIFLeQj4Mw8Q== +"@abp/datatables.net-bs4@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a" + integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ== dependencies: - "@abp/datatables.net" "~3.0.4" + "@abp/datatables.net" "~3.0.5" datatables.net-bs4 "^1.10.21" -"@abp/datatables.net@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.4.tgz#7ef143b83143ad8d769be25296411a812a17b255" - integrity sha512-gdQwRuxO5fQWhG/DiUaf40DQ8dWIVGhOCyt6SZ8LQzq4uSaQqa0qrtTTTW2/aiee4MgJu2vJWYF5YCszIR/HhA== +"@abp/datatables.net@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491" + integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" datatables.net "^1.10.21" -"@abp/font-awesome@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.4.tgz#98f83d0bf8b5a2a4d988cb07f5cceda2e1bc1a09" - integrity sha512-AKhKCUXx8vdHIgyXUfbSLitfYor5dJbo4qMRRGqIJVSj+cyMkpgYbGjqr24GGijgZxdmOGL+s1b9iCUbIPgLNQ== +"@abp/font-awesome@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412" + integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" "@fortawesome/fontawesome-free" "^5.13.0" -"@abp/jquery-form@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.4.tgz#022eebbfe58b55e328984c0f119884c9464bc767" - integrity sha512-xDkk4rY0AmXBJE4qztzr2hZz+OVucVNWeagOEgeQHXJSKU/T+0kj0a1RxEHpyI24xBvEXAbe8PbByTbu0QXYew== +"@abp/jquery-form@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78" + integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.4.tgz#6cc942aadcdde33893e7a45c84aa022e033e4e8b" - integrity sha512-N9sza91HCyPvJo+d0RAfq4skae3TgAYg7KXhlwzRE2hqUQS+EICVARURWyxJW7s7Zu6+gxDTkXhBZRTW56deZA== +"@abp/jquery-validation-unobtrusive@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f" + integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw== dependencies: - "@abp/jquery-validation" "~3.0.4" + "@abp/jquery-validation" "~3.0.5" jquery-validation-unobtrusive "^3.2.11" -"@abp/jquery-validation@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.4.tgz#3f96d1f83c75a7fc47e5e5e12c51dbc8da7582b1" - integrity sha512-2ljqas4YKU0PcwB0wQW2qhXejDHjW+N1XCxZDyCZ2wF9UxGlKBR8rFt76TJByXSotKE7n2hJRZaOh9ICZUow2g== +"@abp/jquery-validation@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279" + integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-validation "^1.19.2" -"@abp/jquery@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.4.tgz#5f236d95cfae7f65012cd8778fd74490b7542442" - integrity sha512-g8N8Te5vf+8djdksfZoZ3tXaqlBC4Ra39pAWU/XerlLl64u+XmAX1s6yKRfC2kS8lf292GgiFYuNOkb77J6k4A== +"@abp/jquery@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754" + integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" jquery "~3.5.1" -"@abp/lodash@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.4.tgz#ee60bc99f98b59fd9fc5ff4bbcf914a13faf209d" - integrity sha512-jxvv74Sm/fUKDBMylK4Ne/8GKm7GuHAdcLFtkoJQptdO+RjZpKJ86ULT+X5vbKskZaTj2fEmOR6l4hmfnBULTA== +"@abp/lodash@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4" + integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" lodash "^4.17.15" -"@abp/luxon@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.4.tgz#aa1bf6a93d6254433101e50a205b276759969414" - integrity sha512-gX9dQJt9C6M1wmCSUIBoNr8CAt0jkUZFQ7LKKO/BoADGM/XFvY5vDbhL7zcwbwitgCOzmucoCxs/6rIxSOVlrg== +"@abp/luxon@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688" + integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw== dependencies: luxon "^1.24.1" -"@abp/malihu-custom-scrollbar-plugin@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.4.tgz#c54824b98363f4190cc6a37fcd421c2bfdc1e201" - integrity sha512-6LdSGctOr4ACtdohdSlb19vrFNJ5C0hy1VdbBEafHOBspXckvRVx4uJl2KfHmAC+bMg9apoK/wcHjVNdBHThYQ== +"@abp/malihu-custom-scrollbar-plugin@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39" + integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/select2@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.4.tgz#49db0cfbc9b08f6ae4949b2cb8bdef92d7d308dd" - integrity sha512-zKEgkWBFdGU2KiAMUKhucHqBfzDy2KNSefyqxpOiUd1L3PEKu+9o23HZRWzDupYltop+SO1Jw33CNoVAkKm4+A== +"@abp/select2@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350" + integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" select2 "^4.0.13" -"@abp/sweetalert@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.4.tgz#f85d11c5aabc6ceb509af1b8fd8d75bc3c74bd27" - integrity sha512-hO0qXUWxeMy/lfy3sb+5HvOmIPst6UvYJJGy3ZQR6DKEdkOktBX9sEDb7m6ij6xAIHqWIx1xLveul0oGteOG0Q== +"@abp/sweetalert@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab" + integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" sweetalert "^2.1.2" -"@abp/timeago@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.4.tgz#82d90a04f703a7db9c0acedcd2f3cea684e7ae18" - integrity sha512-7SbbqScTKCix25xWy5oLT0slJqUQsW6YgJgI0N41C3AsByRBXZeh5Gcep033gprTPmpgNY8JOWcEcCXySnYvfA== +"@abp/timeago@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31" + integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" timeago "^1.6.7" -"@abp/toastr@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.4.tgz#884ab2343ddd59981c291c632e3b0630532f7e9c" - integrity sha512-NG0pVD8bdIXGpjPW9cozCzmyuAdnSLE4vDYLUReP1pZFp9B8SMJqpgPk9eFg0Y06aaHTVMKYBKRoq9xXS4w9Xg== +"@abp/toastr@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc" + integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" toastr "^2.1.4" -"@abp/utils@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.4.tgz#38068fa8f735e1889cf2f226128366825948814d" - integrity sha512-TnXRZIZcxv5+EctEPsZEi2cyRXbpl3A679wzQa6BACv+pM45cjGO1JR8P0VswKN4TVL0aQRMv26N6U/uie8sEQ== +"@abp/utils@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49" + integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg== dependencies: just-compare "^1.3.0" diff --git a/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo.Abp.BlobStoring.Aliyun.Tests.csproj b/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo.Abp.BlobStoring.Aliyun.Tests.csproj index 8c5097bd84..7d61dcecae 100644 --- a/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo.Abp.BlobStoring.Aliyun.Tests.csproj +++ b/framework/test/Volo.Abp.BlobStoring.Aliyun.Tests/Volo.Abp.BlobStoring.Aliyun.Tests.csproj @@ -12,7 +12,7 @@ - + diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Tests.cs index 091c1b7c95..a965f2852b 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainer_Tests.cs @@ -3,7 +3,9 @@ using System.Linq; using System.Threading.Tasks; using Shouldly; using Volo.Abp.BlobStoring.TestObjects; +using Volo.Abp.Clients; using Volo.Abp.Modularity; +using Volo.Abp.MultiTenancy; using Volo.Abp.Testing; using Xunit; @@ -14,11 +16,14 @@ namespace Volo.Abp.BlobStoring { protected IBlobContainer Container { get; } + protected ICurrentTenant CurrentTenant { get; } + protected BlobContainer_Tests() { Container = GetRequiredService>(); + CurrentTenant = GetRequiredService(); } - + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) { options.UseAutofac(); @@ -37,35 +42,68 @@ namespace Volo.Abp.BlobStoring result.SequenceEqual(testContent).ShouldBeTrue(); } + [Fact] + public async Task Should_Save_And_Get_Blobs_In_Different_Tenant() + { + var blobName = "test-blob-1"; + var testContent = "test content".GetBytes(); + + using (CurrentTenant.Change(Guid.NewGuid())) + { + await Container.SaveAsync(blobName, testContent); + (await Container.GetAllBytesAsync(blobName)).SequenceEqual(testContent).ShouldBeTrue(); + } + + using (CurrentTenant.Change(Guid.NewGuid())) + { + await Container.SaveAsync(blobName, testContent); + (await Container.GetAllBytesAsync(blobName)).SequenceEqual(testContent).ShouldBeTrue(); + + using (CurrentTenant.Change(null)) + { + // Could not found the requested BLOB... + await Assert.ThrowsAsync(async () => + await Container.GetAllBytesAsync(blobName) + ); + } + } + + using (CurrentTenant.Change(null)) + { + await Container.SaveAsync(blobName, testContent); + (await Container.GetAllBytesAsync(blobName)).SequenceEqual(testContent).ShouldBeTrue(); + } + } + [Fact] public async Task Should_Overwrite_Pre_Saved_Blob_If_Requested() { var blobName = "test-blob-1"; - + var testContent = "test content".GetBytes(); await Container.SaveAsync(blobName, testContent); - + var testContentOverwritten = "test content overwritten".GetBytes(); await Container.SaveAsync(blobName, testContentOverwritten, true); - + var result = await Container.GetAllBytesAsync(blobName); result.SequenceEqual(testContentOverwritten).ShouldBeTrue(); } - + [Fact] public async Task Should_Not_Allow_To_Overwrite_Pre_Saved_Blob_By_Default() { var blobName = "test-blob-1"; - + var testContent = "test content".GetBytes(); await Container.SaveAsync(blobName, testContent); - + var testContentOverwritten = "test content overwritten".GetBytes(); await Assert.ThrowsAsync(() => Container.SaveAsync(blobName, testContentOverwritten) ); } - + [Theory] [InlineData("test-blob-1")] [InlineData("test-blob-1.txt")] @@ -91,7 +129,7 @@ namespace Volo.Abp.BlobStoring await Container.DeleteAsync(blobName); (await Container.ExistsAsync(blobName)).ShouldBeFalse(); } - + [Theory] [InlineData("test-blob-1")] [InlineData("test-blob-1.txt")] @@ -102,4 +140,4 @@ namespace Volo.Abp.BlobStoring (await Container.ExistsAsync(blobName)).ShouldBeFalse(); } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs index 9c8926f38a..618b8769dc 100644 --- a/framework/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/System/Collections/Generic/AbpListExtensions_Tests.cs @@ -7,6 +7,20 @@ namespace System.Collections.Generic { public class AbpListExtensions_Tests { + [Fact] + public void InsertRange() + { + var list = Enumerable.Range(1, 3).ToList(); + list.InsertRange(1, new[] {7, 8, 9}); + + list[0].ShouldBe(1); + list[1].ShouldBe(7); + list[2].ShouldBe(8); + list[3].ShouldBe(9); + list[4].ShouldBe(2); + list[5].ShouldBe(3); + } + [Fact] public void InsertAfter() { @@ -191,7 +205,7 @@ namespace System.Collections.Generic { var list = RandomHelper .GenerateRandomizedList(new char[] {'A', 'B', 'C', 'D', 'E', 'F', 'G'}); - + list = list.SortByDependencies(c => dependencies[c]); foreach (var dependency in dependencies) @@ -204,4 +218,4 @@ namespace System.Collections.Generic } } } -} \ No newline at end of file +} diff --git a/framework/test/Volo.Abp.TextTemplating.Tests/Volo/Abp/TextTemplating/Localization/ar.json b/framework/test/Volo.Abp.TextTemplating.Tests/Volo/Abp/TextTemplating/Localization/ar.json new file mode 100644 index 0000000000..31a75bad8b --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Tests/Volo/Abp/TextTemplating/Localization/ar.json @@ -0,0 +1,7 @@ +{ + "culture": "ar", + "texts": { + "HelloText": "مرحبا {0}", + "HowAreYou": "كيف حالكم ؟" + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.VirtualFileSystem.Tests/Volo.Abp.VirtualFileSystem.Tests.csproj b/framework/test/Volo.Abp.VirtualFileSystem.Tests/Volo.Abp.VirtualFileSystem.Tests.csproj index bcb9dcbf74..122f54f075 100644 --- a/framework/test/Volo.Abp.VirtualFileSystem.Tests/Volo.Abp.VirtualFileSystem.Tests.csproj +++ b/framework/test/Volo.Abp.VirtualFileSystem.Tests/Volo.Abp.VirtualFileSystem.Tests.csproj @@ -16,7 +16,7 @@ - + - + diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo.Abp.Account.Application.Contracts.csproj b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo.Abp.Account.Application.Contracts.csproj index 8b2ebba2f8..722a75a22c 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo.Abp.Account.Application.Contracts.csproj +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo.Abp.Account.Application.Contracts.csproj @@ -21,7 +21,7 @@ - + - - \ No newline at end of file + + diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json index f0dbdaadb6..2940fff63b 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json @@ -43,6 +43,7 @@ "Description:Abp.Account.EnableLocalLogin": "Indicates if the server will allow users to authenticate with a local account.", "LoggedOutTitle": "Signed Out", "LoggedOutText": "You have been signed out and you will be redirected soon.", - "ReturnToText": "Click here to redirect to {0}" + "ReturnToText": "Click here to redirect to {0}", + "OrLoginWith": "Or login with;" } } diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj index c3a56c653d..bdcf0c9e8d 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj +++ b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj @@ -17,9 +17,9 @@ - + - + - + diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj index 2d62917fcc..566d40b3f9 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Volo.Abp.Account.Web.IdentityServer.csproj @@ -34,7 +34,7 @@ - + - + diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml index 3bace4850b..41986c3856 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Login.cshtml @@ -6,34 +6,34 @@ @model Volo.Abp.Account.Web.Pages.Account.LoginModel @inject IHtmlLocalizer L @inject Volo.Abp.Settings.ISettingProvider SettingProvider -@if (Model.EnableLocalLogin) -{ -
    -
    -

    @L["Login"]

    - @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) - { - - @L["AreYouANewUser"] - @L["Register"] - - } +
    +
    +

    @L["Login"]

    + @if (await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled)) + { + + @L["AreYouANewUser"] + @L["Register"] + + } + @if (Model.EnableLocalLogin) + {
    - - + +
    - +
    - +
    @@ -43,29 +43,30 @@ @L["Cancel"] }
    -
    -
    -} + } -@if (Model.VisibleExternalProviders.Any()) -{ -
    -

    @L["UseAnotherServiceToLogIn"]

    -
    - - - @foreach (var provider in Model.VisibleExternalProviders) - { - - } -
    -
    -} + @if (Model.VisibleExternalProviders.Any()) + { +
    +
    @L["OrLoginWith"]
    +
    + + + @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
    +
    + } + + @if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) + { +
    + @L["InvalidLoginRequest"] + @L["ThereAreNoLoginSchemesConfiguredForThisClient"] +
    + } -@if (!Model.EnableLocalLogin && !Model.VisibleExternalProviders.Any()) -{ -
    - @L["InvalidLoginRequest"] - @L["ThereAreNoLoginSchemesConfiguredForThisClient"]
    -} +
    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 9d6c540ce5..ccd6e07a0f 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 @@ -214,13 +214,23 @@ namespace Volo.Abp.Account.Web.Pages.Account //TODO: Handle other cases for result! // Get the information about the user from the external login provider - var info = await SignInManager.GetExternalLoginInfoAsync(); - if (info == null) + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) { throw new ApplicationException("Error loading external login information during confirmation."); } - var user = await CreateExternalUserAsync(info); + if (!IsEmailRetrievedFromExternalLogin(externalLoginInfo)) + { + return RedirectToPage("./Register", new + { + IsExternalLogin = true, + ExternalLoginAuthSchema = externalLoginInfo.LoginProvider, + ReturnUrl = returnUrl + }); + } + + var user = await CreateExternalUserAsync(externalLoginInfo); await SignInManager.SignInAsync(user, false); @@ -234,6 +244,11 @@ namespace Volo.Abp.Account.Web.Pages.Account return RedirectSafely(returnUrl, returnUrlHash); } + private static bool IsEmailRetrievedFromExternalLogin(ExternalLoginInfo externalLoginInfo) + { + return externalLoginInfo.Principal.FindFirstValue(AbpClaimTypes.Email) != null; + } + protected virtual async Task CreateExternalUserAsync(ExternalLoginInfo info) { var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email); 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 204540e7e7..42779e864c 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 @@ -4,18 +4,27 @@ @model Volo.Abp.Account.Web.Pages.Account.RegisterModel @inject IHtmlLocalizer L -
    -
    -

    @L["Register"]

    - - @L["AlreadyRegistered"] - @L["Login"] - -
    - - - - @L["Register"] - -
    +
    +
    +

    @L["Register"]

    + + @L["AlreadyRegistered"] + @L["Login"] + +
    + @if (!Model.IsExternalLogin) + { + + } + + + + @if (!Model.IsExternalLogin) + { + + } + + @L["Register"] +
    +
    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 66e938d48d..a8ec942084 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 @@ -1,15 +1,16 @@ using System; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Security.Claims; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Volo.Abp.Account.Settings; using Volo.Abp.Auditing; -using Volo.Abp.Application.Dtos; using Volo.Abp.Identity; using Volo.Abp.Settings; -using Volo.Abp.Uow; using Volo.Abp.Validation; using IdentityUser = Volo.Abp.Identity.IdentityUser; @@ -28,6 +29,12 @@ namespace Volo.Abp.Account.Web.Pages.Account [BindProperty] public PostInput Input { get; set; } + [BindProperty(SupportsGet = true)] + public bool IsExternalLogin { get; set; } + + [BindProperty(SupportsGet = true)] + public string ExternalLoginAuthSchema { get; set; } + public RegisterModel(IAccountAppService accountAppService) { AccountAppService = accountAppService; @@ -36,38 +43,108 @@ namespace Volo.Abp.Account.Web.Pages.Account public virtual async Task OnGetAsync() { await CheckSelfRegistrationAsync(); - + await TrySetEmailAsync(); return Page(); } - public virtual async Task OnPostAsync() + private async Task TrySetEmailAsync() { - ValidateModel(); + if (IsExternalLogin) + { + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) + { + return; + } + + if (!externalLoginInfo.Principal.Identities.Any()) + { + return; + } + var identity = externalLoginInfo.Principal.Identities.First(); + var emailClaim = identity.FindFirst(ClaimTypes.Email); + + if (emailClaim == null) + { + return; + } + + Input = new PostInput {EmailAddress = emailClaim.Value}; + } + } + + public virtual async Task OnPostAsync() + { try { await CheckSelfRegistrationAsync(); - var registerDto = new RegisterDto + if (IsExternalLogin) + { + var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync(); + if (externalLoginInfo == null) + { + Logger.LogWarning("External login info is not available"); + return RedirectToPage("./Login"); + } + + await RegisterExternalUserAsync(externalLoginInfo, Input.EmailAddress); + } + else + { + await RegisterLocalUserAsync(); + } + + return Redirect(ReturnUrl ?? "~/"); //TODO: How to ensure safety? IdentityServer requires it however it should be checked somehow! + } + catch (BusinessException e) + { + Alerts.Danger(e.Message); + return Page(); + } + } + + protected virtual async Task RegisterLocalUserAsync() + { + ValidateModel(); + + var userDto = await AccountAppService.RegisterAsync( + new RegisterDto { AppName = "MVC", EmailAddress = Input.EmailAddress, Password = Input.Password, UserName = Input.UserName - }; + } + ); - var userDto = await AccountAppService.RegisterAsync(registerDto); - var user = await UserManager.GetByIdAsync(userDto.Id); + var user = await UserManager.GetByIdAsync(userDto.Id); + await SignInManager.SignInAsync(user, isPersistent: true); + } - await SignInManager.SignInAsync(user, isPersistent: false); + protected virtual async Task RegisterExternalUserAsync(ExternalLoginInfo externalLoginInfo, string emailAddress) + { + var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id); - return Redirect(ReturnUrl ?? "~/"); //TODO: How to ensure safety? IdentityServer requires it however it should be checked somehow! - } - catch (BusinessException e) + (await UserManager.CreateAsync(user)).CheckErrors(); + (await UserManager.AddDefaultRolesAsync(user)).CheckErrors(); + + var userLoginAlreadyExists = user.Logins.Any(x => + x.TenantId == user.TenantId && + x.LoginProvider == externalLoginInfo.LoginProvider && + x.ProviderKey == externalLoginInfo.ProviderKey); + + if (!userLoginAlreadyExists) { - Alerts.Danger(e.Message); - return Page(); + (await UserManager.AddLoginAsync(user, new UserLoginInfo( + externalLoginInfo.LoginProvider, + externalLoginInfo.ProviderKey, + externalLoginInfo.ProviderDisplayName + ))).CheckErrors(); } + + await SignInManager.SignInAsync(user, isPersistent: true); } protected virtual async Task CheckSelfRegistrationAsync() diff --git a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj index 469c17db62..e4bbaf00ba 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj +++ b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj @@ -36,7 +36,7 @@ - + - - \ No newline at end of file + + diff --git a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj index 779c2d2d25..84e7de1746 100644 --- a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj +++ b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo.Abp.Account.Application.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests.csproj b/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests.csproj index 6287d732e2..0a85b56d87 100644 --- a/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests.csproj +++ b/modules/audit-logging/test/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests/Volo.Abp.AuditLogging.EntityFrameworkCore.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Volo.Abp.BackgroundJobs.DemoApp.Shared.csproj b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Volo.Abp.BackgroundJobs.DemoApp.Shared.csproj index cf5ed0ce08..df4f574663 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Volo.Abp.BackgroundJobs.DemoApp.Shared.csproj +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp.Shared/Volo.Abp.BackgroundJobs.DemoApp.Shared.csproj @@ -7,7 +7,7 @@ - + diff --git a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Volo.Abp.BackgroundJobs.DemoApp.csproj b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Volo.Abp.BackgroundJobs.DemoApp.csproj index 0d8388c610..0faa1370a8 100644 --- a/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Volo.Abp.BackgroundJobs.DemoApp.csproj +++ b/modules/background-jobs/app/Volo.Abp.BackgroundJobs.DemoApp/Volo.Abp.BackgroundJobs.DemoApp.csproj @@ -6,7 +6,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests.csproj b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests.csproj index 3246aefa4f..50c8e74edb 100644 --- a/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests.csproj +++ b/modules/background-jobs/test/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests/Volo.Abp.BackgroundJobs.EntityFrameworkCore.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/modules/blob-storing-database/host/BlobStoring.Database.Host.ConsoleApp/src/BlobStoring.Database.Host.ConsoleApp.ConsoleApp/BlobStoring.Database.Host.ConsoleApp.ConsoleApp.csproj b/modules/blob-storing-database/host/BlobStoring.Database.Host.ConsoleApp/src/BlobStoring.Database.Host.ConsoleApp.ConsoleApp/BlobStoring.Database.Host.ConsoleApp.ConsoleApp.csproj index 43455e655a..7e8d79ec68 100644 --- a/modules/blob-storing-database/host/BlobStoring.Database.Host.ConsoleApp/src/BlobStoring.Database.Host.ConsoleApp.ConsoleApp/BlobStoring.Database.Host.ConsoleApp.ConsoleApp.csproj +++ b/modules/blob-storing-database/host/BlobStoring.Database.Host.ConsoleApp/src/BlobStoring.Database.Host.ConsoleApp.ConsoleApp/BlobStoring.Database.Host.ConsoleApp.ConsoleApp.csproj @@ -20,13 +20,13 @@ - + - - + + diff --git a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobProvider.cs b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobProvider.cs index fbccdb067e..7fec4592c6 100644 --- a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobProvider.cs +++ b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobProvider.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.BlobStoring.Database { @@ -11,15 +12,18 @@ namespace Volo.Abp.BlobStoring.Database protected IDatabaseBlobRepository DatabaseBlobRepository { get; } protected IDatabaseBlobContainerRepository DatabaseBlobContainerRepository { get; } protected IGuidGenerator GuidGenerator { get; } + protected ICurrentTenant CurrentTenant { get; } public DatabaseBlobProvider( IDatabaseBlobRepository databaseBlobRepository, IDatabaseBlobContainerRepository databaseBlobContainerRepository, - IGuidGenerator guidGenerator) + IGuidGenerator guidGenerator, + ICurrentTenant currentTenant) { DatabaseBlobRepository = databaseBlobRepository; DatabaseBlobContainerRepository = databaseBlobContainerRepository; GuidGenerator = guidGenerator; + CurrentTenant = currentTenant; } public override async Task SaveAsync(BlobProviderSaveArgs args) @@ -48,7 +52,7 @@ namespace Volo.Abp.BlobStoring.Database } else { - blob = new DatabaseBlob(GuidGenerator.Create(), container.Id, args.BlobName, content); + blob = new DatabaseBlob(GuidGenerator.Create(), container.Id, args.BlobName, content, CurrentTenant.Id); await DatabaseBlobRepository.InsertAsync(blob, autoSave: true); } } @@ -128,10 +132,10 @@ namespace Volo.Abp.BlobStoring.Database return container; } - container = new DatabaseBlobContainer(GuidGenerator.Create(), name); + container = new DatabaseBlobContainer(GuidGenerator.Create(), name, CurrentTenant.Id); await DatabaseBlobContainerRepository.InsertAsync(container, cancellationToken: cancellationToken); return container; } } -} \ No newline at end of file +} diff --git a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/EfCoreDatabaseBlobContainer_Tests.cs b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/EfCoreDatabaseBlobContainer_Tests.cs new file mode 100644 index 0000000000..094217f9e6 --- /dev/null +++ b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/EntityFrameworkCore/EfCoreDatabaseBlobContainer_Tests.cs @@ -0,0 +1,7 @@ +namespace Volo.Abp.BlobStoring.Database.EntityFrameworkCore +{ + public class EfCoreDatabaseBlobContainer_Tests : DatabaseBlobContainer_Tests + { + + } +} diff --git a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests.csproj b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests.csproj index 04bdd5071a..fdaeecf17e 100644 --- a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests.csproj +++ b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests/Volo.Abp.BlobStoring.Database.EntityFrameworkCore.Tests.csproj @@ -7,9 +7,9 @@ - - - + + + diff --git a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.MongoDB.Tests/MongoDB/MongoDbDatabaseBlobContainer_Tests.cs b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.MongoDB.Tests/MongoDB/MongoDbDatabaseBlobContainer_Tests.cs new file mode 100644 index 0000000000..4d476d5e75 --- /dev/null +++ b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.MongoDB.Tests/MongoDB/MongoDbDatabaseBlobContainer_Tests.cs @@ -0,0 +1,10 @@ +using Xunit; + +namespace Volo.Abp.BlobStoring.Database.MongoDB +{ + [Collection(MongoTestCollection.Name)] + public class MongoDbDatabaseBlobContainer_Tests : DatabaseBlobContainer_Tests + { + + } +} diff --git a/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.TestBase/DatabaseBlobContainer_Tests.cs b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.TestBase/DatabaseBlobContainer_Tests.cs new file mode 100644 index 0000000000..542bb82113 --- /dev/null +++ b/modules/blob-storing-database/test/Volo.Abp.BlobStoring.Database.TestBase/DatabaseBlobContainer_Tests.cs @@ -0,0 +1,10 @@ +using Volo.Abp.Modularity; + +namespace Volo.Abp.BlobStoring.Database +{ + public abstract class DatabaseBlobContainer_Tests : BlobContainer_Tests + where TStartupModule : IAbpModule + { + + } +} diff --git a/modules/blogging/app/Volo.BloggingTestApp.EntityFrameworkCore/Volo.BloggingTestApp.EntityFrameworkCore.csproj b/modules/blogging/app/Volo.BloggingTestApp.EntityFrameworkCore/Volo.BloggingTestApp.EntityFrameworkCore.csproj index ad5f9321ff..5d3175a12a 100644 --- a/modules/blogging/app/Volo.BloggingTestApp.EntityFrameworkCore/Volo.BloggingTestApp.EntityFrameworkCore.csproj +++ b/modules/blogging/app/Volo.BloggingTestApp.EntityFrameworkCore/Volo.BloggingTestApp.EntityFrameworkCore.csproj @@ -15,8 +15,8 @@ - - + + diff --git a/modules/blogging/app/Volo.BloggingTestApp/package.json b/modules/blogging/app/Volo.BloggingTestApp/package.json index 9d3c620dd3..63cfac4070 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/package.json +++ b/modules/blogging/app/Volo.BloggingTestApp/package.json @@ -3,7 +3,7 @@ "name": "volo.blogtestapp", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.4", - "@abp/blogging": "^3.0.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.5", + "@abp/blogging": "^3.0.5" } } \ No newline at end of file diff --git a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock index 3ddac913ca..fcd9dbcd00 100644 --- a/modules/blogging/app/Volo.BloggingTestApp/yarn.lock +++ b/modules/blogging/app/Volo.BloggingTestApp/yarn.lock @@ -2,37 +2,37 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.4.tgz#c58e3e06ba78279875cf2b5f96d34b83ce09f6df" - integrity sha512-tKKqVRKpwN7P4Mn+he3bcz4Qbt8bkZ++wk6iOujd6HLTkBNa0uKyM4L6+iCn1Hi62S6HeK2Xytx+b+IYeIlsuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.4.tgz#50db612b6492d36c63dd9042000a9fb00fecec54" - integrity sha512-imd2T5XXkNvGQyXV/A9hA80qSeurKaUcW6HDkte8Y2fs4QfvkjgzWXi7neqF6F+HVbYYPg3rIBWC6QF1OXGn2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~3.0.4" - "@abp/bootstrap" "~3.0.4" - "@abp/bootstrap-datepicker" "~3.0.4" - "@abp/datatables.net-bs4" "~3.0.4" - "@abp/font-awesome" "~3.0.4" - "@abp/jquery-form" "~3.0.4" - "@abp/jquery-validation-unobtrusive" "~3.0.4" - "@abp/lodash" "~3.0.4" - "@abp/luxon" "~3.0.4" - "@abp/malihu-custom-scrollbar-plugin" "~3.0.4" - "@abp/select2" "~3.0.4" - "@abp/sweetalert" "~3.0.4" - "@abp/timeago" "~3.0.4" - "@abp/toastr" "~3.0.4" - -"@abp/aspnetcore.mvc.ui@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.4.tgz#4e4344d95a98a60c53037b351cb915d8ff7a7dcb" - integrity sha512-p7KI5CKkpD4EYzG4fCtygrtZhwMu7Z+IZFGbdqXelLbI5ACkbJyrn+N+9trPfS9papymeXY4p1E3v6xOH7rSHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.5.tgz#c146f20226522f0305cc0fa31cfcf9d39be78901" + integrity sha512-ptNhD4xV2wSLW7vQ5KUgaRiH5Ov1OscZW2ZEoivnt3kqgtIC3DGw+SiE9vjFCvyY83fjRkIlW2PIfQxSySbteQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.5" + +"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76" + integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg== + dependencies: + "@abp/aspnetcore.mvc.ui" "~3.0.5" + "@abp/bootstrap" "~3.0.5" + "@abp/bootstrap-datepicker" "~3.0.5" + "@abp/datatables.net-bs4" "~3.0.5" + "@abp/font-awesome" "~3.0.5" + "@abp/jquery-form" "~3.0.5" + "@abp/jquery-validation-unobtrusive" "~3.0.5" + "@abp/lodash" "~3.0.5" + "@abp/luxon" "~3.0.5" + "@abp/malihu-custom-scrollbar-plugin" "~3.0.5" + "@abp/select2" "~3.0.5" + "@abp/sweetalert" "~3.0.5" + "@abp/timeago" "~3.0.5" + "@abp/toastr" "~3.0.5" + +"@abp/aspnetcore.mvc.ui@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0" + integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg== dependencies: ansi-colors "^4.1.1" extend-object "^1.0.0" @@ -41,213 +41,213 @@ path "^0.12.7" rimraf "^3.0.2" -"@abp/blogging@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-3.0.4.tgz#d309b18a410d047346c39ab3063cd95429ca693a" - integrity sha512-HjPmNgFuAI1PPB4Rn8q+CYZq77kTqdRByIbYHvM9NsLyjb4CKruJxwHIFQUEs2oaB1V0/1CtfQWOcJ8r6ciGfA== +"@abp/blogging@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/blogging/-/blogging-3.0.5.tgz#07d476d9d7bb20b52ccd122c8cf7c7c2bd3c1aa4" + integrity sha512-3qBS3EfBRrcGzgo8xaJXIVNu/qkuhBLYixPur69nUa8sDAsYI725mVIOfznyYyTAxPXB2EDOf4WA+NKiEINrqQ== dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.4" - "@abp/owl.carousel" "~3.0.4" - "@abp/prismjs" "~3.0.4" - "@abp/tui-editor" "~3.0.4" + "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.5" + "@abp/owl.carousel" "~3.0.5" + "@abp/prismjs" "~3.0.5" + "@abp/tui-editor" "~3.0.5" -"@abp/bootstrap-datepicker@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.4.tgz#d21963f158a4d348a0acdc46209e67378aae8e55" - integrity sha512-25e9qm39dj9AijIchQIdLlBiBM5/fhASkEuLKNt6M8S/e3qwKLwbRhX90DKMFvOyKdxxcK1gX7tDuGHyG9A8mA== +"@abp/bootstrap-datepicker@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540" + integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA== dependencies: bootstrap-datepicker "^1.9.0" -"@abp/bootstrap@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.4.tgz#3b0e2b74dc174a04ce669de1b140df833eee6031" - integrity sha512-DwxL1fGYOwChPwmMpwUxCKiIdzTfRf/YDkOLUiwI8eRrpABBeuE2sSHKfks6Rqh9Wqo/nlWXvfccgDRqweqhsA== +"@abp/bootstrap@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880" + integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" bootstrap "^4.5.0" bootstrap-v4-rtl "4.4.1-2" -"@abp/clipboard@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-3.0.4.tgz#17ff40e398de3c0ab5214e564d443ed52ac10630" - integrity sha512-vjjupRau7fgBxvQwaDrBHApP4Ce1x9XM7L64/c+A+oBny0SzuiSBGBYsjlFrJGmTHT/PhjoB1XQiBHrYYPXQfQ== +"@abp/clipboard@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-3.0.5.tgz#7ceadd21360db11c91582a532029bfe65d10a930" + integrity sha512-iq455C096L5D+b6IrWkMxA9mbpggBNZ1kWfCQ3rhIjo0qbOGrvdGB3uQ+BqRS9Ni9ku7vBb2COldouIC9KOp5A== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" clipboard "^2.0.6" -"@abp/codemirror@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-3.0.4.tgz#d223b9e44719844948025f1c47bd404923d9e40d" - integrity sha512-shv8bqnATrEg9K9P0m14eqlJCCSfSO57SCxYmF0DUH0wuykkWjFJ8gok2GijVZtA9FQDJbNbNXh2oSm4MCgRsA== +"@abp/codemirror@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/codemirror/-/codemirror-3.0.5.tgz#2d60ad50f7b50833b0032f3c2e8992a89f257aa9" + integrity sha512-RkbmRGmSCB2962rJdhp0NgJ+qEP4gG05wuSuCMsYJTU3U9IkhkqiJbzupUzAkHQTaCQwCOUeXoBLUkwCHUP2DA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" codemirror "^5.54.0" -"@abp/core@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.4.tgz#281940525e280e9e3a1aa4cb4e8f64548ea026a3" - integrity sha512-ZPEH1fa5bf/f1ek9ZLQHx7u78iF6kRZ8Z2NBI36cNL13RyQbx485qJF+KXyqNlJoJAh0CAs0NsAJADU3utcDNA== +"@abp/core@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8" + integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA== dependencies: - "@abp/utils" "^3.0.4" + "@abp/utils" "^3.0.5" -"@abp/datatables.net-bs4@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.4.tgz#e488336bd2c199146ea60464e0ba75aeb134957e" - integrity sha512-ID1XWKPxG9pv6mxBkku7JWvgXjTxJ1uShxmYv5kFYVuwOGMSvj8QVZInTHFcMdECopV56yjGPNOIFLeQj4Mw8Q== +"@abp/datatables.net-bs4@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a" + integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ== dependencies: - "@abp/datatables.net" "~3.0.4" + "@abp/datatables.net" "~3.0.5" datatables.net-bs4 "^1.10.21" -"@abp/datatables.net@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.4.tgz#7ef143b83143ad8d769be25296411a812a17b255" - integrity sha512-gdQwRuxO5fQWhG/DiUaf40DQ8dWIVGhOCyt6SZ8LQzq4uSaQqa0qrtTTTW2/aiee4MgJu2vJWYF5YCszIR/HhA== +"@abp/datatables.net@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491" + integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" datatables.net "^1.10.21" -"@abp/font-awesome@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.4.tgz#98f83d0bf8b5a2a4d988cb07f5cceda2e1bc1a09" - integrity sha512-AKhKCUXx8vdHIgyXUfbSLitfYor5dJbo4qMRRGqIJVSj+cyMkpgYbGjqr24GGijgZxdmOGL+s1b9iCUbIPgLNQ== +"@abp/font-awesome@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412" + integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" "@fortawesome/fontawesome-free" "^5.13.0" -"@abp/highlight.js@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-3.0.4.tgz#2f4cb60b7b621fd73f3eb92f70c3d8611ce07c2b" - integrity sha512-7iTDFgBBHn3w5MlhRylkV5to0+8cl0akVspe358zvIy9bdBv+t8Xgc8IomxKqA5EfjBFhR+Q/g0+DfoWLpL7xw== +"@abp/highlight.js@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/highlight.js/-/highlight.js-3.0.5.tgz#f222b54958bd842840db5ad8b32d82f1cd209302" + integrity sha512-VT9UfukzbhMsWhUZNc8b792fY04lqkuJL98QprU59No1Rb7+XUa1k9FMbt2kl12Je3jltY3+UrDl+vrdasFxKg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" -"@abp/jquery-form@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.4.tgz#022eebbfe58b55e328984c0f119884c9464bc767" - integrity sha512-xDkk4rY0AmXBJE4qztzr2hZz+OVucVNWeagOEgeQHXJSKU/T+0kj0a1RxEHpyI24xBvEXAbe8PbByTbu0QXYew== +"@abp/jquery-form@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78" + integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.4.tgz#6cc942aadcdde33893e7a45c84aa022e033e4e8b" - integrity sha512-N9sza91HCyPvJo+d0RAfq4skae3TgAYg7KXhlwzRE2hqUQS+EICVARURWyxJW7s7Zu6+gxDTkXhBZRTW56deZA== +"@abp/jquery-validation-unobtrusive@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f" + integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw== dependencies: - "@abp/jquery-validation" "~3.0.4" + "@abp/jquery-validation" "~3.0.5" jquery-validation-unobtrusive "^3.2.11" -"@abp/jquery-validation@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.4.tgz#3f96d1f83c75a7fc47e5e5e12c51dbc8da7582b1" - integrity sha512-2ljqas4YKU0PcwB0wQW2qhXejDHjW+N1XCxZDyCZ2wF9UxGlKBR8rFt76TJByXSotKE7n2hJRZaOh9ICZUow2g== +"@abp/jquery-validation@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279" + integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-validation "^1.19.2" -"@abp/jquery@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.4.tgz#5f236d95cfae7f65012cd8778fd74490b7542442" - integrity sha512-g8N8Te5vf+8djdksfZoZ3tXaqlBC4Ra39pAWU/XerlLl64u+XmAX1s6yKRfC2kS8lf292GgiFYuNOkb77J6k4A== +"@abp/jquery@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754" + integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" jquery "~3.5.1" -"@abp/lodash@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.4.tgz#ee60bc99f98b59fd9fc5ff4bbcf914a13faf209d" - integrity sha512-jxvv74Sm/fUKDBMylK4Ne/8GKm7GuHAdcLFtkoJQptdO+RjZpKJ86ULT+X5vbKskZaTj2fEmOR6l4hmfnBULTA== +"@abp/lodash@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4" + integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" lodash "^4.17.15" -"@abp/luxon@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.4.tgz#aa1bf6a93d6254433101e50a205b276759969414" - integrity sha512-gX9dQJt9C6M1wmCSUIBoNr8CAt0jkUZFQ7LKKO/BoADGM/XFvY5vDbhL7zcwbwitgCOzmucoCxs/6rIxSOVlrg== +"@abp/luxon@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688" + integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw== dependencies: luxon "^1.24.1" -"@abp/malihu-custom-scrollbar-plugin@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.4.tgz#c54824b98363f4190cc6a37fcd421c2bfdc1e201" - integrity sha512-6LdSGctOr4ACtdohdSlb19vrFNJ5C0hy1VdbBEafHOBspXckvRVx4uJl2KfHmAC+bMg9apoK/wcHjVNdBHThYQ== +"@abp/malihu-custom-scrollbar-plugin@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39" + integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/markdown-it@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-3.0.4.tgz#8be97cc1a48add764c76049c6303291252c56b12" - integrity sha512-94HjUbRQ+/RZoG0/h0nYA8Kk6IqJUtVzwM8/rUWGi45kwkSk6N/mCv537QD4hivHS0sLDx2KgEve9Ka8WnIKbQ== +"@abp/markdown-it@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/markdown-it/-/markdown-it-3.0.5.tgz#43c0daf0dd30de3d0109980aa5493db6f72108c1" + integrity sha512-HMRv5B29fQ5nPz4wFlfTsJ3dACotNLHZUuQbOt8SifRkt9Va0t20j+IwIGhSYVc93ttYHjB0IuMk6G10mdBA1A== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" markdown-it "^11.0.0" -"@abp/owl.carousel@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-3.0.4.tgz#18b4f93d06ad3b0f22f9fa4073b757f54e559c93" - integrity sha512-yZjlZ28JCuYSJPtnfoXssn1PdinbLbB4/Rixi43b2RbE0pI3U6ZnUOd0TAOwZyS7sO0OLdlGp2F/MKbln8u53A== +"@abp/owl.carousel@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/owl.carousel/-/owl.carousel-3.0.5.tgz#d575d201a9c4853aa8578e6d8da396de60d6c961" + integrity sha512-fBzNaJPRnFc1VV5MBpRAGr3yZtkcbRHOOKlq74bkg9kvnqNa8dLISPRxHsBCtX+bxndWI3OfmN9Uliiq39TgSA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" owl.carousel "^2.3.4" -"@abp/prismjs@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-3.0.4.tgz#9cd8113b33dfd1b349b12740a2f7462f88bf3841" - integrity sha512-D+RNAhUhOCXnBNZVOq9EQg883eRbPKlIpFsxeMW065XOm5g7iO0eJACvyqGC8mt5reHmtxApnfiGNlhT1dK+Eg== +"@abp/prismjs@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-3.0.5.tgz#81a6ef656f3d7a76f365eb0bcbf2d0d3013d6e72" + integrity sha512-vdSSa1+tXS5+HWIgQyjDy4yQG7Q1YyuHQnFU6zlDJCQlE+20PL6lYgPnqR7DQ+Cd4lhRb3oxvloImYjwiwSQvA== dependencies: - "@abp/clipboard" "~3.0.4" - "@abp/core" "~3.0.4" + "@abp/clipboard" "~3.0.5" + "@abp/core" "~3.0.5" prismjs "^1.20.0" -"@abp/select2@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.4.tgz#49db0cfbc9b08f6ae4949b2cb8bdef92d7d308dd" - integrity sha512-zKEgkWBFdGU2KiAMUKhucHqBfzDy2KNSefyqxpOiUd1L3PEKu+9o23HZRWzDupYltop+SO1Jw33CNoVAkKm4+A== +"@abp/select2@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350" + integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" select2 "^4.0.13" -"@abp/sweetalert@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.4.tgz#f85d11c5aabc6ceb509af1b8fd8d75bc3c74bd27" - integrity sha512-hO0qXUWxeMy/lfy3sb+5HvOmIPst6UvYJJGy3ZQR6DKEdkOktBX9sEDb7m6ij6xAIHqWIx1xLveul0oGteOG0Q== +"@abp/sweetalert@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab" + integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" sweetalert "^2.1.2" -"@abp/timeago@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.4.tgz#82d90a04f703a7db9c0acedcd2f3cea684e7ae18" - integrity sha512-7SbbqScTKCix25xWy5oLT0slJqUQsW6YgJgI0N41C3AsByRBXZeh5Gcep033gprTPmpgNY8JOWcEcCXySnYvfA== +"@abp/timeago@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31" + integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" timeago "^1.6.7" -"@abp/toastr@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.4.tgz#884ab2343ddd59981c291c632e3b0630532f7e9c" - integrity sha512-NG0pVD8bdIXGpjPW9cozCzmyuAdnSLE4vDYLUReP1pZFp9B8SMJqpgPk9eFg0Y06aaHTVMKYBKRoq9xXS4w9Xg== +"@abp/toastr@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc" + integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" toastr "^2.1.4" -"@abp/tui-editor@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-3.0.4.tgz#fe44e7e28bd9d3902fe2e77b4e78eb2aa2959846" - integrity sha512-+H03yYEtPbX+THnzdsW1j+XXSYMrKl8oLPIrrl71QkUW7mif+c2s6YHauJ2mqJ1Uc/7fEL/i1/nBkqU3XjQXPw== +"@abp/tui-editor@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/tui-editor/-/tui-editor-3.0.5.tgz#9eee7cb555793a9c01a6e0ce10e77bb46577857c" + integrity sha512-xwaBhwnyvoIbEKWMSrTs2JHIOf3JK6tsABfcxhTRHXQfr2VONOEjbaLaAruNOuLJy3m/7rOsGWTeT8lMWwf3Fw== dependencies: - "@abp/codemirror" "~3.0.4" - "@abp/highlight.js" "~3.0.4" - "@abp/jquery" "~3.0.4" - "@abp/markdown-it" "~3.0.4" + "@abp/codemirror" "~3.0.5" + "@abp/highlight.js" "~3.0.5" + "@abp/jquery" "~3.0.5" + "@abp/markdown-it" "~3.0.5" tui-editor "^1.4.10" -"@abp/utils@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.4.tgz#38068fa8f735e1889cf2f226128366825948814d" - integrity sha512-TnXRZIZcxv5+EctEPsZEi2cyRXbpl3A679wzQa6BACv+pM45cjGO1JR8P0VswKN4TVL0aQRMv26N6U/uie8sEQ== +"@abp/utils@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49" + integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg== dependencies: just-compare "^1.3.0" diff --git a/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj b/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj index 611e62b1ec..16520deaf6 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj +++ b/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj @@ -24,9 +24,9 @@ - + - + diff --git a/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo.Blogging.Domain.Shared.csproj b/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo.Blogging.Domain.Shared.csproj index cf9c05e3b2..ca7dcca8a2 100644 --- a/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo.Blogging.Domain.Shared.csproj +++ b/modules/blogging/src/Volo.Blogging.Domain.Shared/Volo.Blogging.Domain.Shared.csproj @@ -19,9 +19,9 @@ - + - + - + diff --git a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj index 1c2ddac81f..511b2b5d39 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj +++ b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj @@ -24,9 +24,9 @@ - + - + diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json index 8c5602c2c7..c4410d5c13 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/package.json @@ -3,6 +3,6 @@ "name": "client-simulation-web", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.5" } } \ No newline at end of file diff --git a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock index 72373bfe9d..2233b4f0cb 100644 --- a/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock +++ b/modules/client-simulation/demo/Volo.ClientSimulation.Demo/yarn.lock @@ -2,37 +2,37 @@ # yarn lockfile v1 -"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.4.tgz#c58e3e06ba78279875cf2b5f96d34b83ce09f6df" - integrity sha512-tKKqVRKpwN7P4Mn+he3bcz4Qbt8bkZ++wk6iOujd6HLTkBNa0uKyM4L6+iCn1Hi62S6HeK2Xytx+b+IYeIlsuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.4.tgz#50db612b6492d36c63dd9042000a9fb00fecec54" - integrity sha512-imd2T5XXkNvGQyXV/A9hA80qSeurKaUcW6HDkte8Y2fs4QfvkjgzWXi7neqF6F+HVbYYPg3rIBWC6QF1OXGn2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~3.0.4" - "@abp/bootstrap" "~3.0.4" - "@abp/bootstrap-datepicker" "~3.0.4" - "@abp/datatables.net-bs4" "~3.0.4" - "@abp/font-awesome" "~3.0.4" - "@abp/jquery-form" "~3.0.4" - "@abp/jquery-validation-unobtrusive" "~3.0.4" - "@abp/lodash" "~3.0.4" - "@abp/luxon" "~3.0.4" - "@abp/malihu-custom-scrollbar-plugin" "~3.0.4" - "@abp/select2" "~3.0.4" - "@abp/sweetalert" "~3.0.4" - "@abp/timeago" "~3.0.4" - "@abp/toastr" "~3.0.4" - -"@abp/aspnetcore.mvc.ui@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.4.tgz#4e4344d95a98a60c53037b351cb915d8ff7a7dcb" - integrity sha512-p7KI5CKkpD4EYzG4fCtygrtZhwMu7Z+IZFGbdqXelLbI5ACkbJyrn+N+9trPfS9papymeXY4p1E3v6xOH7rSHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.5.tgz#c146f20226522f0305cc0fa31cfcf9d39be78901" + integrity sha512-ptNhD4xV2wSLW7vQ5KUgaRiH5Ov1OscZW2ZEoivnt3kqgtIC3DGw+SiE9vjFCvyY83fjRkIlW2PIfQxSySbteQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.5" + +"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76" + integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg== + dependencies: + "@abp/aspnetcore.mvc.ui" "~3.0.5" + "@abp/bootstrap" "~3.0.5" + "@abp/bootstrap-datepicker" "~3.0.5" + "@abp/datatables.net-bs4" "~3.0.5" + "@abp/font-awesome" "~3.0.5" + "@abp/jquery-form" "~3.0.5" + "@abp/jquery-validation-unobtrusive" "~3.0.5" + "@abp/lodash" "~3.0.5" + "@abp/luxon" "~3.0.5" + "@abp/malihu-custom-scrollbar-plugin" "~3.0.5" + "@abp/select2" "~3.0.5" + "@abp/sweetalert" "~3.0.5" + "@abp/timeago" "~3.0.5" + "@abp/toastr" "~3.0.5" + +"@abp/aspnetcore.mvc.ui@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0" + integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg== dependencies: ansi-colors "^4.1.1" extend-object "^1.0.0" @@ -41,144 +41,144 @@ path "^0.12.7" rimraf "^3.0.2" -"@abp/bootstrap-datepicker@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.4.tgz#d21963f158a4d348a0acdc46209e67378aae8e55" - integrity sha512-25e9qm39dj9AijIchQIdLlBiBM5/fhASkEuLKNt6M8S/e3qwKLwbRhX90DKMFvOyKdxxcK1gX7tDuGHyG9A8mA== +"@abp/bootstrap-datepicker@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540" + integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA== dependencies: bootstrap-datepicker "^1.9.0" -"@abp/bootstrap@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.4.tgz#3b0e2b74dc174a04ce669de1b140df833eee6031" - integrity sha512-DwxL1fGYOwChPwmMpwUxCKiIdzTfRf/YDkOLUiwI8eRrpABBeuE2sSHKfks6Rqh9Wqo/nlWXvfccgDRqweqhsA== +"@abp/bootstrap@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880" + integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" bootstrap "^4.5.0" bootstrap-v4-rtl "4.4.1-2" -"@abp/core@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.4.tgz#281940525e280e9e3a1aa4cb4e8f64548ea026a3" - integrity sha512-ZPEH1fa5bf/f1ek9ZLQHx7u78iF6kRZ8Z2NBI36cNL13RyQbx485qJF+KXyqNlJoJAh0CAs0NsAJADU3utcDNA== +"@abp/core@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8" + integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA== dependencies: - "@abp/utils" "^3.0.4" + "@abp/utils" "^3.0.5" -"@abp/datatables.net-bs4@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.4.tgz#e488336bd2c199146ea60464e0ba75aeb134957e" - integrity sha512-ID1XWKPxG9pv6mxBkku7JWvgXjTxJ1uShxmYv5kFYVuwOGMSvj8QVZInTHFcMdECopV56yjGPNOIFLeQj4Mw8Q== +"@abp/datatables.net-bs4@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a" + integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ== dependencies: - "@abp/datatables.net" "~3.0.4" + "@abp/datatables.net" "~3.0.5" datatables.net-bs4 "^1.10.21" -"@abp/datatables.net@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.4.tgz#7ef143b83143ad8d769be25296411a812a17b255" - integrity sha512-gdQwRuxO5fQWhG/DiUaf40DQ8dWIVGhOCyt6SZ8LQzq4uSaQqa0qrtTTTW2/aiee4MgJu2vJWYF5YCszIR/HhA== +"@abp/datatables.net@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491" + integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" datatables.net "^1.10.21" -"@abp/font-awesome@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.4.tgz#98f83d0bf8b5a2a4d988cb07f5cceda2e1bc1a09" - integrity sha512-AKhKCUXx8vdHIgyXUfbSLitfYor5dJbo4qMRRGqIJVSj+cyMkpgYbGjqr24GGijgZxdmOGL+s1b9iCUbIPgLNQ== +"@abp/font-awesome@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412" + integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" "@fortawesome/fontawesome-free" "^5.13.0" -"@abp/jquery-form@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.4.tgz#022eebbfe58b55e328984c0f119884c9464bc767" - integrity sha512-xDkk4rY0AmXBJE4qztzr2hZz+OVucVNWeagOEgeQHXJSKU/T+0kj0a1RxEHpyI24xBvEXAbe8PbByTbu0QXYew== +"@abp/jquery-form@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78" + integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.4.tgz#6cc942aadcdde33893e7a45c84aa022e033e4e8b" - integrity sha512-N9sza91HCyPvJo+d0RAfq4skae3TgAYg7KXhlwzRE2hqUQS+EICVARURWyxJW7s7Zu6+gxDTkXhBZRTW56deZA== +"@abp/jquery-validation-unobtrusive@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f" + integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw== dependencies: - "@abp/jquery-validation" "~3.0.4" + "@abp/jquery-validation" "~3.0.5" jquery-validation-unobtrusive "^3.2.11" -"@abp/jquery-validation@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.4.tgz#3f96d1f83c75a7fc47e5e5e12c51dbc8da7582b1" - integrity sha512-2ljqas4YKU0PcwB0wQW2qhXejDHjW+N1XCxZDyCZ2wF9UxGlKBR8rFt76TJByXSotKE7n2hJRZaOh9ICZUow2g== +"@abp/jquery-validation@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279" + integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-validation "^1.19.2" -"@abp/jquery@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.4.tgz#5f236d95cfae7f65012cd8778fd74490b7542442" - integrity sha512-g8N8Te5vf+8djdksfZoZ3tXaqlBC4Ra39pAWU/XerlLl64u+XmAX1s6yKRfC2kS8lf292GgiFYuNOkb77J6k4A== +"@abp/jquery@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754" + integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" jquery "~3.5.1" -"@abp/lodash@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.4.tgz#ee60bc99f98b59fd9fc5ff4bbcf914a13faf209d" - integrity sha512-jxvv74Sm/fUKDBMylK4Ne/8GKm7GuHAdcLFtkoJQptdO+RjZpKJ86ULT+X5vbKskZaTj2fEmOR6l4hmfnBULTA== +"@abp/lodash@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4" + integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" lodash "^4.17.15" -"@abp/luxon@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.4.tgz#aa1bf6a93d6254433101e50a205b276759969414" - integrity sha512-gX9dQJt9C6M1wmCSUIBoNr8CAt0jkUZFQ7LKKO/BoADGM/XFvY5vDbhL7zcwbwitgCOzmucoCxs/6rIxSOVlrg== +"@abp/luxon@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688" + integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw== dependencies: luxon "^1.24.1" -"@abp/malihu-custom-scrollbar-plugin@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.4.tgz#c54824b98363f4190cc6a37fcd421c2bfdc1e201" - integrity sha512-6LdSGctOr4ACtdohdSlb19vrFNJ5C0hy1VdbBEafHOBspXckvRVx4uJl2KfHmAC+bMg9apoK/wcHjVNdBHThYQ== +"@abp/malihu-custom-scrollbar-plugin@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39" + integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/select2@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.4.tgz#49db0cfbc9b08f6ae4949b2cb8bdef92d7d308dd" - integrity sha512-zKEgkWBFdGU2KiAMUKhucHqBfzDy2KNSefyqxpOiUd1L3PEKu+9o23HZRWzDupYltop+SO1Jw33CNoVAkKm4+A== +"@abp/select2@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350" + integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" select2 "^4.0.13" -"@abp/sweetalert@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.4.tgz#f85d11c5aabc6ceb509af1b8fd8d75bc3c74bd27" - integrity sha512-hO0qXUWxeMy/lfy3sb+5HvOmIPst6UvYJJGy3ZQR6DKEdkOktBX9sEDb7m6ij6xAIHqWIx1xLveul0oGteOG0Q== +"@abp/sweetalert@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab" + integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" sweetalert "^2.1.2" -"@abp/timeago@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.4.tgz#82d90a04f703a7db9c0acedcd2f3cea684e7ae18" - integrity sha512-7SbbqScTKCix25xWy5oLT0slJqUQsW6YgJgI0N41C3AsByRBXZeh5Gcep033gprTPmpgNY8JOWcEcCXySnYvfA== +"@abp/timeago@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31" + integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" timeago "^1.6.7" -"@abp/toastr@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.4.tgz#884ab2343ddd59981c291c632e3b0630532f7e9c" - integrity sha512-NG0pVD8bdIXGpjPW9cozCzmyuAdnSLE4vDYLUReP1pZFp9B8SMJqpgPk9eFg0Y06aaHTVMKYBKRoq9xXS4w9Xg== +"@abp/toastr@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc" + integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" toastr "^2.1.4" -"@abp/utils@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.4.tgz#38068fa8f735e1889cf2f226128366825948814d" - integrity sha512-TnXRZIZcxv5+EctEPsZEi2cyRXbpl3A679wzQa6BACv+pM45cjGO1JR8P0VswKN4TVL0aQRMv26N6U/uie8sEQ== +"@abp/utils@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49" + integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg== dependencies: just-compare "^1.3.0" diff --git a/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj b/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj index 3fb091137d..2a0c5e57c1 100644 --- a/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj +++ b/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj @@ -14,8 +14,8 @@ - - + + diff --git a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj index cc4b05cf3d..da90915bf2 100644 --- a/modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj +++ b/modules/cms-kit/host/Volo.CmsKit.IdentityServer/Volo.CmsKit.IdentityServer.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/Volo.CmsKit.Web.Host.csproj b/modules/cms-kit/host/Volo.CmsKit.Web.Host/Volo.CmsKit.Web.Host.csproj index a137c30124..46b83d5180 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/Volo.CmsKit.Web.Host.csproj +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/Volo.CmsKit.Web.Host.csproj @@ -13,8 +13,8 @@ - - + + @@ -28,7 +28,7 @@ - + @@ -52,5 +52,5 @@ Always - + diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs index 183469ce48..32e1b9f905 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs @@ -112,6 +112,11 @@ namespace Volo.CmsKit { options.IsEnabled = MultiTenancyConsts.IsEnabled; }); + + Configure(options => + { + options.PublicCommentEntities.Add("publicQuote"); + }); } public override void OnApplicationInitialization(ApplicationInitializationContext context) diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200719204212_Added_Reactions.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200719204212_Added_Reactions.cs deleted file mode 100644 index c853458396..0000000000 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200719204212_Added_Reactions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Volo.CmsKit.Migrations -{ - public partial class Added_Reactions : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "CmsUserReactions", - columns: table => new - { - Id = table.Column(nullable: false), - EntityType = table.Column(maxLength: 64, nullable: false), - EntityId = table.Column(maxLength: 64, nullable: false), - ReactionName = table.Column(maxLength: 32, nullable: false), - CreatorId = table.Column(nullable: false), - CreationTime = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_CmsUserReactions", x => x.Id); - }); - - migrationBuilder.CreateIndex( - name: "IX_CmsUserReactions_EntityType_EntityId", - table: "CmsUserReactions", - columns: new[] { "EntityType", "EntityId" }); - - migrationBuilder.CreateIndex( - name: "IX_CmsUserReactions_CreatorId_EntityType_EntityId_ReactionName", - table: "CmsUserReactions", - columns: new[] { "CreatorId", "EntityType", "EntityId", "ReactionName" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "CmsUserReactions"); - } - } -} diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200719204212_Added_Reactions.Designer.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200729125121_Initial.Designer.cs similarity index 91% rename from modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200719204212_Added_Reactions.Designer.cs rename to modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200729125121_Initial.Designer.cs index 71e8e68f91..9d5d5e7c9d 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200719204212_Added_Reactions.Designer.cs +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200729125121_Initial.Designer.cs @@ -11,8 +11,8 @@ using Volo.CmsKit.EntityFrameworkCore; namespace Volo.CmsKit.Migrations { [DbContext(typeof(UnifiedDbContext))] - [Migration("20200719204212_Added_Reactions")] - partial class Added_Reactions + [Migration("20200729125121_Initial")] + partial class Initial { protected override void BuildTargetModel(ModelBuilder modelBuilder) { @@ -914,6 +914,47 @@ namespace Volo.CmsKit.Migrations b.ToTable("AbpTenantConnectionStrings"); }); + modelBuilder.Entity("Volo.CmsKit.Comments.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnName("CreationTime") + .HasColumnType("datetime2"); + + b.Property("CreatorId") + .HasColumnName("CreatorId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("RepliedCommentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Text") + .IsRequired() + .HasColumnType("nvarchar(512)") + .HasMaxLength(512); + + b.HasKey("Id"); + + b.HasIndex("RepliedCommentId"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("CmsComments"); + }); + modelBuilder.Entity("Volo.CmsKit.Reactions.UserReaction", b => { b.Property("Id") @@ -952,6 +993,70 @@ namespace Volo.CmsKit.Migrations b.ToTable("CmsUserReactions"); }); + modelBuilder.Entity("Volo.CmsKit.Users.CmsUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnName("ConcurrencyStamp") + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("Email") + .IsRequired() + .HasColumnName("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnName("EmailConfirmed") + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ExtraProperties") + .HasColumnName("ExtraProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnName("Name") + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("PhoneNumber") + .HasColumnName("PhoneNumber") + .HasColumnType("nvarchar(16)") + .HasMaxLength(16); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnName("PhoneNumberConfirmed") + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Surname") + .HasColumnName("Surname") + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("TenantId") + .HasColumnName("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.ToTable("CmsUsers"); + }); + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => { b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200624023340_Initial.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200729125121_Initial.cs similarity index 88% rename from modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200624023340_Initial.cs rename to modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200729125121_Initial.cs index d749beb7b8..6f8b2a069c 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200624023340_Initial.cs +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/20200729125121_Initial.cs @@ -195,6 +195,60 @@ namespace Volo.CmsKit.Migrations table.PrimaryKey("PK_AbpUsers", x => x.Id); }); + migrationBuilder.CreateTable( + name: "CmsComments", + columns: table => new + { + Id = table.Column(nullable: false), + EntityType = table.Column(maxLength: 64, nullable: false), + EntityId = table.Column(maxLength: 64, nullable: false), + Text = table.Column(maxLength: 512, nullable: false), + RepliedCommentId = table.Column(nullable: true), + CreatorId = table.Column(nullable: false), + CreationTime = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CmsComments", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CmsUserReactions", + columns: table => new + { + Id = table.Column(nullable: false), + EntityType = table.Column(maxLength: 64, nullable: false), + EntityId = table.Column(maxLength: 64, nullable: false), + ReactionName = table.Column(maxLength: 32, nullable: false), + CreatorId = table.Column(nullable: false), + CreationTime = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CmsUserReactions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CmsUsers", + columns: table => new + { + Id = table.Column(nullable: false), + ExtraProperties = table.Column(nullable: true), + ConcurrencyStamp = table.Column(maxLength: 40, nullable: true), + TenantId = table.Column(nullable: true), + UserName = table.Column(maxLength: 256, nullable: false), + Email = table.Column(maxLength: 256, nullable: false), + Name = table.Column(maxLength: 64, nullable: true), + Surname = table.Column(maxLength: 64, nullable: true), + EmailConfirmed = table.Column(nullable: false, defaultValue: false), + PhoneNumber = table.Column(maxLength: 16, nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false, defaultValue: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CmsUsers", x => x.Id); + }); + migrationBuilder.CreateTable( name: "AbpAuditLogActions", columns: table => new @@ -564,6 +618,26 @@ namespace Volo.CmsKit.Migrations name: "IX_AbpUsers_UserName", table: "AbpUsers", column: "UserName"); + + migrationBuilder.CreateIndex( + name: "IX_CmsComments_RepliedCommentId", + table: "CmsComments", + column: "RepliedCommentId"); + + migrationBuilder.CreateIndex( + name: "IX_CmsComments_EntityType_EntityId", + table: "CmsComments", + columns: new[] { "EntityType", "EntityId" }); + + migrationBuilder.CreateIndex( + name: "IX_CmsUserReactions_EntityType_EntityId", + table: "CmsUserReactions", + columns: new[] { "EntityType", "EntityId" }); + + migrationBuilder.CreateIndex( + name: "IX_CmsUserReactions_CreatorId_EntityType_EntityId_ReactionName", + table: "CmsUserReactions", + columns: new[] { "CreatorId", "EntityType", "EntityId", "ReactionName" }); } protected override void Down(MigrationBuilder migrationBuilder) @@ -607,6 +681,15 @@ namespace Volo.CmsKit.Migrations migrationBuilder.DropTable( name: "AbpUserTokens"); + migrationBuilder.DropTable( + name: "CmsComments"); + + migrationBuilder.DropTable( + name: "CmsUserReactions"); + + migrationBuilder.DropTable( + name: "CmsUsers"); + migrationBuilder.DropTable( name: "AbpEntityChanges"); diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs index 35d92b2cc6..be32e92f05 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Migrations/UnifiedDbContextModelSnapshot.cs @@ -912,6 +912,47 @@ namespace Volo.CmsKit.Migrations b.ToTable("AbpTenantConnectionStrings"); }); + modelBuilder.Entity("Volo.CmsKit.Comments.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnName("CreationTime") + .HasColumnType("datetime2"); + + b.Property("CreatorId") + .HasColumnName("CreatorId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityId") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("EntityType") + .IsRequired() + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("RepliedCommentId") + .HasColumnType("uniqueidentifier"); + + b.Property("Text") + .IsRequired() + .HasColumnType("nvarchar(512)") + .HasMaxLength(512); + + b.HasKey("Id"); + + b.HasIndex("RepliedCommentId"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("CmsComments"); + }); + modelBuilder.Entity("Volo.CmsKit.Reactions.UserReaction", b => { b.Property("Id") @@ -950,6 +991,70 @@ namespace Volo.CmsKit.Migrations b.ToTable("CmsUserReactions"); }); + modelBuilder.Entity("Volo.CmsKit.Users.CmsUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnName("ConcurrencyStamp") + .HasColumnType("nvarchar(40)") + .HasMaxLength(40); + + b.Property("Email") + .IsRequired() + .HasColumnName("Email") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .ValueGeneratedOnAdd() + .HasColumnName("EmailConfirmed") + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("ExtraProperties") + .HasColumnName("ExtraProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnName("Name") + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("PhoneNumber") + .HasColumnName("PhoneNumber") + .HasColumnType("nvarchar(16)") + .HasMaxLength(16); + + b.Property("PhoneNumberConfirmed") + .ValueGeneratedOnAdd() + .HasColumnName("PhoneNumberConfirmed") + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("Surname") + .HasColumnName("Surname") + .HasColumnType("nvarchar(64)") + .HasMaxLength(64); + + b.Property("TenantId") + .HasColumnName("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserName") + .IsRequired() + .HasColumnName("UserName") + .HasColumnType("nvarchar(256)") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.ToTable("CmsUsers"); + }); + modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLogAction", b => { b.HasOne("Volo.Abp.AuditLogging.AuditLog", null) diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml index 62ef15d4b7..841b505f78 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Index.cshtml @@ -2,13 +2,12 @@ @using Localization.Resources.AbpUi @using Microsoft.Extensions.Localization @using Volo.CmsKit.Pages +@using Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting @using Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.ReactionSelection @model IndexModel @inject IStringLocalizer Localizer

    CMS Kit DEMO

    -

    Reactions

    - @@ -22,6 +21,8 @@ @await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new { entityType = "quote", entityId = "1" }) +
    + @await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "quote", entityId = "1" })
    @@ -38,5 +39,7 @@ @await Component.InvokeAsync(typeof(ReactionSelectionViewComponent), new { entityType = "quote", entityId = "2" }) +
    + @await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "quote", entityId = "2" })
    diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Public.cshtml b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Public.cshtml new file mode 100644 index 0000000000..006fe83c31 --- /dev/null +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Public.cshtml @@ -0,0 +1,36 @@ +@page +@using Localization.Resources.AbpUi +@using Microsoft.Extensions.Localization +@using Volo.CmsKit.Pages +@using Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting +@model PublicModel +@inject IStringLocalizer Localizer +

    CMS Kit DEMO - Public

    + + + + +

    + Any fool can write code that a computer can understand. Good programmers write code that humans can understand. +

    +
    + Martin Fowler +
    +
    +
    + @await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "publicQuote", entityId = "1", loginUrl="Account/Login?ReturnUrl=%2Fpublic" }) +
    + + + + +

    + Writing code is very simple, but writing simple code is the hardest thing there is! +

    +
    + Halil ibrahim Kalkan (inspired from Johan Cruyff) +
    +
    +
    + @await Component.InvokeAsync(typeof(CommentingViewComponent), new { entityType = "publicQuote", entityId = "2", loginUrl="Account/Login?ReturnUrl=%2Fpublic" }) +
    diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Public.cshtml.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Public.cshtml.cs new file mode 100644 index 0000000000..3ec2b2020a --- /dev/null +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Pages/Public.cshtml.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Volo.CmsKit.Pages +{ + public class PublicModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Volo.CmsKit.Web.Unified.csproj b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Volo.CmsKit.Web.Unified.csproj index d0237cd54c..d500cbce2d 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Volo.CmsKit.Web.Unified.csproj +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Unified/Volo.CmsKit.Web.Unified.csproj @@ -13,7 +13,7 @@ - + @@ -47,6 +47,16 @@ + + true + PreserveNewest + + + public.cshtml + + + Public.cshtml + diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Volo.CmsKit.Admin.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Volo.CmsKit.Admin.Web.csproj index 6034ef0eed..f10f38c18c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Volo.CmsKit.Admin.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Volo.CmsKit.Admin.Web.csproj @@ -19,7 +19,7 @@ - + diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj index 7a849b69b3..b1a67ff399 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj @@ -19,7 +19,7 @@ - + diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo.CmsKit.Domain.Shared.csproj b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo.CmsKit.Domain.Shared.csproj index f489caf6e4..03bc3bce5d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo.CmsKit.Domain.Shared.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo.CmsKit.Domain.Shared.csproj @@ -11,7 +11,7 @@ - + diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs new file mode 100644 index 0000000000..9fed054cae --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CommentConsts.cs @@ -0,0 +1,11 @@ +namespace Volo.CmsKit.Comments +{ + public static class CommentConsts + { + public static int EntityTypeLength { get; set; } = 64; + + public static int EntityIdLength { get; set; } = 64; + + public static int MaxTextLength { get; set; } = 512; + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json index e1bdb1b209..3e16a17a47 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json +++ b/modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json @@ -1,6 +1,18 @@ { "culture": "en", "texts": { - "PickYourReaction": "Pick your reaction" + "PickYourReaction": "Pick your reaction", + "YourComment": "Your comment", + "YourReply": "Your reply", + "Comments": "Comments", + "Send": "Send", + "Delete": "Delete", + "Reply": "Reply", + "Update": "Update", + "Edit": "Edit", + "LoginToAddComment": "Login to add comment", + "LoginToReply": "Login to reply", + "MessageDeletionConfirmationMessage": "This comment will be deleted completely.", + "CommentAuthorizationExceptionMessage": "Those comments are not allowed for public display." } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.csproj b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.csproj index f016bed623..ee064cbedc 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo.CmsKit.Domain.csproj @@ -11,6 +11,7 @@ + diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs index e67bca57df..7659c40126 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitDomainModule.cs @@ -1,11 +1,13 @@ using Volo.Abp.Domain; using Volo.Abp.Modularity; +using Volo.Abp.Users; using Volo.CmsKit.Reactions; namespace Volo.CmsKit { [DependsOn( typeof(CmsKitDomainSharedModule), + typeof(AbpUsersDomainModule), typeof(AbpDddDomainModule) )] public class CmsKitDomainModule : AbpModule diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitOptions.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitOptions.cs index cc69f715ce..8e13acca5c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitOptions.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/CmsKitOptions.cs @@ -1,4 +1,5 @@ -using JetBrains.Annotations; +using System.Collections.Generic; +using JetBrains.Annotations; using Volo.CmsKit.Reactions; namespace Volo.CmsKit @@ -8,9 +9,13 @@ namespace Volo.CmsKit [NotNull] public ReactionDefinitionDictionary Reactions { get; } + [NotNull] + public List PublicCommentEntities { get; } + public CmsKitOptions() { Reactions = new ReactionDefinitionDictionary(); + PublicCommentEntities = new List(); } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs new file mode 100644 index 0000000000..fdcdfe37c2 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/Comment.cs @@ -0,0 +1,50 @@ +using System; +using JetBrains.Annotations; +using Volo.Abp; +using Volo.Abp.Auditing; +using Volo.Abp.Domain.Entities; + +namespace Volo.CmsKit.Comments +{ + public class Comment: Entity, IAggregateRoot, IHasCreationTime, IMustHaveCreator + { + public virtual string EntityType { get; protected set; } + + public virtual string EntityId { get; protected set; } + + public virtual string Text { get; protected set; } + + public virtual Guid? RepliedCommentId { get; protected set; } + + public virtual Guid CreatorId { get; set; } + + public virtual DateTime CreationTime { get; set; } + + protected Comment() + { + + } + + public Comment( + Guid id, + [NotNull] string entityType, + [NotNull] string entityId, + [NotNull] string text, + Guid? repliedCommentId, + Guid creatorId) + : base(id) + { + EntityType = Check.NotNullOrWhiteSpace(entityType, nameof(entityType), CommentConsts.EntityTypeLength); + EntityId = Check.NotNullOrWhiteSpace(entityId, nameof(entityId), CommentConsts.EntityIdLength); + RepliedCommentId = repliedCommentId; + CreatorId = creatorId; + + SetText(text); + } + + public virtual void SetText(string text) + { + Text = Check.NotNullOrWhiteSpace(text, nameof(text), CommentConsts.MaxTextLength); + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/CommentWithAuthor.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/CommentWithAuthor.cs new file mode 100644 index 0000000000..3a04a31ee8 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/CommentWithAuthor.cs @@ -0,0 +1,11 @@ +using Volo.CmsKit.Users; + +namespace Volo.CmsKit.Comments +{ + public class CommentWithAuthor + { + public Comment Comment { get; set; } + + public CmsUser Author { get; set; } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs new file mode 100644 index 0000000000..8e5bba3427 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Comments/ICommentRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Domain.Repositories; + +namespace Volo.CmsKit.Comments +{ + public interface ICommentRepository : IBasicRepository + { + Task> GetListWithAuthorsAsync( + [NotNull] string entityType, + [NotNull] string entityId); + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUser.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUser.cs new file mode 100644 index 0000000000..7e66aae6a1 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUser.cs @@ -0,0 +1,82 @@ +using System; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Users; + +namespace Volo.CmsKit.Users +{ + public class CmsUser : AggregateRoot, IUser, IUpdateUserData + { + public virtual Guid? TenantId { get; protected set; } + + public virtual string UserName { get; protected set; } + + public virtual string Email { get; protected set; } + + public virtual string Name { get; set; } + + public virtual string Surname { get; set; } + + public virtual bool EmailConfirmed { get; protected set; } + + public virtual string PhoneNumber { get; protected set; } + + public virtual bool PhoneNumberConfirmed { get; protected set; } + + protected CmsUser() + { + + } + + public CmsUser(IUserData user) + : base(user.Id) + { + TenantId = user.TenantId; + UpdateInternal(user); + } + + public virtual bool Update(IUserData user) + { + if (Id != user.Id) + { + throw new ArgumentException($"Given User's Id '{user.Id}' does not match to this User's Id '{Id}'"); + } + + if (TenantId != user.TenantId) + { + throw new ArgumentException($"Given User's TenantId '{user.TenantId}' does not match to this User's TenantId '{TenantId}'"); + } + + if (Equals(user)) + { + return false; + } + + UpdateInternal(user); + return true; + } + + protected virtual bool Equals(IUserData user) + { + return Id == user.Id && + TenantId == user.TenantId && + UserName == user.UserName && + Name == user.Name && + Surname == user.Surname && + Email == user.Email && + EmailConfirmed == user.EmailConfirmed && + PhoneNumber == user.PhoneNumber && + PhoneNumberConfirmed == user.PhoneNumberConfirmed; + } + + protected virtual void UpdateInternal(IUserData user) + { + Email = user.Email; + Name = user.Name; + Surname = user.Surname; + EmailConfirmed = user.EmailConfirmed; + PhoneNumber = user.PhoneNumber; + PhoneNumberConfirmed = user.PhoneNumberConfirmed; + UserName = user.UserName; + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUserLookupService.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUserLookupService.cs new file mode 100644 index 0000000000..0771140a10 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUserLookupService.cs @@ -0,0 +1,24 @@ +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace Volo.CmsKit.Users +{ + public class CmsUserLookupService: UserLookupService, ICmsUserLookupService + { + public CmsUserLookupService( + ICmsUserRepository userRepository, + IUnitOfWorkManager unitOfWorkManager) + : base( + userRepository, + unitOfWorkManager) + { + + } + + protected override CmsUser CreateUser(IUserData externalUser) + { + return new CmsUser(externalUser); + } + + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUserSynchronizer.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUserSynchronizer.cs new file mode 100644 index 0000000000..6fc785ad4a --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/CmsUserSynchronizer.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities.Events.Distributed; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Users; + +namespace Volo.CmsKit.Users +{ + public class CmsUserSynchronizer : + IDistributedEventHandler>, + ITransientDependency + { + protected ICmsUserRepository UserRepository { get; } + + protected ICmsUserLookupService UserLookupService { get; } + + public CmsUserSynchronizer( + ICmsUserRepository userRepository, + ICmsUserLookupService userLookupService) + { + UserRepository = userRepository; + UserLookupService = userLookupService; + } + + public virtual async Task HandleEventAsync(EntityUpdatedEto eventData) + { + var user = await UserRepository.FindAsync(eventData.Entity.Id); + if (user == null) + { + user = await UserLookupService.FindByIdAsync(eventData.Entity.Id); + if (user == null) + { + return; + } + } + + if (user.Update(eventData.Entity)) + { + await UserRepository.UpdateAsync(user); + } + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/ICmsUserLookupService.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/ICmsUserLookupService.cs new file mode 100644 index 0000000000..8731a187fc --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/ICmsUserLookupService.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Users; + +namespace Volo.CmsKit.Users +{ + public interface ICmsUserLookupService : IUserLookupService + { + + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/ICmsUserRepository.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/ICmsUserRepository.cs new file mode 100644 index 0000000000..95971911af --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Users/ICmsUserRepository.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Users; + +namespace Volo.CmsKit.Users +{ + public interface ICmsUserRepository: IUserRepository + { + + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo.CmsKit.EntityFrameworkCore.csproj b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo.CmsKit.EntityFrameworkCore.csproj index de174d0afd..ace2e010bd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo.CmsKit.EntityFrameworkCore.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo.CmsKit.EntityFrameworkCore.csproj @@ -10,7 +10,8 @@ - + + diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs new file mode 100644 index 0000000000..3c39386f03 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Comments/EfCoreCommentRepository.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Volo.Abp; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; +using Volo.CmsKit.EntityFrameworkCore; + +namespace Volo.CmsKit.Comments +{ + public class EfCoreCommentRepository : EfCoreRepository, + ICommentRepository + { + public EfCoreCommentRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + + public async Task> GetListWithAuthorsAsync( + string entityType, + string entityId) + { + Check.NotNullOrWhiteSpace(entityType, nameof(entityType)); + Check.NotNullOrWhiteSpace(entityId, nameof(entityId)); + + var query = from comment in DbSet + join user in DbContext.CmsUsers on comment.CreatorId equals user.Id + where entityType == comment.EntityType && entityId == comment.EntityId + orderby comment.CreationTime + select new CommentWithAuthor + { + Comment = comment, + Author = user + }; + + return await query.ToListAsync(); + } + + public override async Task DeleteAsync(Guid id, bool autoSave = false, CancellationToken cancellationToken = default) + { + var replies = await DbSet + .Where(x => x.RepliedCommentId == id) + .ToListAsync(GetCancellationToken(cancellationToken)); + + foreach (var reply in replies) + { + //TODO: Discuss if it is better to mark it as deleted and show in the ui as "This is deleted" instead of deleting it and replies completely + await base.DeleteAsync(reply.Id, autoSave, GetCancellationToken(cancellationToken)); + } + + await base.DeleteAsync(id, autoSave, GetCancellationToken(cancellationToken)); + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContext.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContext.cs index e90e133d7f..8033522f70 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContext.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContext.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; +using Volo.CmsKit.Comments; using Volo.CmsKit.Reactions; +using Volo.CmsKit.Users; namespace Volo.CmsKit.EntityFrameworkCore { @@ -10,6 +12,10 @@ namespace Volo.CmsKit.EntityFrameworkCore { public DbSet UserReactions { get; set; } + public DbSet Comments { get; set; } + + public DbSet CmsUsers { get; set; } + public CmsKitDbContext(DbContextOptions options) : base(options) { diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs index 9eeb191f5b..cb7a7b78d6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitDbContextModelCreatingExtensions.cs @@ -2,7 +2,10 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp; using Volo.Abp.EntityFrameworkCore.Modeling; +using Volo.CmsKit.Comments; using Volo.CmsKit.Reactions; +using Volo.CmsKit.Users; +using Volo.Abp.Users.EntityFrameworkCore; namespace Volo.CmsKit.EntityFrameworkCore { @@ -21,6 +24,14 @@ namespace Volo.CmsKit.EntityFrameworkCore optionsAction?.Invoke(options); + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "Users", options.Schema); + + b.ConfigureByConvention(); + b.ConfigureAbpUser(); + }); + builder.Entity(b => { b.ToTable(options.TablePrefix + "UserReactions", options.Schema); @@ -34,6 +45,21 @@ namespace Volo.CmsKit.EntityFrameworkCore b.HasIndex(x => new { x.EntityType, x.EntityId }); b.HasIndex(x => new { x.CreatorId, x.EntityType, x.EntityId, x.ReactionName }); }); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "Comments", options.Schema); + b.ConfigureByConvention(); + + b.Property(x => x.EntityType).IsRequired().HasMaxLength(CommentConsts.EntityTypeLength); + b.Property(x => x.EntityId).IsRequired().HasMaxLength(CommentConsts.EntityIdLength); + b.Property(x => x.Text).IsRequired().HasMaxLength(CommentConsts.MaxTextLength); + b.Property(x => x.RepliedCommentId); + b.Property(x => x.CreationTime); + + b.HasIndex(x => new { x.EntityType, x.EntityId }); + b.HasIndex(x => new { x.RepliedCommentId }); + }); } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs index afad9b56e8..028bc466b2 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/CmsKitEntityFrameworkCoreModule.cs @@ -1,11 +1,16 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Modularity; +using Volo.Abp.Users.EntityFrameworkCore; +using Volo.CmsKit.Comments; +using Volo.CmsKit.Reactions; +using Volo.CmsKit.Users; namespace Volo.CmsKit.EntityFrameworkCore { [DependsOn( typeof(CmsKitDomainModule), + typeof(AbpUsersEntityFrameworkCoreModule), typeof(AbpEntityFrameworkCoreModule) )] public class CmsKitEntityFrameworkCoreModule : AbpModule @@ -14,10 +19,10 @@ namespace Volo.CmsKit.EntityFrameworkCore { context.Services.AddAbpDbContext(options => { - /* Add custom repositories here. Example: - * options.AddRepository(); - */ + options.AddRepository(); + options.AddRepository(); + options.AddRepository(); }); } } -} \ No newline at end of file +} diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/ICmsKitDbContext.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/ICmsKitDbContext.cs index 9441986b23..e63366d5ae 100644 --- a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/ICmsKitDbContext.cs +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/EntityFrameworkCore/ICmsKitDbContext.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; +using Volo.CmsKit.Comments; using Volo.CmsKit.Reactions; +using Volo.CmsKit.Users; namespace Volo.CmsKit.EntityFrameworkCore { @@ -9,5 +11,9 @@ namespace Volo.CmsKit.EntityFrameworkCore public interface ICmsKitDbContext : IEfCoreDbContext { DbSet UserReactions { get; } + + DbSet Comments { get; } + + DbSet CmsUsers { get; set; } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Users/EfCoreCmsUserRepository.cs b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Users/EfCoreCmsUserRepository.cs new file mode 100644 index 0000000000..0b6e804be0 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.EntityFrameworkCore/Volo/CmsKit/Users/EfCoreCmsUserRepository.cs @@ -0,0 +1,14 @@ +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Users.EntityFrameworkCore; +using Volo.CmsKit.EntityFrameworkCore; + +namespace Volo.CmsKit.Users +{ + public class EfCoreCmsUserRepository: EfCoreUserRepositoryBase, ICmsUserRepository + { + public EfCoreCmsUserRepository(IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo.CmsKit.MongoDB.csproj b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo.CmsKit.MongoDB.csproj index 079112e5b2..e723724a87 100644 --- a/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo.CmsKit.MongoDB.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.MongoDB/Volo.CmsKit.MongoDB.csproj @@ -4,7 +4,7 @@ - netcoreapp3.1 + netstandard2.0 diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CmsUserDto.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CmsUserDto.cs new file mode 100644 index 0000000000..3446517efd --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CmsUserDto.cs @@ -0,0 +1,17 @@ +using System; + +namespace Volo.CmsKit.Comments +{ + public class CmsUserDto + { + public Guid Id { get; set; } + + public string UserName { get; set; } + + public string Email { get; set; } + + public string Name { get; set; } + + public string Surname { get; set; } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CommentDto.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CommentDto.cs new file mode 100644 index 0000000000..1dd2ac4056 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CommentDto.cs @@ -0,0 +1,23 @@ +using System; + +namespace Volo.CmsKit.Comments +{ + public class CommentDto + { + public Guid Id { get; set; } + + public string EntityType { get; set; } + + public string EntityId { get; set; } + + public string Text { get; set; } + + public Guid? RepliedCommentId { get; set; } + + public Guid CreatorId { get; set; } + + public DateTime CreationTime { get; set; } + + public CmsUserDto Author { get; set; } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CommentWithDetailsDto.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CommentWithDetailsDto.cs new file mode 100644 index 0000000000..5938316b84 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CommentWithDetailsDto.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Volo.CmsKit.Comments +{ + public class CommentWithDetailsDto + { + public Guid Id { get; set; } + + public string EntityType { get; set; } + + public string EntityId { get; set; } + + public string Text { get; set; } + + public Guid CreatorId { get; set; } + + public DateTime CreationTime { get; set; } + + public List Replies { get; set; } + + public CmsUserDto Author { get; set; } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CreateCommentInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CreateCommentInput.cs new file mode 100644 index 0000000000..30b86b75f0 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/CreateCommentInput.cs @@ -0,0 +1,23 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace Volo.CmsKit.Comments +{ + public class CreateCommentInput + { + [Required] + [DynamicStringLength(typeof(CommentConsts), nameof(CommentConsts.EntityTypeLength))] + public string EntityType { get; set; } + + [Required] + [DynamicStringLength(typeof(CommentConsts), nameof(CommentConsts.EntityIdLength))] + public string EntityId { get; set; } + + [Required] + [DynamicStringLength(typeof(CommentConsts), nameof(CommentConsts.MaxTextLength))] + public string Text { get; set; } + + public Guid? RepliedCommentId { get; set; } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/ICommentPublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/ICommentPublicAppService.cs new file mode 100644 index 0000000000..385a7ea92a --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/ICommentPublicAppService.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Volo.CmsKit.Comments +{ + public interface ICommentPublicAppService : IApplicationService + { + Task> GetAllForEntityAsync(string entityType, string entityId); + + Task CreateAsync(CreateCommentInput input); + + Task UpdateAsync(Guid id, UpdateCommentInput input); + + Task DeleteAsync(Guid id); + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/UpdateCommentInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/UpdateCommentInput.cs new file mode 100644 index 0000000000..e084914626 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Comments/UpdateCommentInput.cs @@ -0,0 +1,13 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Validation; + +namespace Volo.CmsKit.Comments +{ + public class UpdateCommentInput + { + [Required] + [DynamicStringLength(typeof(CommentConsts), nameof(CommentConsts.MaxTextLength))] + public string Text { get; set; } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Comments/CommentPublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Comments/CommentPublicAppService.cs new file mode 100644 index 0000000000..d3c4651e16 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Comments/CommentPublicAppService.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Authorization; +using Volo.Abp.Users; +using Volo.CmsKit.Users; + +namespace Volo.CmsKit.Comments +{ + public class CommentPublicAppService : ApplicationService, ICommentPublicAppService + { + protected CmsKitOptions CmsKitOptions { get; } + protected ICommentRepository CommentRepository { get; } + protected ICmsUserLookupService CmsUserLookupService { get; } + + public CommentPublicAppService( + ICommentRepository commentRepository, + ICmsUserLookupService cmsUserLookupService, + IOptions cmsKitOptions) + { + CmsKitOptions = cmsKitOptions.Value; + CommentRepository = commentRepository; + CmsUserLookupService = cmsUserLookupService; + } + + public async Task> GetAllForEntityAsync(string entityType, string entityId) + { + CheckAuthorizationAsync(entityType); + + var commentsWithAuthor = await CommentRepository.GetListWithAuthorsAsync(entityType, entityId); + + return new ListResultDto( + ConvertCommentsToNestedStructure(commentsWithAuthor) + ); + } + + [Authorize] + public async Task CreateAsync(CreateCommentInput input) + { + var user = await CmsUserLookupService.FindByIdAsync(CurrentUser.GetId()); + + if (user == null) + { + throw new BusinessException(message: "User Not found!"); + } + + var comment = await CommentRepository.InsertAsync(new Comment( + GuidGenerator.Create(), + input.EntityType, + input.EntityId, + input.Text, + input.RepliedCommentId, + user.Id + )); + + return ObjectMapper.Map(comment); + } + + [Authorize] + public async Task UpdateAsync(Guid id, UpdateCommentInput input) + { + var comment = await CommentRepository.GetAsync(id); + + if (comment.CreatorId != CurrentUser.GetId()) + { + throw new BusinessException(); + } + + comment.SetText(input.Text); + + var updatedComment = await CommentRepository.UpdateAsync(comment); + + return ObjectMapper.Map(updatedComment); + } + + [Authorize] + public async Task DeleteAsync(Guid id) + { + var comment = await CommentRepository.GetAsync(id); + + if (comment.CreatorId != CurrentUser.GetId()) + { + throw new BusinessException(); + } + + await CommentRepository.DeleteAsync(id); + } + + private List ConvertCommentsToNestedStructure(List comments) + { + var parentComments = comments + .Where(c=> c.Comment.RepliedCommentId == null) + .Select(c=> ObjectMapper.Map(c.Comment)) + .ToList(); + + foreach (var parentComment in parentComments) + { + parentComment.Author = GetAuthorAsDtoFromCommentList(comments, parentComment.Id); + + parentComment.Replies = comments + .Where(c => c.Comment.RepliedCommentId == parentComment.Id) + .Select(c => ObjectMapper.Map(c.Comment)) + .ToList(); + + foreach (var reply in parentComment.Replies) + { + reply.Author = GetAuthorAsDtoFromCommentList(comments, reply.Id); + } + } + + return parentComments; + } + + private async Task CheckAuthorizationAsync(string entityType) + { + if (await IsPublicEntity(entityType)) + { + return; + } + + if (!CurrentUser.IsAuthenticated) + { + throw new AbpAuthorizationException(L["CommentAuthorizationExceptionMessage"]); + } + } + + private async Task IsPublicEntity(string entityType) + { + return CmsKitOptions.PublicCommentEntities.Contains(entityType); + } + + private CmsUserDto GetAuthorAsDtoFromCommentList(List comments, Guid commentId) + { + return ObjectMapper.Map(comments.Single(c => c.Comment.Id == commentId).Author); + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/PublicApplicationAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/PublicApplicationAutoMapperProfile.cs index e88dd5c4b3..d26814c0c8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/PublicApplicationAutoMapperProfile.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/PublicApplicationAutoMapperProfile.cs @@ -1,4 +1,7 @@ using AutoMapper; +using Volo.Abp.AutoMapper; +using Volo.CmsKit.Comments; +using Volo.CmsKit.Users; namespace Volo.CmsKit { @@ -9,6 +12,10 @@ namespace Volo.CmsKit /* You can configure your AutoMapper mapping configuration here. * Alternatively, you can split your mapping configurations * into multiple profile classes for a better organization. */ + + CreateMap(); + CreateMap().Ignore(x=> x.Author); + CreateMap().Ignore(x=> x.Replies).Ignore(x=> x.Author); } } -} \ No newline at end of file +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Comments/CommentPublicController.cs b/modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Comments/CommentPublicController.cs new file mode 100644 index 0000000000..de8e4e0c83 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.HttpApi/Volo/CmsKit/Comments/CommentPublicController.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp; +using Volo.Abp.Application.Dtos; + +namespace Volo.CmsKit.Comments +{ + [RemoteService(Name = CmsKitPublicRemoteServiceConsts.RemoteServiceName)] + [Area("cms-kit")] + [Route("api/cms-kit-public/comments")] + public class CommentPublicController : CmsKitPublicControllerBase, ICommentPublicAppService + { + public ICommentPublicAppService CommentPublicAppService { get; } + + public CommentPublicController(ICommentPublicAppService commentPublicAppService) + { + CommentPublicAppService = commentPublicAppService; + } + + [HttpGet] + [Route("{entityType}/{entityId}")] + public Task> GetAllForEntityAsync(string entityType, string entityId) + { + return CommentPublicAppService.GetAllForEntityAsync(entityType, entityId); + } + + [HttpPost] + public Task CreateAsync(CreateCommentInput input) + { + return CommentPublicAppService.CreateAsync(input); + } + + [HttpPost] + [Route("{id}")] + public Task UpdateAsync(Guid id, UpdateCommentInput input) + { + return CommentPublicAppService.UpdateAsync(id, input); + } + + [HttpDelete] + [Route("update")] + public Task DeleteAsync(Guid id) + { + return CommentPublicAppService.DeleteAsync(id); + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs index 3534811ad3..5988c33f41 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting; using Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.ReactionSelection; namespace Volo.CmsKit.Web.Controllers @@ -11,5 +12,10 @@ namespace Volo.CmsKit.Web.Controllers { return ViewComponent(typeof(ReactionSelectionViewComponent), new {entityType, entityId}); } + + public async Task Commenting(string entityType, string entityId) + { + return ViewComponent(typeof(CommentingViewComponent), new {entityType, entityId}); + } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingScriptBundleContributor.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingScriptBundleContributor.cs new file mode 100644 index 0000000000..8f1b6ad38e --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingScriptBundleContributor.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using Volo.Abp.AspNetCore.Mvc.UI.Packages.Bootstrap; +using Volo.Abp.Modularity; + +namespace Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting +{ + public class CommentingScriptBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/Pages/CmsKit/Shared/Components/Commenting/default.js"); + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingStyleBundleContributor.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingStyleBundleContributor.cs new file mode 100644 index 0000000000..158c8e15d2 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingStyleBundleContributor.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Volo.Abp.AspNetCore.Mvc.UI.Bundling; + +namespace Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting +{ + public class CommentingStyleBundleContributor : BundleContributor + { + public override void ConfigureBundle(BundleConfigurationContext context) + { + context.Files.AddIfNotContains("/Pages/CmsKit/Shared/Components/Commenting/default.css"); + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs new file mode 100644 index 0000000000..50f07ab9b1 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.UI.Widgets; +using Volo.CmsKit.Comments; + +namespace Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting +{ + [ViewComponent(Name = "CmsCommenting")] + [Widget( + ScriptTypes = new[] {typeof(CommentingScriptBundleContributor)}, + StyleTypes = new[] {typeof(CommentingStyleBundleContributor)}, + RefreshUrl = "/CmsKitPublicWidgets/Commenting" + )] + public class CommentingViewComponent : AbpViewComponent + { + public ICommentPublicAppService CommentPublicAppService { get; } + + public CommentingViewComponent( + ICommentPublicAppService commentPublicAppService) + { + CommentPublicAppService = commentPublicAppService; + } + + public virtual async Task InvokeAsync(string entityType, string entityId, string loginUrl = null) + { + var result = await CommentPublicAppService.GetAllForEntityAsync(entityType, entityId); + + var viewModel = new CommentingViewModel + { + EntityId = entityId, + EntityType = entityType, + LoginUrl = loginUrl, + Comments = result.Items.ToList() + }; + + return View("~/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml", viewModel); + } + + public class CommentingViewModel + { + public string EntityType { get; set; } + + public string EntityId { get; set; } + + public string LoginUrl { get; set; } + + public List Comments { get; set; } + } + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml new file mode 100644 index 0000000000..faf3537430 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -0,0 +1,145 @@ +@using Microsoft.AspNetCore.Html +@using Microsoft.AspNetCore.Mvc.Localization +@using Volo.Abp.Users +@using Volo.CmsKit.Comments +@using Volo.CmsKit.Localization +@inject ICurrentUser CurrentUser +@inject IHtmlLocalizer L +@model Volo.CmsKit.Web.Pages.CmsKit.Shared.Components.Commenting.CommentingViewComponent.CommentingViewModel + +@{ + Func GetCommentTitle(CmsUserDto author, DateTime creationTime) => + @ + @((string.IsNullOrWhiteSpace(author.Name) + ? author.UserName + : author.Name + " " + author.Surname).Trim()) + @creationTime.ToString() + ; +} +@{ + Func GetCommentArea(Guid? repliedCommentId, bool cancelButton = false) => + @
    +
    + +
    + + +
    + @if (cancelButton) + { + @L["Cancel"] + } + @L["Send"] +
    +
    ; +} +@{ + Func GetCommentContentArea(Guid id, Guid authorId, bool isReply, string text) => + @
    +
    +

    + @text +

    +
    + + @if (!isReply) + { + @if (CurrentUser.IsAuthenticated) + { + + @L["Reply"] + + } + else + { + @L["LoginToReply"] + } + } + @if (authorId == CurrentUser.Id) + { + @if (!isReply) + { + | + } + + + @L["Delete"] + + + | + + + @L["Edit"] + + } + +
    ; +} + +
    + +
    +

    + + @L["Comments"] +

    + @foreach (var comment in Model.Comments) + { +
    +
    +
    + @GetCommentTitle(comment.Author, comment.CreationTime).Invoke(null) +
    + + @GetCommentContentArea(comment.Id, comment.Author.Id, false, comment.Text).Invoke(null) + + @if (CurrentUser.IsAuthenticated) + { + @GetCommentArea(comment.Id, true).Invoke(null) + } + + @if (comment.Replies.Any()) + { +
    + @foreach (var reply in comment.Replies) + { +
    +
    + +
    + @GetCommentTitle(reply.Author, reply.CreationTime).Invoke(null) +
    + + @GetCommentContentArea(reply.Id, reply.Author.Id, true, reply.Text).Invoke(null) +
    +
    + } +
    + } +
    +
    + } +
    + @if (CurrentUser.IsAuthenticated) + { + @GetCommentArea(null).Invoke(null) + } + else if (!string.IsNullOrWhiteSpace(Model.LoginUrl)) + { + + } +
    +
    +
    diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.css b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.css new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.css @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js new file mode 100644 index 0000000000..29def70797 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js @@ -0,0 +1,162 @@ +(function ($) { + + var l = abp.localization.getResource('CmsKit'); + + $(document).ready(function () { + + abp.widgets.CmsCommenting = function ($widget) { + var widgetManager = $widget.data('abp-widget-manager'); + var $commentArea = $widget.find('.cms-comment-area'); + + function getFilters() { + return { + entityType: $commentArea.attr('data-entity-type'), + entityId: $commentArea.attr('data-entity-id') + }; + } + + function registerEditLinks($container) { + $container.find('.comment-edit-link').each(function () { + var $link = $(this); + $link.on('click', function (e) { + e.preventDefault(); + + var commentId = $link.data('id'); + + var $relatedCommentContentArea = $container.find('.cms-comment-content-area[data-id='+ commentId +']'); + var $relatedCommentEditFormArea = $container.find('.cms-comment-edit-area[data-id='+ commentId +']'); + + $relatedCommentContentArea.hide(); + $relatedCommentEditFormArea.show(); + $link.removeAttr('href'); + }); + }); + $container.find('.comment-edit-cancel-button').each(function () { + var $button = $(this); + $button.on('click', function (e) { + e.preventDefault(); + + var commentId = $button.data('id'); + + var $relatedCommentContentArea = $container.find('.cms-comment-content-area[data-id='+ commentId +']'); + var $relatedCommentEditFormArea = $container.find('.cms-comment-edit-area[data-id='+ commentId +']'); + var $link = $container.find('.comment-edit-link[data-id='+ commentId +']'); + + $relatedCommentContentArea.show(); + $relatedCommentEditFormArea.hide(); + $link.attr('href','#'); + }); + }); + } + + function registerReplyLinks($container) { + $container.find('.comment-reply-link').each(function () { + var $link = $(this); + $link.on('click', function (e) { + e.preventDefault(); + + var replyCommentId = $link.data('reply-id'); + + var $relatedCommentArea = $container.find('.cms-comment-form-area[data-reply-id='+ replyCommentId +']'); + + $relatedCommentArea.show(); + $link.removeAttr('href'); + }); + }); + $container.find('.reply-cancel-button').each(function () { + var $button = $(this); + $button.on('click', function (e) { + e.preventDefault(); + + var replyCommentId = $button.data('reply-id'); + + var $relatedCommentArea = $container.find('.cms-comment-form-area[data-reply-id='+ replyCommentId +']'); + var $replyLink = $container.find('.comment-reply-link[data-reply-id='+ replyCommentId +']'); + + $relatedCommentArea.hide(); + $replyLink.attr('href','#'); + }); + }); + } + + function registerDeleteLinks($container) { + $container.find('.comment-delete-link').each(function () { + var $link = $(this); + $link.on('click', '', function (e) { + e.preventDefault(); + + abp.message.confirm(l("MessageDeletionConfirmationMessage"), function (ok) { + if (ok){ + volo.cmsKit.comments.commentPublic.delete($link.data('id') + ).then(function () { + widgetManager.refresh($widget); + }); + } + }); + }); + }); + } + + function registerUpdateOfNewComment($container) { + $container.find('.cms-comment-update-form').each(function () { + var $form = $(this); + $form.submit(function (e) { + e.preventDefault(); + var formAsObject = $form.serializeFormToObject(); + volo.cmsKit.comments.commentPublic.update( + formAsObject.id, + { + text: formAsObject.commentText + } + ).then(function () { + widgetManager.refresh($widget); + }); + }); + }); + } + + function registerSubmissionOfNewComment($container) { + $container.find('.cms-comment-form').each(function () { + var $form = $(this); + $form.submit(function (e) { + e.preventDefault(); + var formAsObject = $form.serializeFormToObject(); + console.log(formAsObject); + volo.cmsKit.comments.commentPublic.create( + $.extend(getFilters(), { + repliedCommentId: formAsObject.repliedCommentId, + text: formAsObject.commentText + }) + ).then(function () { + widgetManager.refresh($widget); + }); + }); + }); + } + + function init() { + registerReplyLinks($widget); + registerEditLinks($widget); + registerDeleteLinks($widget); + + registerUpdateOfNewComment($widget); + registerSubmissionOfNewComment($widget); + } + + return { + init: init, + getFilters: getFilters + }; + }; + + $('.abp-widget-wrapper[data-widget-name="CmsCommenting"]') + .each(function () { + var widgetManager = new abp.WidgetManager({ + wrapper: $(this), + }); + + widgetManager.init(); + }); + }); + +})(jQuery); diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj index c3a45b7838..342e1b4a1d 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj @@ -15,7 +15,7 @@ - + diff --git a/modules/cms-kit/src/Volo.CmsKit.Web/Volo.CmsKit.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Web/Volo.CmsKit.Web.csproj index 84a31c94db..74d3bbe84f 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Web/Volo.CmsKit.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Web/Volo.CmsKit.Web.csproj @@ -19,7 +19,7 @@ - + diff --git a/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/Volo.CmsKit.EntityFrameworkCore.Tests.csproj b/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/Volo.CmsKit.EntityFrameworkCore.Tests.csproj index fbcdaa6c17..ab7406a97d 100644 --- a/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/Volo.CmsKit.EntityFrameworkCore.Tests.csproj +++ b/modules/cms-kit/test/Volo.CmsKit.EntityFrameworkCore.Tests/Volo.CmsKit.EntityFrameworkCore.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/modules/cms-kit/test/Volo.CmsKit.HttpApi.Client.ConsoleTestApp/Volo.CmsKit.HttpApi.Client.ConsoleTestApp.csproj b/modules/cms-kit/test/Volo.CmsKit.HttpApi.Client.ConsoleTestApp/Volo.CmsKit.HttpApi.Client.ConsoleTestApp.csproj index 0168088fbc..65b381f4f4 100644 --- a/modules/cms-kit/test/Volo.CmsKit.HttpApi.Client.ConsoleTestApp/Volo.CmsKit.HttpApi.Client.ConsoleTestApp.csproj +++ b/modules/cms-kit/test/Volo.CmsKit.HttpApi.Client.ConsoleTestApp/Volo.CmsKit.HttpApi.Client.ConsoleTestApp.csproj @@ -20,7 +20,7 @@ - + diff --git a/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj b/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj index 4789667e2c..b8d9bcd623 100644 --- a/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj +++ b/modules/docs/app/VoloDocs.EntityFrameworkCore/VoloDocs.EntityFrameworkCore.csproj @@ -16,8 +16,8 @@
    - - + + diff --git a/modules/docs/app/VoloDocs.Web/package.json b/modules/docs/app/VoloDocs.Web/package.json index 2117f0b2f2..c6b5ba0dba 100644 --- a/modules/docs/app/VoloDocs.Web/package.json +++ b/modules/docs/app/VoloDocs.Web/package.json @@ -3,7 +3,7 @@ "name": "volo.docstestapp", "private": true, "dependencies": { - "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.4", - "@abp/docs": "^3.0.4" + "@abp/aspnetcore.mvc.ui.theme.basic": "^3.0.5", + "@abp/docs": "^3.0.5" } } diff --git a/modules/docs/app/VoloDocs.Web/yarn.lock b/modules/docs/app/VoloDocs.Web/yarn.lock index 8c4d6fe3d6..4c93967952 100644 --- a/modules/docs/app/VoloDocs.Web/yarn.lock +++ b/modules/docs/app/VoloDocs.Web/yarn.lock @@ -2,45 +2,45 @@ # yarn lockfile v1 -"@abp/anchor-js@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-3.0.4.tgz#a8a0702c9204ba953e899890338512545af63b4d" - integrity sha512-J8/QIbUK8Cy+7EABw1gUC2CWuJVF44SjdJOIvJSno6k5xbAkyx4oOvPH6x7ngT68rlopt0zez14OpeM6Oy/0Bg== +"@abp/anchor-js@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/anchor-js/-/anchor-js-3.0.5.tgz#e1affea2e21370562b244f81ce8911d3d36f8d55" + integrity sha512-068PVUu/Zu1zQNgWsJXhUmSIm/HRA4Mx6VXxlNL+qiCLffg8AzRuj4rZc+UaYJieDmJDJc0dlWgiyUFpuvzZ8A== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" anchor-js "^4.2.2" -"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.4.tgz#c58e3e06ba78279875cf2b5f96d34b83ce09f6df" - integrity sha512-tKKqVRKpwN7P4Mn+he3bcz4Qbt8bkZ++wk6iOujd6HLTkBNa0uKyM4L6+iCn1Hi62S6HeK2Xytx+b+IYeIlsuw== - dependencies: - "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.4" - -"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.4.tgz#50db612b6492d36c63dd9042000a9fb00fecec54" - integrity sha512-imd2T5XXkNvGQyXV/A9hA80qSeurKaUcW6HDkte8Y2fs4QfvkjgzWXi7neqF6F+HVbYYPg3rIBWC6QF1OXGn2w== - dependencies: - "@abp/aspnetcore.mvc.ui" "~3.0.4" - "@abp/bootstrap" "~3.0.4" - "@abp/bootstrap-datepicker" "~3.0.4" - "@abp/datatables.net-bs4" "~3.0.4" - "@abp/font-awesome" "~3.0.4" - "@abp/jquery-form" "~3.0.4" - "@abp/jquery-validation-unobtrusive" "~3.0.4" - "@abp/lodash" "~3.0.4" - "@abp/luxon" "~3.0.4" - "@abp/malihu-custom-scrollbar-plugin" "~3.0.4" - "@abp/select2" "~3.0.4" - "@abp/sweetalert" "~3.0.4" - "@abp/timeago" "~3.0.4" - "@abp/toastr" "~3.0.4" - -"@abp/aspnetcore.mvc.ui@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.4.tgz#4e4344d95a98a60c53037b351cb915d8ff7a7dcb" - integrity sha512-p7KI5CKkpD4EYzG4fCtygrtZhwMu7Z+IZFGbdqXelLbI5ACkbJyrn+N+9trPfS9papymeXY4p1E3v6xOH7rSHQ== +"@abp/aspnetcore.mvc.ui.theme.basic@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.basic/-/aspnetcore.mvc.ui.theme.basic-3.0.5.tgz#c146f20226522f0305cc0fa31cfcf9d39be78901" + integrity sha512-ptNhD4xV2wSLW7vQ5KUgaRiH5Ov1OscZW2ZEoivnt3kqgtIC3DGw+SiE9vjFCvyY83fjRkIlW2PIfQxSySbteQ== + dependencies: + "@abp/aspnetcore.mvc.ui.theme.shared" "~3.0.5" + +"@abp/aspnetcore.mvc.ui.theme.shared@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui.theme.shared/-/aspnetcore.mvc.ui.theme.shared-3.0.5.tgz#5b370902cf229963f9aa10dd64482d57299dde76" + integrity sha512-iobAH39xrCdnqdaABaiYZg4lqNSOwPkMCLnyc+ytxOMbu/YnNKahXPmRhgd8dR/l+iQY1+6T4SWfLdGswi3gAg== + dependencies: + "@abp/aspnetcore.mvc.ui" "~3.0.5" + "@abp/bootstrap" "~3.0.5" + "@abp/bootstrap-datepicker" "~3.0.5" + "@abp/datatables.net-bs4" "~3.0.5" + "@abp/font-awesome" "~3.0.5" + "@abp/jquery-form" "~3.0.5" + "@abp/jquery-validation-unobtrusive" "~3.0.5" + "@abp/lodash" "~3.0.5" + "@abp/luxon" "~3.0.5" + "@abp/malihu-custom-scrollbar-plugin" "~3.0.5" + "@abp/select2" "~3.0.5" + "@abp/sweetalert" "~3.0.5" + "@abp/timeago" "~3.0.5" + "@abp/toastr" "~3.0.5" + +"@abp/aspnetcore.mvc.ui@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/aspnetcore.mvc.ui/-/aspnetcore.mvc.ui-3.0.5.tgz#a050eaee328c56a3730f5abbf25035adf612d6f0" + integrity sha512-/EOgjXTzR+I1OK9KA7L3yzQ2RGSDW97+n3cQrSdLAUF+tnRvMJS0nytDlSJSRK2hb8/clFMaofZHCC81aUkYUg== dependencies: ansi-colors "^4.1.1" extend-object "^1.0.0" @@ -49,180 +49,180 @@ path "^0.12.7" rimraf "^3.0.2" -"@abp/bootstrap-datepicker@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.4.tgz#d21963f158a4d348a0acdc46209e67378aae8e55" - integrity sha512-25e9qm39dj9AijIchQIdLlBiBM5/fhASkEuLKNt6M8S/e3qwKLwbRhX90DKMFvOyKdxxcK1gX7tDuGHyG9A8mA== +"@abp/bootstrap-datepicker@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap-datepicker/-/bootstrap-datepicker-3.0.5.tgz#3a7fd86dbcadc31c11be5b2b31c8cf8ac3035540" + integrity sha512-vIY1wT4RHoJAPhVK11Q5MS71iZbwsmM/0VTBvvs04mjRSSi6xhdyRYkA+l+P49fnOwhZNaaK19XwQdVVScDuhA== dependencies: bootstrap-datepicker "^1.9.0" -"@abp/bootstrap@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.4.tgz#3b0e2b74dc174a04ce669de1b140df833eee6031" - integrity sha512-DwxL1fGYOwChPwmMpwUxCKiIdzTfRf/YDkOLUiwI8eRrpABBeuE2sSHKfks6Rqh9Wqo/nlWXvfccgDRqweqhsA== +"@abp/bootstrap@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/bootstrap/-/bootstrap-3.0.5.tgz#91f2c8fe6a57fc5789ae8fe9c206b2c159e55880" + integrity sha512-LdydeJIbw5k1k7FkWaI1lTectZeTXmEIoccJtj9Qx4N9yGt40wGsjz9SOvUdJ0IKAgntIWNQP8ohWiBPgzT7Xg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" bootstrap "^4.5.0" bootstrap-v4-rtl "4.4.1-2" -"@abp/clipboard@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-3.0.4.tgz#17ff40e398de3c0ab5214e564d443ed52ac10630" - integrity sha512-vjjupRau7fgBxvQwaDrBHApP4Ce1x9XM7L64/c+A+oBny0SzuiSBGBYsjlFrJGmTHT/PhjoB1XQiBHrYYPXQfQ== +"@abp/clipboard@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/clipboard/-/clipboard-3.0.5.tgz#7ceadd21360db11c91582a532029bfe65d10a930" + integrity sha512-iq455C096L5D+b6IrWkMxA9mbpggBNZ1kWfCQ3rhIjo0qbOGrvdGB3uQ+BqRS9Ni9ku7vBb2COldouIC9KOp5A== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" clipboard "^2.0.6" -"@abp/core@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.4.tgz#281940525e280e9e3a1aa4cb4e8f64548ea026a3" - integrity sha512-ZPEH1fa5bf/f1ek9ZLQHx7u78iF6kRZ8Z2NBI36cNL13RyQbx485qJF+KXyqNlJoJAh0CAs0NsAJADU3utcDNA== +"@abp/core@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/core/-/core-3.0.5.tgz#9a7bd990d02edc8128a5be3233b6fbb5669b15b8" + integrity sha512-LdkSkZQfN4hugcGNxBkje0+eQomivwNFbnAkMFQbvCwYLwKB2yvk7GCemEetW1piqvyrUGz4jqhtunjoXps5JA== dependencies: - "@abp/utils" "^3.0.4" + "@abp/utils" "^3.0.5" -"@abp/datatables.net-bs4@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.4.tgz#e488336bd2c199146ea60464e0ba75aeb134957e" - integrity sha512-ID1XWKPxG9pv6mxBkku7JWvgXjTxJ1uShxmYv5kFYVuwOGMSvj8QVZInTHFcMdECopV56yjGPNOIFLeQj4Mw8Q== +"@abp/datatables.net-bs4@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net-bs4/-/datatables.net-bs4-3.0.5.tgz#9b79935970d2d4074994240b5deb01dff5987c0a" + integrity sha512-jIWRBEG/NlhaE43i6Xzr18XJ5aTWA+WWslf7WgNOArG5slGBbH4lLLja4HswLP3NGdSmzIyHJR1p9XQ82nBkbQ== dependencies: - "@abp/datatables.net" "~3.0.4" + "@abp/datatables.net" "~3.0.5" datatables.net-bs4 "^1.10.21" -"@abp/datatables.net@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.4.tgz#7ef143b83143ad8d769be25296411a812a17b255" - integrity sha512-gdQwRuxO5fQWhG/DiUaf40DQ8dWIVGhOCyt6SZ8LQzq4uSaQqa0qrtTTTW2/aiee4MgJu2vJWYF5YCszIR/HhA== +"@abp/datatables.net@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/datatables.net/-/datatables.net-3.0.5.tgz#0a0a39e8fa54522bf8a09ce76ff3927207a79491" + integrity sha512-HClQ2tcZuWBAChf9oOJOtMP1zHv8jqloPEcw3iqbhS/tWZBw5VQ4TO+BVfZsqHDXZNnWSmR6/2xiKsK32nliew== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" datatables.net "^1.10.21" -"@abp/docs@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-3.0.4.tgz#366d550247189981d5989652d3d2cf663ae518e5" - integrity sha512-NdqukX/oPkkmG31xZflc8oeVm0Up/CUZCBb9ahKCHleK7C0os8eQBQ52wO27dJFa5FaeUhW2ejLYT3aS2XkrTg== +"@abp/docs@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/docs/-/docs-3.0.5.tgz#733211ea72f3064ea5c23ed36a16c7f8b0ef5d8b" + integrity sha512-LQDHz6mE3ptP1AmpK+IEoY6edQB/t7Hu5ueLdtNHqTJWkwW7ngXOaECbkC2KVPywLqbXw0vzHBw2OnCVSKxtyw== dependencies: - "@abp/anchor-js" "~3.0.4" - "@abp/clipboard" "~3.0.4" - "@abp/malihu-custom-scrollbar-plugin" "~3.0.4" - "@abp/popper.js" "~3.0.4" - "@abp/prismjs" "~3.0.4" + "@abp/anchor-js" "~3.0.5" + "@abp/clipboard" "~3.0.5" + "@abp/malihu-custom-scrollbar-plugin" "~3.0.5" + "@abp/popper.js" "~3.0.5" + "@abp/prismjs" "~3.0.5" -"@abp/font-awesome@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.4.tgz#98f83d0bf8b5a2a4d988cb07f5cceda2e1bc1a09" - integrity sha512-AKhKCUXx8vdHIgyXUfbSLitfYor5dJbo4qMRRGqIJVSj+cyMkpgYbGjqr24GGijgZxdmOGL+s1b9iCUbIPgLNQ== +"@abp/font-awesome@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/font-awesome/-/font-awesome-3.0.5.tgz#62d58c34f0abf268e30bdf788dfbd3f45b19b412" + integrity sha512-/+31Vtw1S9vvuNy/rN/Q6awkgUaYNNYXESgfvqI0pqIkvlPgigvS8E68mHqGzsxyRqdZisI6JGd94AF+vJD7sQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" "@fortawesome/fontawesome-free" "^5.13.0" -"@abp/jquery-form@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.4.tgz#022eebbfe58b55e328984c0f119884c9464bc767" - integrity sha512-xDkk4rY0AmXBJE4qztzr2hZz+OVucVNWeagOEgeQHXJSKU/T+0kj0a1RxEHpyI24xBvEXAbe8PbByTbu0QXYew== +"@abp/jquery-form@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-form/-/jquery-form-3.0.5.tgz#9d2ea9b2509448ee163a163298a7a09480865f78" + integrity sha512-RKgvQciaMml33NvzA28aYyfNI+WQHRfKLFWvOvqc8qO4C3BvyrnwepuzZcnI/9UsELKLSLoFrfk3rJHhxnTJhg== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-form "^4.3.0" -"@abp/jquery-validation-unobtrusive@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.4.tgz#6cc942aadcdde33893e7a45c84aa022e033e4e8b" - integrity sha512-N9sza91HCyPvJo+d0RAfq4skae3TgAYg7KXhlwzRE2hqUQS+EICVARURWyxJW7s7Zu6+gxDTkXhBZRTW56deZA== +"@abp/jquery-validation-unobtrusive@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation-unobtrusive/-/jquery-validation-unobtrusive-3.0.5.tgz#b8912e1c4cfdd4fdff6ad4aa7ee8693b55a3be1f" + integrity sha512-kPifDA+1ErbeQBwn+BzwtWuTHXzBke/EMCmITuj59plM/60gZ1ZfVm8EzzTmKS7iLaVVeOqTKf0vEQTobNYffw== dependencies: - "@abp/jquery-validation" "~3.0.4" + "@abp/jquery-validation" "~3.0.5" jquery-validation-unobtrusive "^3.2.11" -"@abp/jquery-validation@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.4.tgz#3f96d1f83c75a7fc47e5e5e12c51dbc8da7582b1" - integrity sha512-2ljqas4YKU0PcwB0wQW2qhXejDHjW+N1XCxZDyCZ2wF9UxGlKBR8rFt76TJByXSotKE7n2hJRZaOh9ICZUow2g== +"@abp/jquery-validation@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery-validation/-/jquery-validation-3.0.5.tgz#55e36b3c832a35447fac69af312dedf1dee22279" + integrity sha512-fdqxc5CJ6TpUAF8+NEsKCmXvvZiOPR5bzczcfvfovxLOjvHvdEO74XPGjsjlBNTdaKMhmNGZRK4JRLUz7fvTng== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" jquery-validation "^1.19.2" -"@abp/jquery@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.4.tgz#5f236d95cfae7f65012cd8778fd74490b7542442" - integrity sha512-g8N8Te5vf+8djdksfZoZ3tXaqlBC4Ra39pAWU/XerlLl64u+XmAX1s6yKRfC2kS8lf292GgiFYuNOkb77J6k4A== +"@abp/jquery@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/jquery/-/jquery-3.0.5.tgz#2e43ac19a8df6e1e0220d6d4991d33a93d890754" + integrity sha512-2d2l+smWKAVP4/b6GSO+8rmJ7pFXiUewpBDwkHm5qgkK6n6UGqIcK9lIgiS2YQ6rmL6qdtuNhhqtC9GF2TF2kQ== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" jquery "~3.5.1" -"@abp/lodash@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.4.tgz#ee60bc99f98b59fd9fc5ff4bbcf914a13faf209d" - integrity sha512-jxvv74Sm/fUKDBMylK4Ne/8GKm7GuHAdcLFtkoJQptdO+RjZpKJ86ULT+X5vbKskZaTj2fEmOR6l4hmfnBULTA== +"@abp/lodash@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/lodash/-/lodash-3.0.5.tgz#d5d9f14095d76239bc0e6f9804c24d1f92ffcab4" + integrity sha512-+B2vxfPwxoIIb8Ac8eHi6OgAn0qbjmPZuun7/es0/ouYYTbEhvhTAD20X3PGZlHrIYeDyS2ansuXm8gJI+/BzA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" lodash "^4.17.15" -"@abp/luxon@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.4.tgz#aa1bf6a93d6254433101e50a205b276759969414" - integrity sha512-gX9dQJt9C6M1wmCSUIBoNr8CAt0jkUZFQ7LKKO/BoADGM/XFvY5vDbhL7zcwbwitgCOzmucoCxs/6rIxSOVlrg== +"@abp/luxon@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/luxon/-/luxon-3.0.5.tgz#083042c939ea58bf719e35114a6c8efb07b68688" + integrity sha512-0UVHO2CO89t34O35OA3JzWGWLBpg+e1NQXSfVtK90nmg/QmzGkHVM9m7Su0jjLNBt3N5EA5OPOhriot3bCKItw== dependencies: luxon "^1.24.1" -"@abp/malihu-custom-scrollbar-plugin@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.4.tgz#c54824b98363f4190cc6a37fcd421c2bfdc1e201" - integrity sha512-6LdSGctOr4ACtdohdSlb19vrFNJ5C0hy1VdbBEafHOBspXckvRVx4uJl2KfHmAC+bMg9apoK/wcHjVNdBHThYQ== +"@abp/malihu-custom-scrollbar-plugin@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/malihu-custom-scrollbar-plugin/-/malihu-custom-scrollbar-plugin-3.0.5.tgz#f908289927a63fd76fb8f60b00a37747d9edbe39" + integrity sha512-3GoXA8VtGyZWKiqaPCipPXYLLM7h2Y5Oecq5c5YlMQyW2akv7uHiQQF2GZW/nVsFu3HBbGTOWpKRRmVk3PEO9Q== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" malihu-custom-scrollbar-plugin "^3.1.5" -"@abp/popper.js@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-3.0.4.tgz#f68f2c3392dd9f86ce0f53cc12b33b0258bbbf9f" - integrity sha512-DTxqTMyLFLGjpf7sbKLJMsPbuZ/+8OdgqlO/JjNjyBVWCCM6b50yhYAcYKQq1f2kcqIegJv0B6+zDHAFMtnibg== +"@abp/popper.js@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/popper.js/-/popper.js-3.0.5.tgz#ef46770e3a63cf10c91d4bda983198cd84b8e063" + integrity sha512-5MzLBMRWh2hHLVJJIrvJa6c4fwEdUk9HmnqpaqXnSX52B4uy9jcBexMdi/H7+cmao9NHm+Ld9n9G0YA8vMNeKA== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" popper.js "^1.16.0" -"@abp/prismjs@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-3.0.4.tgz#9cd8113b33dfd1b349b12740a2f7462f88bf3841" - integrity sha512-D+RNAhUhOCXnBNZVOq9EQg883eRbPKlIpFsxeMW065XOm5g7iO0eJACvyqGC8mt5reHmtxApnfiGNlhT1dK+Eg== +"@abp/prismjs@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/prismjs/-/prismjs-3.0.5.tgz#81a6ef656f3d7a76f365eb0bcbf2d0d3013d6e72" + integrity sha512-vdSSa1+tXS5+HWIgQyjDy4yQG7Q1YyuHQnFU6zlDJCQlE+20PL6lYgPnqR7DQ+Cd4lhRb3oxvloImYjwiwSQvA== dependencies: - "@abp/clipboard" "~3.0.4" - "@abp/core" "~3.0.4" + "@abp/clipboard" "~3.0.5" + "@abp/core" "~3.0.5" prismjs "^1.20.0" -"@abp/select2@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.4.tgz#49db0cfbc9b08f6ae4949b2cb8bdef92d7d308dd" - integrity sha512-zKEgkWBFdGU2KiAMUKhucHqBfzDy2KNSefyqxpOiUd1L3PEKu+9o23HZRWzDupYltop+SO1Jw33CNoVAkKm4+A== +"@abp/select2@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/select2/-/select2-3.0.5.tgz#79d7754713c64a6549da520dc6867ef3e3dd0350" + integrity sha512-VbeSHdAdDy8exrjplHMMhLVV4oG7Gq9zBb5M52f0sIkA4DkXFhksXHcHNoOCfhaKAXpenGA03Z9GJewZ3MTpMw== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" select2 "^4.0.13" -"@abp/sweetalert@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.4.tgz#f85d11c5aabc6ceb509af1b8fd8d75bc3c74bd27" - integrity sha512-hO0qXUWxeMy/lfy3sb+5HvOmIPst6UvYJJGy3ZQR6DKEdkOktBX9sEDb7m6ij6xAIHqWIx1xLveul0oGteOG0Q== +"@abp/sweetalert@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/sweetalert/-/sweetalert-3.0.5.tgz#e3b92d23cf3633983980b6255bf63c43d422ceab" + integrity sha512-OUt4ANgj14GESfRG7+UqdONS8daOu+8x2OZDWv98hriuX0uCVBjwB2J0jRI/n653C68Mi0yHixGONDv9OLy/lg== dependencies: - "@abp/core" "~3.0.4" + "@abp/core" "~3.0.5" sweetalert "^2.1.2" -"@abp/timeago@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.4.tgz#82d90a04f703a7db9c0acedcd2f3cea684e7ae18" - integrity sha512-7SbbqScTKCix25xWy5oLT0slJqUQsW6YgJgI0N41C3AsByRBXZeh5Gcep033gprTPmpgNY8JOWcEcCXySnYvfA== +"@abp/timeago@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/timeago/-/timeago-3.0.5.tgz#b191a7c43f3dc77645fe27273f2f6619f7d4eb31" + integrity sha512-QhvAOU4C+Qxh/gFuCSGZtLU4KwVhdv/jtK8ovWyqbGvQMCCIimqa8BFKIz0H5XWIJhHwwWblkpjICWgjWh6uXw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" timeago "^1.6.7" -"@abp/toastr@~3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.4.tgz#884ab2343ddd59981c291c632e3b0630532f7e9c" - integrity sha512-NG0pVD8bdIXGpjPW9cozCzmyuAdnSLE4vDYLUReP1pZFp9B8SMJqpgPk9eFg0Y06aaHTVMKYBKRoq9xXS4w9Xg== +"@abp/toastr@~3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/toastr/-/toastr-3.0.5.tgz#fc83b678f5be3a22040d0421ad9db5b2eac9d4fc" + integrity sha512-qgi2DEu6FYpN/DYcOZrmXsAr1HqcIKMhBWeXgjfoYUIl/K5+OuUgbQ69D/jdc0jz8HU9a4Cw7Q8hhe4CkgssEw== dependencies: - "@abp/jquery" "~3.0.4" + "@abp/jquery" "~3.0.5" toastr "^2.1.4" -"@abp/utils@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.4.tgz#38068fa8f735e1889cf2f226128366825948814d" - integrity sha512-TnXRZIZcxv5+EctEPsZEi2cyRXbpl3A679wzQa6BACv+pM45cjGO1JR8P0VswKN4TVL0aQRMv26N6U/uie8sEQ== +"@abp/utils@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@abp/utils/-/utils-3.0.5.tgz#66b3da953500e73ff1f23d1b41168cde8216ce49" + integrity sha512-X9V1xLIEazItaf93nFSA4p94WT6QK3yA4eIyvU7GyyEkkOQdaakNwPbRwy930sd+I+/oXnIqM1vDsJu9QDhgvg== dependencies: just-compare "^1.3.0" diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj index 72fd333456..d482c4d954 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo.Docs.Admin.Application.Contracts.csproj @@ -22,7 +22,7 @@
    - + - + diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/cs.json b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/cs.json index 081a5f2cd4..f02fa96f50 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/cs.json +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/cs.json @@ -31,6 +31,8 @@ "DisplayName:GitHubRootUrl": "GitHub kořenové URL", "DisplayName:GitHubAccessToken": "GitHub přístupový token", "DisplayName:GitHubUserAgent": "GitHub user agent", + "DisplayName:GithubVersionProviderSource": "GitHub version provider source", + "DisplayName:VersionBranchPrefix": "Version branch prefix", "DisplayName:All": "Pull všeho", "DisplayName:LanguageCode": "Kód jazyka", "DisplayName:Version": "Verze", diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/en.json b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/en.json index b73f43317c..8b940df86d 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/en.json +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Localization/Resources/Docs/ApplicationContracts/en.json @@ -31,6 +31,8 @@ "DisplayName:GitHubRootUrl": "GitHub root URL", "DisplayName:GitHubAccessToken": "GitHub access token", "DisplayName:GitHubUserAgent": "GitHub user agent", + "DisplayName:GithubVersionProviderSource": "GitHub version provider source", + "DisplayName:VersionBranchPrefix": "Version branch prefix", "DisplayName:All": "Pull all", "DisplayName:LanguageCode": "Language code", "DisplayName:Version": "Version", @@ -55,4 +57,4 @@ "FileName": "File name", "LastCachedTime": "Cache time" } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/CreateProjectDto.cs b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/CreateProjectDto.cs index eb603b334c..0918e19d8e 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/CreateProjectDto.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/CreateProjectDto.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Volo.Docs.Projects; namespace Volo.Docs.Admin.Projects { @@ -26,4 +27,4 @@ namespace Volo.Docs.Admin.Projects public Dictionary ExtraProperties { get; set; } } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/ProjectDto.cs b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/ProjectDto.cs index c71f34e357..b624a1f187 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/ProjectDto.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/ProjectDto.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Volo.Abp.Application.Dtos; +using Volo.Docs.Projects; namespace Volo.Docs.Admin.Projects { @@ -29,4 +30,4 @@ namespace Volo.Docs.Admin.Projects public Dictionary ExtraProperties { get; set; } } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/UpdateProjectDto.cs b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/UpdateProjectDto.cs index 9e94cec66e..f2d3c218c8 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/UpdateProjectDto.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application.Contracts/Volo/Docs/Admin/Projects/UpdateProjectDto.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Volo.Docs.Projects; namespace Volo.Docs.Admin.Projects { @@ -22,4 +23,4 @@ namespace Volo.Docs.Admin.Projects public Dictionary ExtraProperties { get; set; } } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs index ca430cdaad..83f993d1e8 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs +++ b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs @@ -15,7 +15,11 @@ namespace Volo.Docs.Admin CreateMap().Ignore(x => x.ExtraProperties); CreateMap () - .Ignore(x => x.GitHubAccessToken).Ignore(x => x.GitHubRootUrl).Ignore(x => x.GitHubUserAgent); + .Ignore(x => x.GitHubAccessToken) + .Ignore(x => x.GitHubRootUrl) + .Ignore(x => x.GitHubUserAgent) + .Ignore(x => x.GithubVersionProviderSource) + .Ignore(x => x.VersionBranchPrefix); CreateMap(); CreateMap(); diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs index 506b674ba5..3f5b847538 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Create.cshtml.cs @@ -62,7 +62,9 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects { {nameof(GithubProject.GitHubRootUrl), GithubProject.GitHubRootUrl}, {nameof(GithubProject.GitHubUserAgent), GithubProject.GitHubUserAgent}, - {nameof(GithubProject.GitHubAccessToken), GithubProject.GitHubAccessToken} + {nameof(GithubProject.GitHubAccessToken), GithubProject.GitHubAccessToken}, + {nameof(GithubProject.GithubVersionProviderSource), GithubProject.GithubVersionProviderSource}, + {nameof(GithubProject.VersionBranchPrefix), GithubProject.VersionBranchPrefix} }; return dto; @@ -118,6 +120,13 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects [DisplayOrder(10002)] [StringLength(64)] public string GitHubUserAgent { get; set; } + + [DisplayOrder(10003)] + public GithubVersionProviderSource GithubVersionProviderSource { get; set; } = GithubVersionProviderSource.Releases; + + [DisplayOrder(10004)] + [StringLength(64)] + public string VersionBranchPrefix { get; set; } } } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs index ef1762c460..ee27a2dc15 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs @@ -62,7 +62,9 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects { {nameof(GithubProject.GitHubRootUrl), GithubProject.GitHubRootUrl}, {nameof(GithubProject.GitHubUserAgent), GithubProject.GitHubUserAgent}, - {nameof(GithubProject.GitHubAccessToken), GithubProject.GitHubAccessToken} + {nameof(GithubProject.GitHubAccessToken), GithubProject.GitHubAccessToken}, + {nameof(GithubProject.GithubVersionProviderSource), GithubProject.GithubVersionProviderSource}, + {nameof(GithubProject.VersionBranchPrefix), GithubProject.VersionBranchPrefix} }; return dto; @@ -75,6 +77,16 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects GithubProject.GitHubAccessToken = (string) dto.ExtraProperties[nameof(GithubProject.GitHubAccessToken)]; GithubProject.GitHubRootUrl = (string) dto.ExtraProperties[nameof(GithubProject.GitHubRootUrl)]; GithubProject.GitHubUserAgent = (string) dto.ExtraProperties[nameof(GithubProject.GitHubUserAgent)]; + + if (dto.ExtraProperties.ContainsKey(nameof(GithubProject.GithubVersionProviderSource))) + { + GithubProject.GithubVersionProviderSource = (GithubVersionProviderSource) (long) dto.ExtraProperties[nameof(GithubProject.GithubVersionProviderSource)]; + } + + if (dto.ExtraProperties.ContainsKey(nameof(GithubProject.VersionBranchPrefix))) + { + GithubProject.VersionBranchPrefix = (string) dto.ExtraProperties[nameof(GithubProject.VersionBranchPrefix)]; + } } public abstract class EditProjectViewModelBase @@ -125,6 +137,13 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects [DisplayOrder(10002)] [StringLength(64)] public string GitHubUserAgent { get; set; } + + [DisplayOrder(10003)] + public GithubVersionProviderSource GithubVersionProviderSource { get; set; } = GithubVersionProviderSource.Releases; + + [DisplayOrder(10004)] + [StringLength(64)] + public string VersionBranchPrefix { get; set; } } } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj b/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj index 48c28054ef..8e6805cf0a 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj +++ b/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj @@ -17,7 +17,7 @@ - + @@ -27,16 +27,16 @@ - + - + - + diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs index 6cacf9d3e3..6df4c6179e 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs +++ b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Documents/DocumentAppService.cs @@ -64,6 +64,8 @@ namespace Volo.Docs.Documents { var project = await _projectRepository.GetAsync(input.ProjectId); + input.Version = GetProjectVersionPrefixIfExist(project) + input.Version; + return await GetDocumentWithDetailsDtoAsync( project, input.Name, @@ -76,6 +78,8 @@ namespace Volo.Docs.Documents { var project = await _projectRepository.GetAsync(input.ProjectId); + input.Version = GetProjectVersionPrefixIfExist(project) + input.Version; + return await GetDocumentWithDetailsDtoAsync( project, project.DefaultDocumentName + "." + project.Format, @@ -88,6 +92,8 @@ namespace Volo.Docs.Documents { var project = await _projectRepository.GetAsync(input.ProjectId); + input.Version = GetProjectVersionPrefixIfExist(project) + input.Version; + var navigationDocument = await GetDocumentWithDetailsDtoAsync( project, project.NavigationDocumentName, @@ -133,10 +139,11 @@ namespace Volo.Docs.Documents public async Task GetResourceAsync(GetDocumentResourceInput input) { var project = await _projectRepository.GetAsync(input.ProjectId); - var cacheKey = - CacheKeyGenerator.GenerateDocumentResourceCacheKey(project, input.Name, input.LanguageCode, - input.Version); + input.Version = string.IsNullOrWhiteSpace(input.Version) ? project.LatestVersionBranchName : input.Version; + input.Version = GetProjectVersionPrefixIfExist(project) + input.Version; + + var cacheKey = CacheKeyGenerator.GenerateDocumentResourceCacheKey(project, input.Name, input.LanguageCode,input.Version); async Task GetResourceAsync() { @@ -166,6 +173,8 @@ namespace Volo.Docs.Documents { var project = await _projectRepository.GetAsync(input.ProjectId); + input.Version = GetProjectVersionPrefixIfExist(project) + input.Version; + var esDocs = await _documentFullSearch.SearchAsync(input.Context, project.Id, input.LanguageCode, input.Version); @@ -188,6 +197,8 @@ namespace Volo.Docs.Documents { var project = await _projectRepository.GetAsync(input.ProjectId); + input.Version = GetProjectVersionPrefixIfExist(project) + input.Version; + try { if (string.IsNullOrWhiteSpace(project.ParametersDocumentName)) @@ -337,5 +348,22 @@ namespace Volo.Docs.Documents return TimeSpan.Parse(value); } + + private string GetProjectVersionPrefixIfExist(Project project) + { + if (GetGithubVersionProviderSource(project) == GithubVersionProviderSource.Branches) + { + return project.ExtraProperties["VersionBranchPrefix"].ToString(); + } + + return ""; + } + + private GithubVersionProviderSource GetGithubVersionProviderSource(Project project) + { + return project.ExtraProperties.ContainsKey("GithubVersionProviderSource") + ? (GithubVersionProviderSource) (long) project.ExtraProperties["GithubVersionProviderSource"] + : GithubVersionProviderSource.Releases; + } } } diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs index 2284804143..728840135e 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs +++ b/modules/docs/src/Volo.Docs.Application/Volo/Docs/Projects/ProjectAppService.cs @@ -106,6 +106,8 @@ namespace Volo.Docs.Projects var project = await _projectRepository.GetByShortNameAsync(shortName); var store = _documentSource.Create(project.DocumentStoreType); + version = GetProjectVersionPrefixIfExist(project) + version; + async Task GetLanguagesAsync() { return await store.GetLanguageListAsync(project, version); @@ -120,5 +122,22 @@ namespace Volo.Docs.Projects } ); } + + private string GetProjectVersionPrefixIfExist(Project project) + { + if (GetGithubVersionProviderSource(project) == GithubVersionProviderSource.Branches) + { + return project.ExtraProperties["VersionBranchPrefix"].ToString(); + } + + return ""; + } + + private GithubVersionProviderSource GetGithubVersionProviderSource(Project project) + { + return project.ExtraProperties.ContainsKey("GithubVersionProviderSource") + ? (GithubVersionProviderSource) (long) project.ExtraProperties["GithubVersionProviderSource"] + : GithubVersionProviderSource.Releases; + } } } diff --git a/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Projects/GithubVersionProviderSource.cs b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Projects/GithubVersionProviderSource.cs new file mode 100644 index 0000000000..4773ecf74d --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain.Shared/Volo/Docs/Projects/GithubVersionProviderSource.cs @@ -0,0 +1,8 @@ +namespace Volo.Docs.Projects +{ + public enum GithubVersionProviderSource + { + Releases, + Branches + } +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj index 1042aeec0e..0f0b393787 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj +++ b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj @@ -17,12 +17,13 @@ - + - + + - + diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs index 05a3f1c4c8..b525aa65d9 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs @@ -73,7 +73,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic FileName = document.FileName, Content = document.Content, LanguageCode = ConvertLanguageCode(document.LanguageCode), - Version = document.Version + Version = ConvertVersion(document.Version) }; if (!existsResponse.Exists) @@ -173,7 +173,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic new TermQuery { Field = "version", - Value = version + Value = ConvertVersion(version) }, new TermQuery { @@ -237,5 +237,10 @@ namespace Volo.Docs.Documents.FullSearch.Elastic { return languageCode.Replace("-", "").ToLower(); } + + protected string ConvertVersion(string version) + { + return version.Replace("-", "").ToLower(); + } } } diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs index 1da1ab1f9d..3a79227934 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs @@ -9,6 +9,7 @@ using Volo.Docs.GitHub.Projects; using Volo.Docs.Projects; using Octokit; using Volo.Abp; +using Volo.Docs.GitHub.Documents.Version; using Volo.Extensions; using Project = Volo.Docs.Projects.Project; @@ -22,11 +23,13 @@ namespace Volo.Docs.GitHub.Documents private readonly IGithubRepositoryManager _githubRepositoryManager; private readonly IGithubPatchAnalyzer _githubPatchAnalyzer; + private readonly IVersionHelper _versionHelper; - public GithubDocumentSource(IGithubRepositoryManager githubRepositoryManager, IGithubPatchAnalyzer githubPatchAnalyzer) + public GithubDocumentSource(IGithubRepositoryManager githubRepositoryManager, IGithubPatchAnalyzer githubPatchAnalyzer, IVersionHelper versionHelper) { _githubRepositoryManager = githubRepositoryManager; _githubPatchAnalyzer = githubPatchAnalyzer; + _versionHelper = versionHelper; } public virtual async Task GetDocumentAsync(Project project, string documentName, string languageCode, string version, DateTime? lastKnownSignificantUpdateTime = null) @@ -172,7 +175,7 @@ namespace Volo.Docs.GitHub.Documents * Getting file commits usually throws "Resource temporarily unavailable" or "Network is unreachable" * This is a trival information and running this inside try-catch is safer. */ - + try { return await GetFileCommitsAsync(project, version, project.GetGitHubInnerUrl(languageCode, documentName)); @@ -221,15 +224,19 @@ namespace Volo.Docs.GitHub.Documents public async Task> GetVersionsAsync(Project project) { + var url = project.GetGitHubUrl(); + var ownerName = GetOwnerNameFromUrl(url); + var repositoryName = GetRepositoryNameFromUrl(url); + var githubVersionProviderSource = GetGithubVersionProviderSource(project); + List versions; try { - versions = (await GetReleasesAsync(project)) - .OrderByDescending(r => r.PublishedAt) + versions = (await _githubRepositoryManager.GetVersionsAsync(ownerName, repositoryName, project.GetGitHubAccessTokenOrNull(), githubVersionProviderSource)) .Select(r => new VersionInfo { - Name = r.TagName, - DisplayName = r.TagName + Name = r.Name, + DisplayName = r.Name }).ToList(); } catch (Exception ex) @@ -239,14 +246,45 @@ namespace Volo.Docs.GitHub.Documents versions = new List(); } - if (!versions.Any() && !string.IsNullOrEmpty(project.LatestVersionBranchName)) + if (githubVersionProviderSource == GithubVersionProviderSource.Branches && project.ExtraProperties.ContainsKey("VersionBranchPrefix")) + { + var prefix = (string) project.ExtraProperties["VersionBranchPrefix"]; + + if (!string.IsNullOrEmpty(prefix)) + { + versions = versions.Where(v => v.Name.StartsWith(prefix)).ToList(); + foreach (var v in versions) + { + v.Name = v.Name.Substring(prefix.Length); + v.DisplayName = v.DisplayName.Substring(prefix.Length); + } + } + + versions = _versionHelper.OrderByDescending(versions); + } + + if(githubVersionProviderSource == GithubVersionProviderSource.Releases) { - versions.Add(new VersionInfo { DisplayName = "1.0.0", Name = project.LatestVersionBranchName }); + if (!versions.Any() && !string.IsNullOrEmpty(project.LatestVersionBranchName)) + { + versions.Add(new VersionInfo { DisplayName = "1.0.0", Name = project.LatestVersionBranchName }); + } + else + { + versions = _versionHelper.OrderByDescending(versions); + } } return versions; } + private GithubVersionProviderSource GetGithubVersionProviderSource(Project project) + { + return project.ExtraProperties.ContainsKey("GithubVersionProviderSource") + ? (GithubVersionProviderSource) (long) project.ExtraProperties["GithubVersionProviderSource"] + : GithubVersionProviderSource.Releases; + } + public async Task GetResource(Project project, string resourceName, string languageCode, string version) { var rawRootUrl = CalculateRawRootUrlWithLanguageCode(project.GetGitHubUrl(version), languageCode); @@ -277,14 +315,6 @@ namespace Volo.Docs.GitHub.Documents return languageConfig; } - private async Task> GetReleasesAsync(Project project) - { - var url = project.GetGitHubUrl(); - var ownerName = GetOwnerNameFromUrl(url); - var repositoryName = GetRepositoryNameFromUrl(url); - return await _githubRepositoryManager.GetReleasesAsync(ownerName, repositoryName, project.GetGitHubAccessTokenOrNull()); - } - private async Task> GetFileCommitsAsync(Project project, string version, string filename) { var url = project.GetGitHubUrl(); diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubRepositoryManager.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubRepositoryManager.cs index ef21353236..8d99e360ef 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubRepositoryManager.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubRepositoryManager.cs @@ -6,6 +6,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Octokit; using Octokit.Internal; +using Volo.Docs.GitHub.Documents.Version; +using Volo.Docs.Projects; using ProductHeaderValue = Octokit.ProductHeaderValue; namespace Volo.Docs.GitHub.Documents @@ -15,10 +17,12 @@ namespace Volo.Docs.GitHub.Documents public const string HttpClientName = "GithubRepositoryManagerHttpClientName"; private readonly IHttpClientFactory _clientFactory; + private readonly IGithubVersionProviderFactory _githubVersionProviderFactory; - public GithubRepositoryManager(IHttpClientFactory clientFactory) + public GithubRepositoryManager(IHttpClientFactory clientFactory, IGithubVersionProviderFactory githubVersionProviderFactory) { _clientFactory = clientFactory; + _githubVersionProviderFactory = githubVersionProviderFactory; } public async Task GetFileRawStringContentAsync(string rawUrl, string token, string userAgent) @@ -32,11 +36,12 @@ namespace Volo.Docs.GitHub.Documents using var httpClient = CreateHttpClient(token, userAgent); return await httpClient.GetByteArrayAsync(new Uri(rawUrl)); } - - public async Task> GetReleasesAsync(string name, string repositoryName, string token) + + public async Task> GetVersionsAsync(string name, string repositoryName, string token, GithubVersionProviderSource githubVersionProviderSource) { - var client = GetGitHubClient(name, token); - return await client.Repository.Release.GetAll(name, repositoryName); + var _provider = _githubVersionProviderFactory.Create(githubVersionProviderSource); + + return await _provider.GetVersions(name, repositoryName, token); } public async Task> GetFileCommitsAsync(string name, string repositoryName, string version, string filename, string token) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/IGithubRepositoryManager.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/IGithubRepositoryManager.cs index 67536fc559..12f3bc4ef2 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/IGithubRepositoryManager.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/IGithubRepositoryManager.cs @@ -4,6 +4,8 @@ using System.Text; using System.Threading.Tasks; using Octokit; using Volo.Abp.DependencyInjection; +using Volo.Docs.GitHub.Documents.Version; +using Volo.Docs.Projects; namespace Volo.Docs.GitHub.Documents { @@ -13,7 +15,7 @@ namespace Volo.Docs.GitHub.Documents Task GetFileRawByteArrayContentAsync(string rawUrl, string token, string userAgent); - Task> GetReleasesAsync(string name, string repositoryName, string token); + Task> GetVersionsAsync(string name, string repositoryName, string token, GithubVersionProviderSource githubVersionProviderSource); Task> GetFileCommitsAsync(string name, string repositoryName, string version, string filename, string token); diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/BranchGithubVersionProvider.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/BranchGithubVersionProvider.cs new file mode 100644 index 0000000000..524eecb33c --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/BranchGithubVersionProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octokit; +using Octokit.Internal; +using Volo.Abp.DependencyInjection; + +namespace Volo.Docs.GitHub.Documents.Version +{ + public class BranchGithubVersionProvider : IGithubVersionProvider, ITransientDependency + { + public async Task> GetVersions(string name, string repositoryName, string token) + { + var client = GetGitHubClient(name, token); + var branches = await client.Repository.Branch.GetAll(name, repositoryName); + + return branches.Select(b => new GithubVersion {Name = b.Name}).ToList(); + } + + private static GitHubClient GetGitHubClient(string name, string token) + { + return token.IsNullOrWhiteSpace() + ? new GitHubClient(new ProductHeaderValue(name)) + : new GitHubClient(new ProductHeaderValue(name), new InMemoryCredentialStore(new Credentials(token))); + } + } +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/GithubVersion.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/GithubVersion.cs new file mode 100644 index 0000000000..5c3e98aac0 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/GithubVersion.cs @@ -0,0 +1,7 @@ +namespace Volo.Docs.GitHub.Documents.Version +{ + public class GithubVersion + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/GithubVersionProviderFactory.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/GithubVersionProviderFactory.cs new file mode 100644 index 0000000000..d3c7ba9a72 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/GithubVersionProviderFactory.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; +using Volo.Docs.Projects; + +namespace Volo.Docs.GitHub.Documents.Version +{ + public class GithubVersionProviderFactory : IGithubVersionProviderFactory, ITransientDependency + { + public IServiceProvider ServiceProvider { get; } + + public GithubVersionProviderFactory(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + public IGithubVersionProvider Create(GithubVersionProviderSource source) + { + Type serviceType; + + switch (source) + { + case GithubVersionProviderSource.Branches: + serviceType = typeof(BranchGithubVersionProvider); + break; + case GithubVersionProviderSource.Releases: + default: + serviceType = typeof(ReleaseGithubVersionProvider); + break; + } + + return (IGithubVersionProvider)ServiceProvider.GetRequiredService(serviceType); + } + } +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IGithubVersionProvider.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IGithubVersionProvider.cs new file mode 100644 index 0000000000..429e2ec34b --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IGithubVersionProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Docs.GitHub.Documents.Version +{ + public interface IGithubVersionProvider + { + Task> GetVersions(string name, string repositoryName, string token); + } +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IGithubVersionProviderFactory.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IGithubVersionProviderFactory.cs new file mode 100644 index 0000000000..5236e52fbd --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IGithubVersionProviderFactory.cs @@ -0,0 +1,9 @@ +using Volo.Docs.Projects; + +namespace Volo.Docs.GitHub.Documents.Version +{ + public interface IGithubVersionProviderFactory + { + IGithubVersionProvider Create(GithubVersionProviderSource source); + } +} diff --git a/modules/docs/src/Volo.Docs.Web/Version/IVersionHelper.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IVersionHelper.cs similarity index 65% rename from modules/docs/src/Volo.Docs.Web/Version/IVersionHelper.cs rename to modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IVersionHelper.cs index f3fd202155..67bed57093 100644 --- a/modules/docs/src/Volo.Docs.Web/Version/IVersionHelper.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/IVersionHelper.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using Volo.Docs.Projects; -namespace Volo.Docs.Version +namespace Volo.Docs.GitHub.Documents.Version { public interface IVersionHelper { List OrderByDescending(List versions); - List OrderByDescending(List versions); + List OrderByDescending(List versions); bool IsPreRelease(string version); } diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/ReleaseGithubVersionProvider.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/ReleaseGithubVersionProvider.cs new file mode 100644 index 0000000000..f4b9fee2b2 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/ReleaseGithubVersionProvider.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Octokit; +using Octokit.Internal; +using Volo.Abp.DependencyInjection; + +namespace Volo.Docs.GitHub.Documents.Version +{ + public class ReleaseGithubVersionProvider : IGithubVersionProvider, ITransientDependency + { + public async Task> GetVersions(string name, string repositoryName, string token) + { + var client = GetGitHubClient(name, token); + var releases = await client.Repository.Release.GetAll(name, repositoryName); + + return releases.Select(r => new GithubVersion {Name = r.TagName}).ToList(); + } + + private static GitHubClient GetGitHubClient(string name, string token) + { + return token.IsNullOrWhiteSpace() + ? new GitHubClient(new ProductHeaderValue(name)) + : new GitHubClient(new ProductHeaderValue(name), new InMemoryCredentialStore(new Credentials(token))); + } + } +} diff --git a/modules/docs/src/Volo.Docs.Web/Version/SemanticVersionHelper.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/SemanticVersionHelper.cs similarity index 67% rename from modules/docs/src/Volo.Docs.Web/Version/SemanticVersionHelper.cs rename to modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/SemanticVersionHelper.cs index c60abaccea..fbd315f834 100644 --- a/modules/docs/src/Volo.Docs.Web/Version/SemanticVersionHelper.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/Version/SemanticVersionHelper.cs @@ -5,7 +5,7 @@ using NuGet.Versioning; using Volo.Abp.DependencyInjection; using Volo.Docs.Projects; -namespace Volo.Docs.Version +namespace Volo.Docs.GitHub.Documents.Version { public class SemanticVersionHelper : IVersionHelper, ITransientDependency { @@ -14,7 +14,7 @@ namespace Volo.Docs.Version return versions.OrderByDescending(v=> SemanticVersion.Parse(NormalizeVersion(v)), new VersionComparer()).ToList(); } - public List OrderByDescending(List versions) + public List OrderByDescending(List versions) { return versions.OrderByDescending(v => SemanticVersion.Parse(NormalizeVersion(v.Name)), new VersionComparer()).ToList(); } @@ -32,9 +32,19 @@ namespace Volo.Docs.Version var versionParts = version.Split("-"); - if (versionParts[0].Split(".").Length > 3) + var firstVersionPartSplitted = versionParts[0].Split("."); + + if (firstVersionPartSplitted.Length > 3) + { + normalizedVersion = string.Join(".",firstVersionPartSplitted.Take(3)); + } + else if (firstVersionPartSplitted.Length < 3) { - normalizedVersion = string.Join(".",versionParts[0].Split(".").Take(3)); + normalizedVersion = versionParts[0]; + for (int i = firstVersionPartSplitted.Length; i < 3; i++) + { + normalizedVersion += ".0"; + } } else { diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml index 71c7d4790c..bd76814461 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml @@ -53,7 +53,7 @@