diff --git a/.github/workflows/auto-pr.yml b/.github/workflows/auto-pr.yml new file mode 100644 index 0000000000..987a3ada1b --- /dev/null +++ b/.github/workflows/auto-pr.yml @@ -0,0 +1,24 @@ +name: Merge branch dev with rel-4.2 +on: + push: + branches: + - rel-4.2 +jobs: + merge-dev-with-rel: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: dev + - name: Reset promotion branch + run: | + git fetch origin $GITHUB_REF:$GITHUB_REF + git reset --hard $GITHUB_REF + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + branch: auto-merge/rel-4-2/${{github.run_number}} + title: Merge branch dev with rel-4.2 + body: This PR generated automatically to merge dev with rel-4.2. Please review the changed files before merging to prevent any errors that may occur. + reviewers: ${{github.actor}} + token: ${{ github.token }} diff --git a/README.md b/README.md index cd27da8fe0..a73e9ef3ed 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ![build and test](https://github.com/abpframework/abp/workflows/build%20and%20test/badge.svg) [![NuGet](https://img.shields.io/nuget/v/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) -[![MyGet (with prereleases)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) +[![NuGet (with prereleases)](https://img.shields.io/nuget/vpre/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) +[![MyGet (nightly builds)](https://img.shields.io/myget/abp-nightly/vpre/Volo.Abp.svg?style=flat-square)](https://docs.abp.io/en/abp/latest/Nightly-Builds) [![NuGet Download](https://img.shields.io/nuget/dt/Volo.Abp.Core.svg?style=flat-square)](https://www.nuget.org/packages/Volo.Abp.Core) ABP Framework is a complete **infrastructure** based on the **ASP.NET Core** to create **modern web applications** and **APIs** by following the software development **best practices** and the **latest technologies**. @@ -103,4 +104,4 @@ ABP is a community-driven open source project. See [the contribution guide](http ## Support the ABP Framework -Love ABP Framework? **Please give a star** to this repository :star: \ No newline at end of file +Love ABP Framework? **Please give a star** to this repository :star: diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..c1f56de865 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json @@ -0,0 +1,14 @@ +{ + "culture": "en-GB", + "texts": { + "Account": "ABP Account - Login & Register | ABP.IO", + "Welcome": "Welcome", + "UseOneOfTheFollowingLinksToContinue": "Use one of the following links to continue", + "FrameworkHomePage": "Framework home page", + "FrameworkDocumentation": "Framework documentation", + "OfficialBlog": "Official blog", + "CommercialHomePage": "Commercial home page", + "CommercialSupportWebSite": "Commercial support web site", + "CommunityWebSite": "ABP community web site" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..970e04d24e --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en-GB.json @@ -0,0 +1,202 @@ +{ + "culture": "en-GB", + "texts": { + "Permission:Organizations": "Organisations", + "Permission:Manage": "Manage Organisations", + "Permission:DiscountRequests": "Discount Requests", + "Permission:DiscountManage": "Manage Discount Requests", + "Permission:Disable": "Disable", + "Permission:Enable": "Enable", + "Permission:EnableSendEmail": "Enable Send Email", + "Permission:SendEmail": "Send Email", + "Permission:NpmPackages": "NPM Packages", + "Permission:NugetPackages": "Nuget Packages", + "Permission:Maintenance": "Maintenance", + "Permission:Maintain": "Maintain", + "Permission:ClearCaches": "Clear caches", + "Permission:Modules": "Modules", + "Permission:Packages": "Packages", + "Permission:Edit": "Edit", + "Permission:Delete": "Delete", + "Permission:Create": "Create", + "Permission:Accounting": "Accounting", + "Permission:Accounting:Quotation": "Quotation", + "Permission:Accounting:Invoice": "Invoice", + "Menu:Organizations": "Organisations", + "Menu:Accounting": "Accounting", + "Menu:Packages": "Packages", + "Menu:DiscountRequests": "Discount Requests", + "NpmPackageDeletionWarningMessage": "This NPM Package will be deleted. Please confirm this?", + "NugetPackageDeletionWarningMessage": "This Nuget Package will be deleted. Please confirm this?", + "ModuleDeletionWarningMessage": "This Module will be deleted. Please confirm this?", + "Name": "Name", + "DisplayName": "Display name", + "ShortDescription": "Short description", + "NameFilter": "Name", + "CreationTime": "Creation time", + "IsPro": "Is pro", + "ShowOnModuleList": "Show in module list", + "EfCoreConfigureMethodName": "Configure method name", + "IsProFilter": "Is pro", + "ApplicationType": "Application type", + "Target": "Target", + "TargetFilter": "Target", + "ModuleClass": "Module class", + "NugetPackageTarget.DomainShared": "Domain Shared", + "NugetPackageTarget.Domain": "Domain", + "NugetPackageTarget.Application": "Application", + "NugetPackageTarget.ApplicationContracts": "Application Contracts", + "NugetPackageTarget.HttpApi": "Http Api", + "NugetPackageTarget.HttpApiClient": "Http Api Client", + "NugetPackageTarget.Web": "Web", + "NugetPackageTarget.EntityFrameworkCore": "Delete Entity Framework Core", + "NugetPackageTarget.MongoDB": "MongoDB", + "Edit": "Edit", + "Delete": "Delete", + "Refresh": "Refresh", + "NpmPackages": "NPM Packages", + "NugetPackages": "Nuget Packages", + "NpmPackageCount": "NPM Package Count", + "NugetPackageCount": "Nuget Package Count", + "Module": "Modules", + "ModuleInfo": "Module info", + "CreateANpmPackage": "Create a NPM package", + "CreateAModule": "Create a module", + "CreateANugetPackage": "Create a Nuget package", + "AddNew": "Add new", + "PackageAlreadyExist{0}": "\"{0}\" package has already been added.", + "ModuleAlreadyExist{0}": "\"{0}\" module has already been added.", + "ClearCache": "Clear cache", + "SuccessfullyCleared": "Successfully cleared", + "Menu:NpmPackages": "NPM Packages", + "Menu:Modules": "Modules", + "Menu:Maintenance": "Maintenance", + "Menu:NugetPackages": "Nuget Packages", + "CreateAnOrganization": "Create an organisation", + "Organizations": "Organisations", + "LongName": "Long name", + "LicenseType": "License type", + "MissingLicenseTypeField": "The license type field is required!", + "LicenseStartTime": "License start time", + "LicenseEndTime": "License end time", + "AllowedDeveloperCount": "Allowed developer count", + "UserNameOrEmailAddress": "Username or email address", + "AddOwner": "Add owner", + "UserName": "Username", + "Email": "Email", + "Developers": "Developers", + "AddDeveloper": "Add developer", + "Create": "Create", + "UserNotFound": "User not found", + "{0}WillBeRemovedFromDevelopers": "{0} Will be removed from developers, please confirm?", + "{0}WillBeRemovedFromOwners": "{0} Will be removed from owners, please confirm?", + "Computers": "Computers", + "UniqueComputerId": "Unique computer id", + "LastSeenDate": "Last seen date", + "{0}Computer{1}WillBeRemovedFromRecords": "Computer of {0} ({1}) will be removed from records", + "OrganizationDeletionWarningMessage": "Organisation will be deleted", + "DeletingLastOwnerWarningMessage": "An organisation must have at least one owner, therefore you cannot remove this owner", + "This{0}AlreadyExistInThisOrganization": "{0} already exist in this organisation", + "AreYouSureYouWantToDeleteAllComputers": "Are you sure you want to delete all computers?", + "DeleteAll": "Delete all", + "DoYouWantToCreateNewUser": "Do you want to create new user?", + "MasterModules": "Master Modules", + "OrganizationName": "Organisation name", + "CreationDate": "Creation date", + "LicenseStartDate": "License start date", + "LicenseEndDate": "License end date", + "OrganizationNamePlaceholder": "Organisation name...", + "TotalQuestionCountPlaceholder": "Total question count...", + "RemainingQuestionCountPlaceholder": "Remaining question count...", + "LicenseTypePlaceholder": "License type...", + "CreationDatePlaceholder": "Creation date...", + "LicenseStartDatePlaceholder": "License start date...", + "LicenseEndDatePlaceholder": "License end date...", + "UsernameOrEmail": "Username or email", + "UsernameOrEmailPlaceholder": "Username or email...", + "Member": "Member", + "PurchaseOrderNo": "Purchase Order No.", + "QuotationDate": "Quotation date", + "CompanyName": "Company name", + "CompanyAddress": "Company address", + "Price": "Price", + "DiscountText": "Discount text", + "DiscountQuantity": "Discount quantity", + "DiscountPrice": "Discount price", + "Quotation": "Quotation", + "ExtraText": "Extra text", + "ExtraAmount": "Extra Amount", + "DownloadQuotation": "Download Quotation", + "Invoice": "Invoice", + "TaxNumber": "Tax No.", + "InvoiceNumber": "Invoice No.", + "InvoiceDate": "Invoice Date", + "InvoiceNote": "Invoice Note", + "Quantity": "Quantity", + "AddProduct": "Add Product", + "AddProductWarning": "You need to add a Product!", + "TotalPrice": "Total Price", + "Generate": "Generate", + "MissingQuantityField": "The Quantity field is required!", + "MissingPriceField": "The Price field is required!", + "CodeUsageStatus": "Status", + "Country": "Country", + "DeveloperCount": "Developer Count", + "RequestCode": "Request Code", + "WebSite": "Website", + "GithubUsername": "Github Username", + "PhoneNumber": "Phone Number", + "ProjectDescription": "Project Description", + "Referrer": "Referrer", + "DiscountRequests": "Discount Request", + "Copylink": "Copy Link", + "Disable": "Disable", + "Enable": "Enable", + "EnableSendEmail": "Enable Send Email", + "SendEmail": "Send Email", + "SuccessfullyDisabled": "Successfully disabled", + "SuccessfullyEnabled": "Successfully enabled", + "EmailSent": "Email sent", + "SuccessfullySent": "Successfully sent", + "SuccessfullyDeleted": "Successfully deleted", + "DiscountRequestDeletionWarningMessage": "Discount request will be deleted", + "BusinessType": "Business Type", + "TotalQuestionCount": "Total question count", + "RemainingQuestionCount": "Remaining question count", + "TotalQuestionMustBeGreaterWarningMessage": "TotalQuestionCount must be greater than RemainingQuestionCount!", + "QuestionCountsMustBeGreaterThanZero": "TotalQuestionCount and RemainingQuestionCount must be zero or greater than zero!", + "UnlimitedQuestionCount": "Unlimited question count", + "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": "Author name", + "Enum:Status:2": "Approved", + "Summary": "Summary", + "AuthorName": "Author", + "CoverImage": "Cover Image", + "RemoveCacheConfirmationMessage": "Are you sure you want remove the cache for \"{0}\" article?", + "SuccessfullyRemoved": "Successfully cleared", + "RemoveCache": "Remove Cache", + "Language": "Language", + "Optional": "Optional", + "CreateArticleLanguageInfo": "The language in which the article is written" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 32d3c16a01..c190a91470 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -197,6 +197,7 @@ "RemoveCache": "Remove Cache", "Language": "Language", "Optional": "Optional", - "CreateArticleLanguageInfo": "The language in which the article is written" + "CreateArticleLanguageInfo": "The language in which the post is written", + "Enum:ContentSource:2": "Video Post" } -} \ No newline at end of file +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..235a7c61d0 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en-GB.json @@ -0,0 +1,35 @@ +{ + "culture": "en-GB", + "texts": { + "Volo.AbpIo.Domain:010004": "Maximum member count reached!", + "Volo.AbpIo.Domain:010005": "Maximum owner count reached!", + "Volo.AbpIo.Domain:010006": "This user is already an owner in this organisation!", + "Volo.AbpIo.Domain:010007": "This user is already a developer in this organisation!", + "Volo.AbpIo.Domain:010008": "Allowed Developer Count can not be less then current developer count!", + "Volo.AbpIo.Domain:010009": "Allowed Developer Count can not be less then 0!", + "Volo.AbpIo.Domain:010010": "Maximum mac address count has been exceeded!", + "Volo.AbpIo.Domain:010011": "Personal license can't have more than 1 developer!", + "Volo.AbpIo.Domain:010012": "License can't be extended one month after license expires!", + "Volo.AbpIo.Domain:020001": "Couldn't delete this NPM Package because \"{NugetPackages}\" Nuget Packages are dependent to this package.", + "Volo.AbpIo.Domain:020002": "Couldn't delete this NPM Package because \"{Modules}\" Modules are using this package.", + "Volo.AbpIo.Domain:020003": "Couldn't delete this NPM Package because \"{Modules}\" Modules are using this package and \"{NugetPackages}\" Nuget Packages are dependent to this package.", + "Volo.AbpIo.Domain:020004": "Couldn't delete this Nuget Package because \"{Modules}\" Modules are using this package.", + "WantToLearn?": "Want to learn?", + "ReadyToGetStarted?": "Ready to get started?", + "JoinOurCommunity": "Join our community", + "GetStartedUpper": "GET STARTED", + "ForkMeOnGitHub": "Fork me on GitHub", + "Features": "Features", + "GetStarted": "Get Started", + "Documents": "Documents", + "Community": "Community", + "ContributionGuide": "Contribution Guide", + "Blog": "Blog", + "Commercial": "Commercial", + "MyAccount": "My account", + "Permission:License": "License", + "Permission:UserInfo": "User info", + "SeeDocuments": "See Documents", + "Samples": "Samples" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json index 79493b537e..f061408c98 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json @@ -27,6 +27,8 @@ "Blog": "博客", "Commercial": "商业版", "MyAccount": "我的账户", + "Permission:License": "许可", + "Permission:UserInfo": "用户信息", "SeeDocuments": "查看文档", "Samples": "示例" } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..56f4ed934f --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en-GB.json @@ -0,0 +1,39 @@ +{ + "culture": "en-GB", + "texts": { + "OrganizationManagement": "Organisation Management", + "OrganizationList": "Organisation list", + "Volo.AbpIo.Commercial:010003": "You are not owner of this organisation!", + "OrganizationNotFoundMessage": "Organisation not found!", + "DeveloperCount": "Allocated / total developers", + "QuestionCount": "Remaining / total questions", + "Unlimited": "Unlimited", + "Owners": "Owners", + "AddMember": "Add member", + "AddOwner": "Add owner", + "AddDeveloper": "Add developer", + "UserName": "Username", + "Name": "Name", + "EmailAddress": "Email address", + "Developers": "Developers", + "LicenseType": "License type", + "Manage": "Manage", + "StartDate": "Start date", + "EndDate": "End date", + "Modules": "Modules", + "LicenseExtendMessage": "Your license end date is extended to {0}", + "LicenseUpgradeMessage": "Your license is upgraded to {0}", + "LicenseAddDeveloperMessage": "{0} developers added to your license", + "Volo.AbpIo.Commercial:010004": "Cannot find the specified user! The user must have already registered.", + "MyOrganizations": "My organisations", + "ApiKey": "API key", + "UserNameNotFound": "There is no user with username {0}", + "SuccessfullyAddedToNewsletter": "Thank you for subscribing to our newsletter!", + "MyProfile": "My Profile", + "EmailNotValid": "Please enter a valid email address.", + "JoinOurMarketingNewsletter": "Join our marketing newsletter", + "WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials e.g. product deals & special offers.", + "StartUsingYourLicenseNow": "Start using your license now!", + "WelcomePage": "Welcome Page" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json index 0147e71a05..82c23450a0 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/zh-Hans.json @@ -28,8 +28,12 @@ "MyOrganizations": "我的组织", "ApiKey": "API key", "UserNameNotFound": "没有用户名为{0}的用户", - "SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻通讯!", + "SuccessfullyAddedToNewsletter": "感谢你订阅我们的新闻简讯!", "MyProfile": "我的资料", - "EmailNotValid": "请输入有效的电子邮件地址" + "EmailNotValid": "请输入有效的电子邮件地址", + "JoinOurMarketingNewsletter": "加入我们的营销简讯", + "WouldLikeToReceiveMarketingMaterials": "我想收到市场营销资料,例如产品交易和特别优惠.", + "StartUsingYourLicenseNow": "立即开始使用你的许可证", + "WelcomePage": "欢迎页面" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..652090d864 --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en-GB.json @@ -0,0 +1,106 @@ +{ + "culture": "en-GB", + "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": "The Url already exists. 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", + "Latest": "Latest", + "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 Organisations", + "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.", + "CreateArticleCoverInfo": "To create an effective article, add a cover photo and an upload 16:9 aspect ratio pictures for the best view (Maximum file size: 1MB)", + "ThisExtensionIsNotAllowed": "The extension is not allowed.", + "TheFileIsTooLarge": "The file is too large.", + "GoToTheArticle": "Go to the Article", + "Contribute": "Contribute", + "OverallProgress": "Overall Progress", + "Done": "Done", + "Open": "Open", + "Closed": "Closed", + "LatestQuestionOnThe": "Latest Question on the", + "Stackoverflow": "Stackoverflow", + "Votes": "votes", + "Answer": "Answer", + "Views": "views", + "Answered": "Answered", + "WaitingForYourAnswer": "Waiting for your answer", + "Asked": "asked", + "AllQuestions": "All Questions", + "NextVersion": "Next Version", + "MilestoneErrorMessage": "Couldn't get the current milestone details from Github.", + "QuestionItemErrorMessage": "Couldn't get the latest question details from Stackoverflow.", + "Oops": "Oops!", + "CreateArticleSuccessMessage": "The Article has been successfully submitted. It will be published once it has been reviewed by the site admin.", + "ChooseCoverImage": "Choose a cover image...", + "CoverImage": "Cover Image", + "ShareYourExperiencesWithTheABPFramework": "Share your experiences with the ABP Framework!", + "Optional": "Optional", + "UpdateUserWebSiteInfo": "Example: https://johndoe.com", + "UpdateUserTwitterInfo": "Example: johndoe", + "UpdateUserGithubInfo": "Example: johndoe", + "UpdateUserLinkedinInfo": "Example: https://www.linkedin.com/...", + "UpdateUserCompanyInfo": "Example: Volosoft", + "UpdateUserJobTitleInfo": "Example: Software Developer", + "UserName": "UserName", + "Company": "Company", + "PersonalWebsite": "Personal website", + "RegistrationDate": "Registration date", + "Social": "Social", + "Biography": "Biography", + "HasNoPublishedArticlesYet": "has no published articles yet", + "Author": "Author", + "LatestGithubAnnouncements": "Latest GitHub Announcements", + "SeeAllAnnouncements": "View all Announcements", + "LatestBlogPost": "Latest Blog Post", + "Edit": "Edit", + "ProfileImageChange": "Change the profile image", + "BlogItemErrorMessage": "Couldn't get the latest blog post details from ABP.", + "PlannedReleaseDate": "Planned release date", + "CommunityArticleRequestErrorMessage": "Couldn't get the latest article request from GitHub.", + "ArticleRequestFromGithubIssue": "There aren't any article requests at the moment.", + "LatestArticles": "Latest Articles", + "ArticleRequests": "Article Requests", + "AllArticleRequests": "See all Article requests", + "SubscribeToTheNewsletter": "Subscribe to the Newsletter", + "NewsletterEmailDefinition": "Receive information about what's happening in ABP; new releases, free sources, articles, and more.", + "NoThanks": "No thanks", + "MaybeLater": "Maybe later", + "JoinOurArticleNewsletter": "Join our article newsletter", + "Community": "Community", + "Marketing": "Marketing", + "CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and Privacy Policy.", + "ArticleRequestMessageTitle": "Open an issue on the GitHub to request an article/tutorial you want to see on this web site.", + "ArticleRequestMessageBody": "Here is the list of the requested articles by the Community. Do you want to write a requested article? Please click on the request and join the discussion.", + "Language": "Language", + "CreateArticleLanguageInfo": "The language in which the article is written" + } +} \ 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 index a29355b401..8b5fa2014e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -25,19 +25,18 @@ "MostRead": "Most Read", "Latest": "Latest", "ContributeAbpCommunity": "Contribute to the ABP Community", - "SubmitYourArticle": "Submit Your Article", + "SubmitYourArticle": "Submit Your Post", "ContributionGuide": "Contribution Guide", "BugReport": "Bug Report", - "SeeAllArticles": "See All Articles", + "SeeAllArticles": "See All Posts", "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.", - "CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view.", + "CreateArticleCoverInfo": "For creating an effective article, add a cover photo. Upload 16:9 aspect ratio pictures for the best view. Maximum file size: 1MB.", "ThisExtensionIsNotAllowed": "This extension is not allowed.", "TheFileIsTooLarge": "The file is too large.", "GoToTheArticle": "Go to the Article", @@ -87,7 +86,7 @@ "PlannedReleaseDate": "Planned release date", "CommunityArticleRequestErrorMessage": "Could not get the latest article request from Github.", "ArticleRequestFromGithubIssue": "There are not any article requests now.", - "LatestArticles": "Latest Articles", + "LatestArticles": "Latest Posts", "ArticleRequests": "Article Requests", "AllArticleRequests": "See All Article Requests", "SubscribeToTheNewsletter": "Subscribe to the Newsletter", @@ -101,6 +100,17 @@ "ArticleRequestMessageTitle": "Open an issue on the GitHub to request an article/tutorial you want to see on this web site.", "ArticleRequestMessageBody": "Here, the list of the requested articles by the community. Do you want to write a requested article? Please click to the request and join to the discussion.", "Language": "Language", - "CreateArticleLanguageInfo": "The language in which the article is written" + "CreateArticleLanguageInfo": "The language in which the article is written", + "VideoPost": "Video Post", + "Article": "Article", + "Read": "Read", + "CreateGithubArticleUrlInfo": "Original GitHub URL of the article.", + "CreateVideoContentUrlInfo": "Original Youtube URL of the article.", + "CreateExternalArticleUrlInfo": "Original External Url of the article.", + "VideoContentForm": "Video Content Form", + "GithubPostForm": "Github Post Form", + "ExternalPostForm": "External Post Form", + "PostSourceTypeChooses": "We accept three source types for the posts;", + "Posts": "Posts" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json index fa793c91a0..49b819227f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json @@ -35,7 +35,6 @@ "EmailNotValid": "请输入有效的电子邮箱地址.", "FeatureRequest": "功能请求", "CreateArticleTitleInfo": "文章标题显示在文章列表中.", - "CreateArticleUrlInfo": "文章的原始GitHub/外部URL.", "CreateArticleSummaryInfo": "文章的简短摘要将显示在文章列表中.", "CreateArticleCoverInfo": "为了创建有效的文章,请添加封面图. 仅支持16:9的图片!", "ThisExtensionIsNotAllowed": "不允许此扩展名.", @@ -84,6 +83,34 @@ "Edit": "修改", "ProfileImageChange": "更改资料图片", "BlogItemErrorMessage": "无法从ABP获取最新的博客文章详细信息.", - "PlannedReleaseDate": "计划发布日期" + "PlannedReleaseDate": "计划发布日期", + "CommunityArticleRequestErrorMessage": "无法从Github获取最新的文章请求.", + "ArticleRequestFromGithubIssue": "现在没有任何文章请求.", + "LatestArticles": "最新的帖子", + "ArticleRequests": "文章请求", + "AllArticleRequests": "查看所有文章请求", + "SubscribeToTheNewsletter": "订阅简讯", + "NewsletterEmailDefinition": "获取有关ABP发生的信息,例如新版本,免费资源,文章等.", + "NoThanks": "不用了,谢谢", + "MaybeLater": "以后再说", + "JoinOurArticleNewsletter": "加入我们的文章简讯", + "Community": "社区", + "Marketing": "营销", + "CommunityPrivacyPolicyConfirmation": "我同意条款和条件以及隐私政策.", + "ArticleRequestMessageTitle": "在GitHub上创建一个Issue,以请求你要在此网站上查看的文章/教程.", + "ArticleRequestMessageBody": "在这里,是社区请求的文章列表. 您要写一篇要求的文章吗? 请单击该请求并加入讨论.", + "Language": "语言", + "CreateArticleLanguageInfo": "本文所用的语言", + "VideoPost": "视频", + "Article": "文章", + "Read": "阅读", + "CreateGithubArticleUrlInfo": "文章的原始GitHub链接.", + "CreateVideoContentUrlInfo": "文章的原始Youtube链接.", + "CreateExternalArticleUrlInfo": "本文的原始外部网址", + "VideoContentForm": "视频内容来源", + "GithubPostForm": "Github文章来源", + "ExternalPostForm": "外部文章来源", + "PostSourceTypeChooses": "我们接受文章的三种来源类型;", + "Posts": "文章" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json index a5d83f9807..3160dd45b4 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json @@ -156,6 +156,7 @@ "UiFramework": "إطار عمل واجهة المستخدم", "EmailAddress": "البريد الإلكترونى", "Mobile": "المحمول", - "ReactNative": "React Native" + "ReactNative": "React Native", + "SelectLanguage": "اختار اللغة" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json index 6f9f0726cf..523247a7b6 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de-DE.json @@ -184,6 +184,7 @@ "ABPCLIExamplesInfo": "Der Befehl new erstellt eine mehrschichtige MVC-Anwendung mit Entity Framework Core als Datenbankanbieter. Es gibt jedoch zusätzliche Optionen. Beispiele:", "SeeCliDocumentForMoreInformation": "Weitere Optionen finden Sie im ABP CLI-Dokument oder wählen Sie oben die Registerkarte \"Direkter Download\".", "Optional": "Optional", - "LocalFrameworkRef": "Behalten Sie die lokale Projektreferenz für die Framework-Pakete bei." + "LocalFrameworkRef": "Behalten Sie die lokale Projektreferenz für die Framework-Pakete bei.", + "SelectLanguage": "Sprache auswählen" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en-GB.json new file mode 100644 index 0000000000..cd0592524d --- /dev/null +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en-GB.json @@ -0,0 +1,198 @@ +{ + "culture": "en-GB", + "texts": { + "GetStarted": "Get Started - Startup Templates", + "Create": "Create", + "NewProject": "New Project", + "DirectDownload": "Direct Download", + "ProjectName": "Project name", + "ProjectType": "Project type", + "DatabaseProvider": "Database provider", + "NTier": "N-Tier", + "IncludeUserInterface": "Include user interface", + "CreateNow": "Create now", + "TheStartupProject": "The startup project", + "Tutorial": "Tutorial", + "UsingCLI": "Using CLI", + "SeeDetails": "See Details", + "AbpShortDescription": "ABP Framework is a complete infrastructure to create modern web applications by following the software development best practices and conventions.", + "SourceCodeUpper": "SOURCE CODE", + "LatestReleaseLogs": "Latest release logs", + "Infrastructure": "Infrastructure", + "Architecture": "Architecture", + "Modular": "Modular", + "DontRepeatYourself": "Don’t Repeat Yourself", + "DeveloperFocused": "Developer Focused", + "FullStackApplicationInfrastructure": "Full stack application infrastructure.", + "DomainDrivenDesign": "Domain Driven Design", + "DomainDrivenDesignExplanation": "Designed and developed based on DDD patterns and principles. Provides a layered model for your application.", + "Authorization": "Authorisation", + "AuthorizationExplanation": "Advanced authorisation with user, role and fine-grained permission system. Built on the Microsoft Identity library.", + "MultiTenancy": "Multi-Tenancy", + "MultiTenancyExplanationShort": "SaaS applications made easy! Integrated multi-tenancy from database to UI.", + "CrossCuttingConcerns": "Cross Cutting Concerns", + "CrossCuttingConcernsExplanationShort": "Complete infrastructure for authorization, validation, exception handling, caching, audit logging, transaction management and more.", + "BuiltInBundlingMinification": "Built-In Bundling & Minification", + "BuiltInBundlingMinificationExplanation": "No need to use external tools for bundling & minification. ABP offers a simpler, dynamic, powerful, modular and built-in way!", + "VirtualFileSystem": "Virtual File System", + "VirtualFileSystemExplanation": "Embed views, scripts, styles, images... into packages/libraries and reuse them in different applications.", + "Theming": "Theming", + "ThemingExplanationShort": "Use and customise the bootstrap-based standard UI theme or create your own.", + "BootstrapTagHelpersDynamicForms": "Bootstrap Tag Helpers & Dynamic Forms", + "BootstrapTagHelpersDynamicFormsExplanation": "Instead of manually writing bootstrap components, Use ABP's tag helpers to simplify and take advantage of intellisense. Quickly build UI forms based on a C# model using the dynamic form tag helper.", + "HTTPAPIsDynamicProxies": "HTTP APIs & Dynamic Proxies", + "HTTPAPIsDynamicProxiesExplanation": "Automatically expose application services as REST style HTTP APIs, and consume them with dynamic JavaScript and C# proxies.", + "CompleteArchitectureInfo": "Modern architecture to create maintainable software solutions.", + "DomainDrivenDesignBasedLayeringModelExplanation": "Helps you to implement a DDD based layered architecture and build a maintainable code base.", + "DomainDrivenDesignBasedLayeringModelExplanationCont": "Provides startup templates, abstractions, base classes, services, documentation and guides to help you to develop your application based on DDD patterns & principles.", + "MicroserviceCompatibleModelExplanation": "The core framework & pre-build modules are designed with microservice architecture in mind.", + "MicroserviceCompatibleModelExplanationCont": "Provides infrastructure, integrations, samples and documentation to implement microservice solutions easier, while it doesn’t bring additional complexity if you want a monolithic application.", + "ModularInfo": "ABP provides a module system that allows you to develop reusable application modules, tie into application lifecycle events, and express dependencies between core parts of your system.", + "PreBuiltModulesThemes": "Pre-Built Modules & Themes", + "PreBuiltModulesThemesExplanation": "Open source and commercial modules & themes are ready to use in your business application.", + "NuGetNPMPackages": "NuGet & NPM Packages", + "NuGetNPMPackagesExplanation": "Distributed as NuGet & NPM packages. Easy to install and upgrade.", + "ExtensibleReplaceable": "Extensible/Replaceable", + "ExtensibleReplaceableExplanation": "All services & modules are designed extensibility in mind. You can replace services, pages, styles and components.", + "CrossCuttingConcernsExplanation2": "Keep your codebase smaller so you can maintain focus on the code that’s specific to your business.", + "CrossCuttingConcernsExplanation3": "Don’t spend time implementing common application requirements on multiple projects.", + "AuthenticationAuthorization": "Authentication & Authorization", + "ExceptionHandling": "Exception Handling", + "Validation": "Validation", + "DatabaseConnection": "Database Connection", + "TransactionManagement": "Transaction management", + "AuditLogging": "Audit Logging", + "Caching": "Caching", + "Multitenancy": "Multitenancy", + "DataFiltering": "Data filtering", + "ConventionOverConfiguration": "Convention Over Configuration", + "ConventionOverConfigurationExplanation": "ABP implements common application conventions by default with a minimal or zero configuration.", + "ConventionOverConfigurationExplanationList1": "Auto registers known services to dependency injection.", + "ConventionOverConfigurationExplanationList2": "Exposes application services as HTTP APIs by naming conventions.", + "ConventionOverConfigurationExplanationList3": "Creates dynamic HTTP client proxies for C# and JavaScript.", + "ConventionOverConfigurationExplanationList4": "Provides default repositories for your entities.", + "ConventionOverConfigurationExplanationList5": "Manages Unit of Work per web request or application service method.", + "ConventionOverConfigurationExplanationList6": "Publishes create, update & delete events for your entities.", + "BaseClasses": "Base Classes", + "BaseClassesExplanation": "Pre-built base classes for common application patterns.", + "DeveloperFocusedExplanation": "ABP is for developers.", + "DeveloperFocusedExplanationCont": "It aims to simplify your daily software development while not restricting you from writing low level code.", + "SeeAllFeatures": "See All Features", + "CLI_CommandLineInterface": "CLI (Command Line Interface)", + "CLI_CommandLineInterfaceExplanation": "Includes a CLI to help you automate the creation of new projects and the addition of new modules.", + "StartupTemplates": "Startup Templates", + "StartupTemplatesExplanation": "Various startup templates provide a fully configured solution to jump start your development.", + "BasedOnFamiliarTools": "Based on Familiar Tools", + "BasedOnFamiliarToolsExplanation": "Built and integrated with popular tools you already know. Low learning curve, easy adaptation, comfortable development.", + "ORMIndependent": "ORM Independent", + "ORMIndependentExplanation": "The core framework is ORM/database independent and can work with any data source. Entity Framework Core and MongoDB providers are already available.", + "Features": "Explore the ABP Framework Features", + "ABPCLI": "ABP CLI", + "Modularity": "Modularity", + "BootstrapTagHelpers": "Bootstrap Tag Helpers", + "DynamicForms": "Dynamic Forms", + "BundlingMinification": "Bundling & Minification", + "BackgroundJobs": "Background Jobs", + "BackgroundJobsExplanation": "Define simple classes to execute jobs in the background as queued. Use the built-in job manager or integrate your own. Hangfire & RabbitMQ integrations are already available.", + "DDDInfrastructure": "DDD Infrastructure", + "DomainDrivenDesignInfrastructure": "Domain Driven Design Infrastructure", + "AutoRESTAPIs": "Auto REST APIs", + "DynamicClientProxies": "Dynamic Client Proxies", + "DistributedEventBus": "Distributed Event Bus", + "DistributedEventBusWithRabbitMQIntegration": "Distributed Event Bus with RabbitMQ Integration", + "TestInfrastructure": "Test Infrastructure", + "AuditLoggingEntityHistories": "Audit Logging & Entity Histories", + "ObjectToObjectMapping": "Object to Object Mapping", + "ObjectToObjectMappingExplanation": "Object to object mapping abstraction with AutoMapper integration.", + "EmailSMSAbstractions": "Email & SMS Abstractions", + "EmailSMSAbstractionsWithTemplatingSupport": "Email & SMS Abstractions with Templating Support", + "Localization": "Localization", + "SettingManagement": "Setting Management", + "ExtensionMethods": "Extension Methods", + "ExtensionMethodsHelpers": "Extension Methods & Helpers", + "AspectOrientedProgramming": "Aspect Oriented Programming", + "DependencyInjection": "Dependency Injection", + "DependencyInjectionByConventions": "Dependency Injection by Conventions", + "ABPCLIExplanation": "The ABP CLI (Command Line Interface) is a command line tool to perform common operations for ABP based solutions.", + "ModularityExplanation": "ABP provides a complete infrastructure to build your own application modules those may have entities, services, database integration, APIs, UI components and so on.", + "MultiTenancyExplanation": "ABP framework not only supports to develop multi-tenant applications, but also makes your code mostly unaware of the multi-tenancy.", + "MultiTenancyExplanation2": "Can automatically determine the current Tenant and isolate each Tenant's data from each other.", + "MultiTenancyExplanation3": "Supports single database, database per tenant and hybrid approaches.", + "MultiTenancyExplanation4": "You focus on your business code and let the framework to handle multi-tenancy on behalf of you.", + "BootstrapTagHelpersExplanation": "Instead of manually writing the repeating details of bootstrap components, use ABP's tag helpers to simplify and take advantage of intellisense. You can use Bootstrap whenever you need it.", + "DynamicFormsExplanation": "Dynamic form & input tag helpers can create the complete form from a C# class as the model.", + "AuthenticationAuthorizationExplanation": "Rich authentication & authorization options integrated to ASP.NET Core Identity & IdentityServer4. Provides an extensible & detailed permission system.", + "CrossCuttingConcernsExplanation": "Don't repeat yourself when implementing common features. Instead, focus on your business logic and let ABP automate by convention.", + "DatabaseConnectionTransactionManagement": "Database Connection & Transaction Management", + "CorrelationIdTracking": "Correlation-Id Tracking", + "BundlingMinificationExplanation": "ABP offers a simple, dynamic, powerful, modular and built-in bundling & minification system.", + "VirtualFileSystemnExplanation": "The Virtual File System makes it possible to manage files those do not physically exist on the file system (disk). It's mainly used to embed (js, css, image, cshtml...) files into assemblies and use them like physical files on runtime.", + "ThemingExplanation": "Theming system allows to develop your application & modules theme independent by defining a set of common base libraries and layouts, based on the latest Bootstrap framework.", + "DomainDrivenDesignInfrastructureExplanation": "A complete infrastructure to build layered applications based on the Domain Driven Design patterns & principles.", + "Specification": "Specification", + "Repository": "Repository", + "DomainService": "Domain Service", + "ValueObject": "Value Object", + "ApplicationService": "Application Service", + "DataTransferObject": "Data Transfer Object", + "AggregateRootEntity": "Aggregate Root, Entity", + "AutoRESTAPIsExplanation": "ABP can automagically configure your application services as API Controllers by convention.", + "DynamicClientProxiesExplanation": "Easily consume your APIs from JavaScript and C# clients.", + "DistributedEventBusWithRabbitMQIntegrationExplanation": "Easily publish & consume distributed events using built-in Distributed Event Bus with RabbitMQ integration available.", + "TestInfrastructureExplanation": "The framework has been developed unit & integration testing in mind. Provides you base classes to make it easier. Startup templates come with pre-configured for testing.", + "AuditLoggingEntityHistoriesExplanation": "Built-in audit logging for business-critical applications. Request, service, method level audit logging and entity histories with property-level details.", + "EmailSMSAbstractionsWithTemplatingSupportExplanation": "IEmailSender and ISmsSender abstractions decouples your application logic from the infrastructure. Advanced email template system allows to create & localize email templates and easily use whenever needed.", + "LocalizationExplanation": "Localization system allows to create resources in plain JSON files and use them to localize your UI. It supports advanced scenarios like inheritance, extensions and JavaScript integration while it is fully compatible with AspNet Core's localization system.", + "SettingManagementExplanation": "Define settings for your application and get values on runtime based on the current configuration, tenant and user.", + "ExtensionMethodsHelpersExplanation": "Don't repeat yourself even for trivial code parts. Extensions & helpers for standard types makes your code much cleaner and easy to write.", + "AspectOrientedProgrammingExplanation": "Provides a comfortable infrastructure to create dynamic proxies and implement Aspect Oriented Programming. Intercept any class and execute your code before & after every method execution.", + "DependencyInjectionByConventionsExplanation": "No need to register your classes to dependency injection manually. Automatically registers common service types by convention. For other type of services, you can use interfaces and attributes to make it easier and in-place.", + "DataFilteringExplanation": "Define and use data filters those are automatically applied when you query entities from database. Soft Delete & Multi-Tenant filters are provided out of the box when you implement simple interfaces.", + "PublishEvents": "Publish Events", + "HandleEvents": "Handle Events", + "AndMore": "and more...", + "Code": "Code", + "Result": "Result", + "SeeTheDocumentForMoreInformation": "See the {0} document for more information", + "IndexPageHeroSection": "open sourceWeb Application
Framework
for asp.net core", + "UiFramework": "UI Framework", + "EmailAddress": "Email address", + "Mobile": "Mobile", + "ReactNative": "React Native", + "Strong": "Strong", + "Complete": "Complete", + "BasedLayeringModel": "Based Layering Model", + "Microservice": "Microservice", + "Compatible": "Compatible", + "MeeTTheABPCommunityInfo": "Our mission is to create an environment where developers can help each other with articles, tutorials, case studies, etc. and meet like-minded people.", + "JoinTheABPCommunityInfo": "Get involved with a vibrant community and become a contributor to the ABP Framework!", + "AllArticles": "All Articles", + "SubmitYourArticle": "Submit Your Article", + "DynamicClientProxyDocument": "See the dynamic client proxy documentations for JavaScript & C#.", + "EmailSMSAbstractionsDocument": "See the emailing and SMS sending documents for more information.", + "CreateProjectWizard": "This wizard creates a new project from the startup template which is properly configured to jump start to your project.", + "TieredOption": "Creates a tiered solution where Web and Http API layers are physically separated. If not checked, creates a layered solution which is less complex and suitable for most scenarios.", + "SeparateIdentityServerOption": "Separates server side into two applications: First one is for the identity server and the second one is for your server side HTTP API.", + "UseslatestPreVersion": "Uses latest pre-release version", + "ReadTheDocumentation": "ReadThe Documentation", + "Documentation": "Documentation", + "GettingStartedTutorial": "Getting Started Tutorial", + "ApplicationDevelopmentTutorial": "Application Development Tutorial", + "TheStartupTemplate": "The Startup Template", + "InstallABPCLIInfo": "ABP CLI is the fastest way to start a new solution with the ABP framework. Install the ABP CLI using a command line window:", + "DifferentLevelOfNamespaces": "You can use different level of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.", + "ABPCLIExamplesInfo": "new command creates a layered MVC application with Entity Framework Core as the database provider. However, it has additional options. Examples:", + "SeeCliDocumentForMoreInformation": "See the ABP CLI document for more options or select the \"Direct Download\" tab above.", + "Optional": "Optional", + "LocalFrameworkRef": "Keep local project reference for the framework packages.", + "BlobStoring": "BLOB Storing", + "BlobStoringExplanation": "BLOB Storing system provides an abstraction to work with BLOBs. ABP provides some pre-built storage provider integrations (Azure, AWS, File System, Database, etc.) that you can easily use in your applications.", + "TextTemplating": "Text Templating", + "TextTemplatingExplanation": "Text templating is used to dynamically render contents based on a template and a model (a data object). For example, you can use it to create dynamic email contents with a pre-built template.", + "MultipleUIOptions": "Multiple UI Options", + "MultipleDBOptions": "Multiple Database Providers", + "MultipleUIOptionsExplanation": "The core framework is designed as UI independent and can work with any type of UI system, while there are multiple pre-built and integrated options are provided out of the box.", + "MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported:", + "SelectLanguage": "Select language" + } +} \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index e1dd405851..bdc7364788 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -192,6 +192,7 @@ "MultipleUIOptions": "Multiple UI Options", "MultipleDBOptions": "Multiple Database Providers", "MultipleUIOptionsExplanation": "The core framework is designed as UI independent and can work with any type of UI system, while there are multiple pre-built and integrated options are provided out of the box.", - "MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported;" + "MultipleDBOptionsExplanation": "The framework can work with any data source, while the following providers are officially developed and supported;", + "SelectLanguage": "Select language" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json index 3c36634f56..06d197d82e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json @@ -184,6 +184,7 @@ "ABPCLIExamplesInfo": "nuevo comando crea una aplicación MVC por capas con Entity Framework Core como proveedor de base de datos. Sin embargo, tiene distintas opciones. Ejemplos:", "SeeCliDocumentForMoreInformation": "Ver el documento ABP CLI para más opciones o selecciona la \"Direct Download\" pestaña de arriba.", "Optional": "Opcional", - "LocalFrameworkRef": "Mantén la referencia al proyecto local para los paquetes del framework." + "LocalFrameworkRef": "Mantén la referencia al proyecto local para los paquetes del framework.", + "SelectLanguage": "Vali keel" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json index 34debb8ec8..3dfd773204 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json @@ -154,6 +154,7 @@ "SeeTheDocumentForMoreInformation": "Consulte o {0} documento para obter mais informações", "IndexPageHeroSection": "código abertoAplicativo da Web
Framework
para o ASP.NET Core", "UiFramework": "UI Framework", - "EmailAddress": "Endereço de email" + "EmailAddress": "Endereço de email", + "SelectLanguage": "Selecione o idioma" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json index 5f423e940b..ef4ecbdcac 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json @@ -156,6 +156,7 @@ "UiFramework": "UI Framework", "EmailAddress": "E-Posta Adresi", "Mobile": "Mobil", - "ReactNative": "React Native" + "ReactNative": "React Native", + "SelectLanguage": "Dil seçin" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json index 6e9d9bf0da..d01d847716 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json @@ -191,6 +191,8 @@ "TextTemplatingExplanation": "文本模板是基于模板和模型(数据对象)使用动态渲染内容. 例如你可以使用预构建的模板来创建动态的电子邮件内容.", "MultipleUIOptions": "多个UI选项", "MultipleDBOptions": "多个数据库提供程序", - "MultipleUIOptionsExplanation": "核心框架设计为独立与UI,可以和任何类型的UI系统一起使用. 同时提供了多个开箱即用的预构建集成选项." + "MultipleUIOptionsExplanation": "核心框架设计为独立与UI,可以和任何类型的UI系统一起使用. 同时提供了多个开箱即用的预构建集成选项.", + "MultipleDBOptionsExplanation": "该框架可以使用任何数据源,并且以下提供程序已得到正式开发和支持;", + "SelectLanguage": "选择语言" } -} \ No newline at end of file +} diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json index 777c9f6bbd..37f6747ea0 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json @@ -154,6 +154,7 @@ "SeeTheDocumentForMoreInformation": "查看{0} 文件獲得更多訊息", "IndexPageHeroSection": "asp.net core的開源Web應用程式
框架
", "UiFramework": "UI框架", - "EmailAddress": "電子信箱地址" + "EmailAddress": "電子信箱地址", + "SelectLanguage": "选择语言" } } \ No newline at end of file diff --git a/common.props b/common.props index b79bf61409..f5db446396 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 4.1.0-rc.1 + 4.2.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ @@ -10,10 +10,8 @@ https://github.com/abpframework/abp/ true - - true - snupkg - + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb @@ -21,4 +19,4 @@ runtime; build; native; contentfiles; analyzers - \ No newline at end of file + diff --git a/docs/en/Background-Workers.md b/docs/en/Background-Workers.md index 2e1b9d2a90..b12f1881fb 100644 --- a/docs/en/Background-Workers.md +++ b/docs/en/Background-Workers.md @@ -45,7 +45,7 @@ Assume that we want to make a user passive, if the user has not logged in to the public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase { public PassiveUserCheckerWorker( - AbpTimer timer, + AbpAsyncTimer timer, IServiceScopeFactory serviceScopeFactory ) : base( timer, @@ -80,7 +80,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase ## Register Background Worker -After creating a background worker class, you should to add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class: +After creating a background worker class, you should add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class: ````csharp [DependsOn(typeof(AbpBackgroundWorkersModule))] @@ -137,4 +137,4 @@ ABP Framework's background worker system is good to implement periodic tasks. Ho ## See Also * [Quartz Integration for the background workers](Background-Workers-Quartz.md) -* [Background Jobs](Background-Jobs.md) \ No newline at end of file +* [Background Jobs](Background-Jobs.md) diff --git a/docs/en/Blob-Storing-Aliyun.md b/docs/en/Blob-Storing-Aliyun.md index 62e57e58c7..6cdebb3acc 100644 --- a/docs/en/Blob-Storing-Aliyun.md +++ b/docs/en/Blob-Storing-Aliyun.md @@ -55,7 +55,7 @@ Configure(options => * **RoleSessionName** ([NotNull]string): Used to identify the temporary access credentials, it is recommended to use different application users to distinguish. * **Policy** (string): Additional permission restrictions. See the [document](https://help.aliyun.com/document_detail/100680.html) for details. * **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. -* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): +* **ContainerName** (string): You can specify the container name in Aliyun. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aliyun has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://help.aliyun.com/knowledge_detail/39668.html): * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. * Container names Must start and end with lowercase letters and numbers. * Container names must be from **3** through **63** characters long. diff --git a/docs/en/Blob-Storing-Aws.md b/docs/en/Blob-Storing-Aws.md index cfee5c3f57..05e3caa183 100644 --- a/docs/en/Blob-Storing-Aws.md +++ b/docs/en/Blob-Storing-Aws.md @@ -60,7 +60,7 @@ Configure(options => * **Region** (string): The system name of the service. * **Policy** (string): An IAM policy in JSON format that you want to use as an inline session policy. * **DurationSeconds** (int): Validity period(s) of a temporary access certificate,minimum is 900 and the maximum is 3600. **note**: Using subaccounts operated OSS,if the value is 0. -* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): +* **ContainerName** (string): You can specify the container name in Aws. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Aws has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): * Bucket names must be between **3** and **63** characters long. * Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). * Bucket names must begin and end with a letter or number. diff --git a/docs/en/Blob-Storing-Azure.md b/docs/en/Blob-Storing-Azure.md index 9d033013da..de6ed63152 100644 --- a/docs/en/Blob-Storing-Azure.md +++ b/docs/en/Blob-Storing-Azure.md @@ -40,7 +40,7 @@ Configure(options => ### Options * **ConnectionString** (string): A connection string includes the authorization information required for your application to access data in an Azure Storage account at runtime using Shared Key authorization. Please refer to Azure documentation: https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string -* **ContainerName** (string): You can specify the container name in azure. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Azure has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): +* **ContainerName** (string): You can specify the container name in azure. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)). Please note that Azure has some **rules for naming containers**. A container name must be a valid DNS name, conforming to the [following naming rules](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): * Container names must start or end with a letter or number, and can contain only letters, numbers, and the dash (-) character. * Every dash (-) character must be immediately preceded and followed by a letter or number; consecutive dashes are not permitted in container names. * All letters in a container name must be **lowercase**. diff --git a/docs/en/Blob-Storing-Minio.md b/docs/en/Blob-Storing-Minio.md index 505c6721b0..6e0d7626b7 100644 --- a/docs/en/Blob-Storing-Minio.md +++ b/docs/en/Blob-Storing-Minio.md @@ -43,7 +43,7 @@ Configure(options => * **EndPoint** (string): URL to object storage service. Please refer to MinIO Client SDK for .NET: https://docs.min.io/docs/dotnet-client-quickstart-guide.html * **AccessKey** (string): Access key is the user ID that uniquely identifies your account. * **SecretKey** (string): Secret key is the password to your account. -* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlogContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets: +* **BucketName** (string): You can specify the bucket name in MinIO. If this is not specified, it uses the name of the BLOB container defined with the `BlobContainerName` attribute (see the [BLOB storing document](Blob-Storing.md)).MinIO is the defacto standard for S3 compatibility, So MinIO has some **rules for naming bucket**. The [following rules](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html) apply for naming MinIO buckets: * Bucket names must be between **3** and **63** characters long. * Bucket names can consist only of **lowercase** letters, numbers, dots (.), and hyphens (-). * Bucket names must begin and end with a letter or number. diff --git a/docs/en/Blob-Storing.md b/docs/en/Blob-Storing.md index 63281a5ee6..82c2a861ab 100644 --- a/docs/en/Blob-Storing.md +++ b/docs/en/Blob-Storing.md @@ -14,14 +14,14 @@ ABP BLOB Storage system is also compatible to other ABP Framework features like ## BLOB Storage Providers -The ABP Framework has already the following storage provider implementations; +The ABP Framework has already the following storage provider implementations: * [File System](Blob-Storing-File-System.md): Stores BLOBs in a folder of the local file system, as standard files. * [Database](Blob-Storing-Database.md): Stores BLOBs in a database. * [Azure](Blob-Storing-Azure.md): Stores BLOBs on the [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/). * [Aliyun](Blob-Storing-Aliyun.md): Stores BLOBs on the [Aliyun Storage Service](https://help.aliyun.com/product/31815.html). * [Minio](Blob-Storing-Minio.md): Stores BLOBs on the [MinIO Object storage](https://min.io/). -* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the[Amazon Simple Storage Service](https://aws.amazon.com/s3/). +* [Aws](Blob-Storing-Aws.md): Stores BLOBs on the [Amazon Simple Storage Service](https://aws.amazon.com/s3/). More providers will be implemented by the time. You can [request](https://github.com/abpframework/abp/issues/new) it for your favorite provider or [create it yourself](Blob-Storing-Custom-Provider.md) and [contribute](Contribution/Index.md) to the ABP Framework. diff --git a/docs/en/Blog-Posts/2021-01-07 v4_1_Release_Stable/POST.md b/docs/en/Blog-Posts/2021-01-07 v4_1_Release_Stable/POST.md new file mode 100644 index 0000000000..926f1193d4 --- /dev/null +++ b/docs/en/Blog-Posts/2021-01-07 v4_1_Release_Stable/POST.md @@ -0,0 +1,51 @@ +# ABP.IO Platform 4.1 Final Has Been Released! + +[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 4.1 versions have been released today. + +## What's New With 4.1? + +Since all the new features are already explained in details with the [4.1 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.1-RC-Has-Been-Released), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-v4.1-RC-Has-Been-Released) for all the features and enhancements. + +## Creating New Solutions + +You can create a new solution with the ABP Framework version 4.1 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started). + +> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for details. + +## How to Upgrade an Existing Solution + +### Install/Update the ABP CLI + +First of all, install the ABP CLI or upgrade to the latest version. + +If you haven't installed yet: + +```bash +dotnet tool install -g Volo.Abp.Cli +``` + +To update an existing installation: + +```bash +dotnet tool update -g Volo.Abp.Cli +``` + +### ABP UPDATE Command + +[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command: + +```bash +abp update +``` + +Run this command in the root folder of your solution. + +## ABP Community + +We started to get more contributions by the community for the [ABP Community](https://community.abp.io/) contents. Thank you all! + +We will be adding **Video Content** sharing system in a short time. We are planning to create short video contents, especially to explore the new features in every release. Again, we will be waiting video contributions by the community :) + +## About the Next Versions + +Planned preview date for the version **4.2 is January 14, 2021**. See the [Road Map](https://docs.abp.io/en/abp/latest/Road-Map) document and [GitHub Milestones](https://github.com/abpframework/abp/milestones) to learn what's planned for the next versions. We are trying to be clear about the coming features and the next release dates. \ No newline at end of file diff --git a/docs/en/CLI.md b/docs/en/CLI.md index 2e7b117799..fc76aa997b 100644 --- a/docs/en/CLI.md +++ b/docs/en/CLI.md @@ -105,7 +105,13 @@ abp new Acme.BookStore * `--preview`: Use latest preview version. * `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D:\local-template` or `https://.../my-template-file.zip`). * `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder. -* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution). +* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution). +* `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's: + * `SqlServer` + * `MySQL` + * `SQLite` + * `Oracle-Devart` + * `PostgreSQL` * `--local-framework-ref --abp-path`: Uses local projects references to the ABP framework instead of using the NuGet packages. This can be useful if you download the ABP Framework source code and have a local reference to the framework from your application. * `--no-random-port`: Uses template's default ports. @@ -162,7 +168,7 @@ abp add-package Volo.Abp.MongoDB Adds a [multi-package application module](Modules/Index) to a solution by finding all packages of the module, finding related projects in the solution and adding each package to the corresponding project in the solution. -It can also create a new module for your solution and add it to your solution. See `--new-template` option. +It can also create a new module for your solution and add it to your solution. See `--new` option. > A business module generally consists of several packages (because of layering, different database provider options or other reasons). Using `add-module` command dramatically simplifies adding a module to a solution. However, each module may require some additional configurations which is generally indicated in the documentation of the related module. @@ -219,7 +225,7 @@ abp get-source Volo.Blogging --local-framework-ref --abp-path D:\GitHub\abp * `--output-folder` or `-o`: Specifies the directory that source code will be downloaded in. If not specified, current directory is used. * `--version` or `-v`: Specifies the version of the source code that will be downloaded. If not specified, latest version is used. * `--preview`: If no version option is specified, this option specifies if latest [preview version](Previews.md) will be used instead of latest stable version. -* `-- local-framework-ref --abp-path`: Path of [ABP Framework GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. +* `--local-framework-ref --abp-path`: Path of [ABP Framework GitHub repository](https://github.com/abpframework/abp) in your computer. This will be used for converting project references to your local system. If this is not specified, project references will be converted to NuGet references. ### generate-proxy @@ -419,4 +425,5 @@ abp bundle [options] * ```--working-directory``` or ```-wd```: Specifies the working directory. This option is useful when executing directory doesn't contain a Blazor project file. * ```--force``` or ```-f```: Forces to build project before generating references. -`bundle` command reads the `appsettings.json` file inside the Blazor project for bundling options. For more details about managing style and script references in Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) \ No newline at end of file +`bundle` command reads the `appsettings.json` file inside the Blazor project for bundling options. For more details about managing style and script references in Blazor apps, see [Managing Global Scripts & Styles](UI/Blazor/Global-Scripts-Styles.md) + diff --git a/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md b/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md index 1342407880..9ae426624f 100644 --- a/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md +++ b/docs/en/Community-Articles/2020-04-19-Customize-the-SignIn-Manager/POST.md @@ -71,7 +71,8 @@ public async override Task GetE ?? provider; return new Microsoft.AspNetCore.Identity.ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) { - AuthenticationTokens = auth.Properties.GetTokens() + AuthenticationTokens = auth.Properties.GetTokens(), + AuthenticationProperties = auth.Properties }; } ```` diff --git a/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md b/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md index 718a949f3a..d0d2b1ccee 100644 --- a/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md +++ b/docs/en/Community-Articles/2020-08-12-Patch-Chrome-Login-Issue-For-IdentityServer4/POST.md @@ -11,157 +11,75 @@ When you use HTTP on your Identity Server 4 enabled website, users may not login Create the below extension in your ***.Web** project. ```csharp -using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection { public static class SameSiteCookiesServiceCollectionExtensions { - /// - /// -1 defines the unspecified value, which tells ASPNET Core to NOT - /// send the SameSite attribute. With ASPNET Core 3.1 the - /// enum will have a definition for - /// Unspecified. - /// - private const SameSiteMode Unspecified = (SameSiteMode)(-1); - - /// - /// Configures a cookie policy to properly set the SameSite attribute - /// for Browsers that handle unknown values as Strict. Ensure that you - /// add the - /// into the pipeline before sending any cookies! - /// - /// - /// Minimum ASPNET Core Version required for this code: - /// - 2.1.14 - /// - 2.2.8 - /// - 3.0.1 - /// - 3.1.0-preview1 - /// Starting with version 80 of Chrome (to be released in February 2020) - /// cookies with NO SameSite attribute are treated as SameSite=Lax. - /// In order to always get the cookies send they need to be set to - /// SameSite=None. But since the current standard only defines Lax and - /// Strict as valid values there are some browsers that treat invalid - /// values as SameSite=Strict. We therefore need to check the browser - /// and either send SameSite=None or prevent the sending of SameSite=None. - /// Relevant links: - /// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 - /// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 - /// - https://www.chromium.org/updates/same-site - /// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ - /// - https://bugs.webkit.org/show_bug.cgi?id=198181 - /// - /// The service collection to register into. - /// The modified . - public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services) + public static IServiceCollection AddSameSiteCookiePolicy(this IServiceCollection services) { services.Configure(options => { - options.MinimumSameSitePolicy = Unspecified; - options.OnAppendCookie = cookieContext => - CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); - options.OnDeleteCookie = cookieContext => - CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.MinimumSameSitePolicy = SameSiteMode.Unspecified; + options.OnAppendCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); + options.OnDeleteCookie = cookieContext => + CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); }); return services; } - + private static void CheckSameSite(HttpContext httpContext, CookieOptions options) { if (options.SameSite == SameSiteMode.None) { var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); - - if (DisallowsSameSiteNone(userAgent)) + if (!httpContext.Request.IsHttps || DisallowsSameSiteNone(userAgent)) { - options.SameSite = Unspecified; + // For .NET Core < 3.1 set SameSite = (SameSiteMode)(-1) + options.SameSite = SameSiteMode.Unspecified; } } } - /// - /// Checks if the UserAgent is known to interpret an unknown value as Strict. - /// For those the property should be - /// set to . - /// - /// - /// This code is taken from Microsoft: - /// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ - /// - /// The user agent string to check. - /// Whether the specified user agent (browser) accepts SameSite=None or not. private static bool DisallowsSameSiteNone(string userAgent) { // Cover all iOS based browsers here. This includes: - // - Safari on iOS 12 for iPhone, iPod Touch, iPad - // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad - // - Chrome on iOS 12 for iPhone, iPod Touch, iPad - // All of which are broken by SameSite=None, because they use the - // iOS networking stack. - // Notes from Thinktecture: - // Regarding https://caniuse.com/#search=samesite iOS versions lower - // than 12 are not supporting SameSite at all. Starting with version 13 - // unknown values are NOT treated as strict anymore. Therefore we only - // need to check version 12. - if (userAgent.Contains("CPU iPhone OS 12") - || userAgent.Contains("iPad; CPU OS 12")) + // - Safari on iOS 12 for iPhone, iPod Touch, iPad + // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad + // - Chrome on iOS 12 for iPhone, iPod Touch, iPad + // All of which are broken by SameSite=None, because they use the iOS networking stack + if (userAgent.Contains("CPU iPhone OS 12") || userAgent.Contains("iPad; CPU OS 12")) { return true; } - // Cover Mac OS X based browsers that use the Mac OS networking stack. - // This includes: - // - Safari on Mac OS X. + // Cover Mac OS X based browsers that use the Mac OS networking stack. This includes: + // - Safari on Mac OS X. // This does not include: - // - Chrome on Mac OS X - // because they do not use the Mac OS networking stack. - // Notes from Thinktecture: - // Regarding https://caniuse.com/#search=samesite MacOS X versions lower - // than 10.14 are not supporting SameSite at all. Starting with version - // 10.15 unknown values are NOT treated as strict anymore. Therefore we - // only need to check version 10.14. - if (userAgent.Contains("Safari") - && userAgent.Contains("Macintosh; Intel Mac OS X 10_14") - && userAgent.Contains("Version/")) + // - Chrome on Mac OS X + // Because they do not use the Mac OS networking stack. + if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && + userAgent.Contains("Version/") && userAgent.Contains("Safari")) { return true; } - // Cover Chrome 50-69, because some versions are broken by SameSite=None + // Cover Chrome 50-69, because some versions are broken by SameSite=None, // and none in this range require it. - // Note: this covers some pre-Chromium Edge versions, + // Note: this covers some pre-Chromium Edge versions, // but pre-Chromium Edge does not require SameSite=None. - // Notes from Thinktecture: - // We can not validate this assumption, but we trust Microsofts - // evaluation. And overall not sending a SameSite value equals to the same - // behavior as SameSite=None for these old versions anyways. if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) { return true; } - - if (GetChromeVersion(userAgent) >= 80) - { - return true; - } return false; } - - private static int GetChromeVersion(string userAgent) - { - try - { - return Convert.ToInt32(userAgent.Split("Chrome/")[1].Split('.')[0]); - } - catch (Exception) - { - return 0; - } - } } } ``` @@ -173,7 +91,7 @@ Assume that your project name is *Acme.BookStore*. Then open `AcmeBookStoreWebMo Add the following line to `ConfigureServices()` method. ```csharp - context.Services.ConfigureNonBreakingSameSiteCookies(); + context.Services.AddSameSiteCookiePolicy(); // cookie policy to deal with temporary browser incompatibilities ``` ### Step-3 @@ -195,18 +113,14 @@ public override void OnApplicationInitialization(ApplicationInitializationContex app.UseHsts(); } - app.UseCookiePolicy(); //<--- added this ---> + app.UseCookiePolicy(); // added this, Before UseAuthentication or anything else that writes cookies. //.... } ``` - - It's all! You are ready to go! - - --- -Referenced from https://www.thinktecture.com/en/identity/samesite/prepare-your-identityserver/ +Referenced from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/ diff --git a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md index 7b1ae846af..02cef11a2e 100644 --- a/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md +++ b/docs/en/Community-Articles/2020-12-11-Using-Angular-Material-Components-With-ABP-Framework/POST.md @@ -260,7 +260,7 @@ import { MatFormFieldModule } from "@angular/material/form-field"; import { MatInputModule } from "@angular/material/input"; import { MatSelectModule } from "@angular/material/select"; import { MatIconModule } from "@angular/material/icon"; -import { MatNativeDateModule } from '@angular/material/core'; +import { MatNativeDateModule } from "@angular/material/core"; @NgModule({ imports: [ @@ -1576,6 +1576,10 @@ Final UI looks as shown below: ![Author With Books](./author-with-books.gif) +## The Source Code + +You can download the source code from [here](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreAngularMaterial). + ## Conclusion We implemented Angular Material Components to our angular application which was created with ABP Framework. There is no blocker case of using angular libraries with the ABP framework. diff --git a/docs/en/Contribution/Index.md b/docs/en/Contribution/Index.md index 07dc73ab24..d2525ae191 100644 --- a/docs/en/Contribution/Index.md +++ b/docs/en/Contribution/Index.md @@ -10,7 +10,7 @@ If you want to write **articles** or **how to guides** related to the ABP Framew You can always send pull requests to the GitHub repository. -- Clone the [ABP repository](https://github.com/abpframework/abp/) from GitHub. +- [Fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the [ABP repository](https://github.com/abpframework/abp/) from GitHub. - Build the repository using the `/build/build-all.ps1 -f` for one time. - Make the necessary changes, including unit/integration tests. - Send a pull request. diff --git a/docs/en/Customizing-Application-Modules-Guide.md b/docs/en/Customizing-Application-Modules-Guide.md index d184d82906..56b94ae4fc 100644 --- a/docs/en/Customizing-Application-Modules-Guide.md +++ b/docs/en/Customizing-Application-Modules-Guide.md @@ -43,17 +43,59 @@ In any case, you can create a **separate solution** for the desired module and d #### Publishing the Customized Module as Packages -One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company. +One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company, for example. ## Module Customization / Extending Approaches -This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways: +This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways. + +### Module Entity Extension System + +> Module entity extension system is the **main and high level extension system** that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. + +See the [Module Entity Extensions document](Module-Entity-Extensions.md) to learn how to use it. + +### Extending Entities + +If you only need to get/set extra data on an existing entity, follow the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document. + +### Overriding Services/Components + +In addition to the extensibility systems, you can partially or completely override any service or user interface page/component. -* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) * [Overriding Services](Customizing-Application-Modules-Overriding-Services.md) * [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md) -### See Also +### Additional UI Extensibility Points + +There are some low level systems that you can control entity actions, table columns and page toolbar of a page defined by a module. + +#### Entity Actions + +Entity action extension system allows you to add a new action to the action menu for an entity on the user interface; + +* [Entity Action Extensions for ASP.NET Core UI](UI/AspNetCore/Entity-Action-Extensions.md) +* [Entity Action Extensions for Angular](UI/Angular/Entity-Action-Extensions.md) + +#### Data Table Column Extensions + +Data table column extension system allows you to add a new column in the data table on the user interface; + +* [Data Table Column Extensions for ASP.NET Core UI](UI/AspNetCore/Data-Table-Column-Extensions.md) +* [Data Table Column Extensions for Angular](UI/Angular/Data-Table-Column-Extensions.md) + +#### Page Toolbar + +Page toolbar system allows you to add components to the toolbar of a page; + +* [Page Toolbar Extensions for ASP.NET Core UI](UI/AspNetCore/Page-Toolbar-Extensions.md) +* [Page Toolbar Extensions for Angular](UI/Angular/Page-Toolbar-Extensions.md) + +#### Others + +* [Dynamic Form Extensions for Angular](UI/Angular/Dynamic-Form-Extensions.md) + +## See Also Also, see the following documents: diff --git a/docs/en/Emailing.md b/docs/en/Emailing.md index aa341a3898..a98874165d 100644 --- a/docs/en/Emailing.md +++ b/docs/en/Emailing.md @@ -223,7 +223,7 @@ Pathes of the templates in the virtual file system are shown below: * `/Volo/Abp/Emailing/Templates/Layout.tpl` * `/Volo/Abp/Emailing/Templates/Message.tpl` -If you add files to the same localization in the virtual file system, your files will override them. +If you add files to the same location in the virtual file system, your files will override them. Templates are inline localized, that means you can take the power of the [localization system](Localization.md) to make your templates multi-cultural. @@ -247,4 +247,4 @@ So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent ## See Also -* [MailKit integration for sending emails](MailKit.md) \ No newline at end of file +* [MailKit integration for sending emails](MailKit.md) diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index a93ee217da..be5968a9af 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -586,7 +586,7 @@ First step is to change the connection string section inside all the `appsetting ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" } ```` @@ -594,10 +594,10 @@ Change it as shown below: ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True", + "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True" } ```` diff --git a/docs/en/Entity-Framework-Core-MySQL.md b/docs/en/Entity-Framework-Core-MySQL.md index a04d6073fc..14f419557e 100644 --- a/docs/en/Entity-Framework-Core-MySQL.md +++ b/docs/en/Entity-Framework-Core-MySQL.md @@ -12,10 +12,10 @@ Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFra ## UseMySQL() -Find `UseSqlServer()` calls in your solution, replace with `UseMySQL()`. Check the following files: +Find `UseSqlServer()` calls in your solution. Check the following files: -* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. -* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. +* *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySQL()`. +* *YourProjectName*MigrationsDbContextFactory.cs inside the `.EntityFrameworkCore.DbMigrations` project. Replace `UseSqlServer()` with `UseMySql()`. Then add a new parameter (`ServerVersion`) to `UseMySql()` method. Example: `.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.FromString("8.0.21-mysql"))`. See [this issue](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1233) for more information about `ServerVersion`) > Depending on your solution structure, you may find more code files need to be changed. @@ -38,4 +38,4 @@ Run the `.DbMigrator` project to create the database and seed the initial data. ## Run the Application -It is ready. Just run the application and enjoy coding. \ No newline at end of file +It is ready. Just run the application and enjoy coding. diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 8c0a1c9cc9..b2b216397a 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -735,6 +735,47 @@ Configure(options => }); ```` +### Customize Bulk Operations + +If you have better logic or using an external library for bulk operations, you can override the logic via implementing`IEfCoreBulkOperationProvider`. + +- You may use example template below: + +```csharp +public class MyCustomEfCoreBulkOperationProvider : IEfCoreBulkOperationProvider, ITransientDependency +{ + public async Task DeleteManyAsync(IEfCoreRepository repository, + IEnumerable entities, + bool autoSave, + CancellationToken cancellationToken) + where TDbContext : IEfCoreDbContext + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task InsertManyAsync(IEfCoreRepository repository, + IEnumerable entities, + bool autoSave, + CancellationToken cancellationToken) + where TDbContext : IEfCoreDbContext + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task UpdateManyAsync(IEfCoreRepository repository, + IEnumerable entities, + bool autoSave, + CancellationToken cancellationToken) + where TDbContext : IEfCoreDbContext + where TEntity : class, IEntity + { + // Your logic here. + } +} +``` + ## See Also * [Entities](Entities.md) diff --git a/docs/en/Module-Entity-Extensions.md b/docs/en/Module-Entity-Extensions.md index 042c10b815..dc23d1e663 100644 --- a/docs/en/Module-Entity-Extensions.md +++ b/docs/en/Module-Entity-Extensions.md @@ -1,5 +1,7 @@ # Module Entity Extensions +> This feature is not supported by the Blazor UI yet. + ## Introduction Module entity extension system is a **high level** extension system that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point. @@ -325,6 +327,108 @@ One of the following names can be used as the localization key: Localization system searches for the key with the given order. Localized text are used on the table and the create/edit forms. +### Navigation Properties / Foreign Keys + +It is supported to add an extension property to an entity that is Id of another entity (foreign key). + +#### Example: Associate a department to a user + +````csharp +ObjectExtensionManager.Instance.Modules() + .ConfigureIdentity(identity => + { + identity.ConfigureUser(user => + { + user.AddOrUpdateProperty( + "DepartmentId", + property => + { + property.UI.Lookup.Url = "/api/departments"; + property.UI.Lookup.DisplayPropertyName = "name"; + } + ); + }); + }); +```` + +`UI.Lookup.Url` option takes a URL to get list of departments to select on edit/create forms. This endpoint can be a typical controller, an [auto API controller](API/Auto-API-Controllers.md) or any type of endpoint that returns a proper JSON response. + +An example implementation that returns a fixed list of departments (in real life, you get the list from a data source): + +````csharp +[Route("api/departments")] +public class DepartmentController : AbpController +{ + [HttpGet] + public async Task> GetAsync() + { + return new ListResultDto( + new[] + { + new DepartmentDto + { + Id = Guid.Parse("6267f0df-870f-4173-be44-d74b4b56d2bd"), + Name = "Human Resources" + }, + new DepartmentDto + { + Id = Guid.Parse("21c7b61f-330c-489e-8b8c-80e0a78a5cc5"), + Name = "Production" + } + } + ); + } +} +```` + +This API returns such a JSON response: + +````json +{ + "items": [{ + "id": "6267f0df-870f-4173-be44-d74b4b56d2bd", + "name": "Human Resources" + }, { + "id": "21c7b61f-330c-489e-8b8c-80e0a78a5cc5", + "name": "Production" + }] +} +```` + +ABP can now show an auto-complete select component to pick the department while creating or editing a user: + +![extension-navigation-property-form](images/extension-navigation-property-form.png) + +And shows the department name on the data table: + +![extension-navigation-property-form](images/extension-navigation-property-table.png) + +#### Lookup Options + +`UI.Lookup` has the following options to customize how to read the response returned from the `Url`: + +* `Url`: The endpoint to get the list of target entities. This is used on edit and create forms. +* `DisplayPropertyName`: The property in the JSON response to read the display name of the target entity to show on the UI. Default: `text`. +* `ValuePropertyName`: The property in the JSON response to read the Id of the target entity. Default: `id`. +* `FilterParamName`: ABP allows to search/filter the entity list on edit/create forms. This is especially useful if the target list contains a lot of items. In this case, you can return a limited list (top 100, for example) and allow user to search on the list. ABP sends filter text to the server (as a simple query string) with the name of this option. Default: `filter`. +* `ResultListPropertyName`: By default, returned JSON result should contain the entity list in an `items` array. You can change the name of this field. Default: `items`. + +#### Lookup Properties: How Display Name Works? + +You may wonder how ABP shows the department name on the data table above. + +It is easy to understand how to fill the dropdown on edit and create forms: ABP makes an AJAX request to the given URL. It re-requests whenever user types to filter the items. + +However, for the data table, multiple items are shown on the UI and performing a separate AJAX call to get display name of the department for each row would not be so efficient. + +Instead, the display name of the foreign entity is also saved as an extra property of the entity (see *Extra Properties* section of the [Entities](Entities.md) document) in addition to Id of the foreign entity. If you check the database, you can see the `DepartmentId_Text` in the `ExtraProperties` field in the database table: + +````json +{"DepartmentId":"21c7b61f-330c-489e-8b8c-80e0a78a5cc5","DepartmentId_Text":"Production"} +```` + +So, this is a type of *data duplication*. If your target entity's name changes in the database later, there is no automatic synchronization system. The system works as expected, but you see the old name on the data tables. If that's a problem for you, you should care yourself to update this information when display name of your entity changes. + ## Database Mapping For relational databases, all extension property values are stored in a single field in the table: diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index 5780b4b827..35478d9ce0 100644 --- a/docs/en/Modules/Docs.md +++ b/docs/en/Modules/Docs.md @@ -47,7 +47,7 @@ The database connection string is located in `appsettings.json` of your `Acme.My ```json { "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True" } } ``` diff --git a/docs/en/Modules/Index.md b/docs/en/Modules/Index.md index 9d0199f4c3..8851f8b554 100644 --- a/docs/en/Modules/Index.md +++ b/docs/en/Modules/Index.md @@ -2,7 +2,7 @@ ABP is a **modular application framework** which consists of dozens of **NuGet & NPM packages**. It also provides a complete infrastructure to build your own application modules which may have entities, services, database integration, APIs, UI components and so on. -There are **two types of modules.** They don't have any structural difference but categorized by functionality and purpose: +There are **two types of modules.** They don't have any structural difference but are categorized by functionality and purpose: * [**Framework modules**](https://github.com/abpframework/abp/tree/master/framework/src): These are **core modules of the framework** like caching, emailing, theming, security, serialization, validation, EF Core integration, MongoDB integration... etc. They do not have application/business functionalities but makes your daily development easier by providing common infrastructure, integration and abstractions. * [**Application modules**](https://github.com/abpframework/abp/tree/master/modules): These modules implement specific application/business functionalities like blogging, document management, identity management, tenant management... etc. They generally have their own entities, services, APIs and UI components. diff --git a/docs/en/Modules/Setting-Management.md b/docs/en/Modules/Setting-Management.md index 134e24aed2..dff8da3f8e 100644 --- a/docs/en/Modules/Setting-Management.md +++ b/docs/en/Modules/Setting-Management.md @@ -75,7 +75,7 @@ Setting values are cached using the [distributed cache](../Caching.md) system. A ## Setting Management Providers -Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered by the order below: +Setting Management module is extensible, just like the [setting system](../Settings.md). You can extend it by defining setting management providers. There are 5 pre-built setting management providers registered it the following order: * `DefaultValueSettingManagementProvider`: Gets the value from the default value of the setting definition. It can not set the default value since default values are hard-coded on the setting definition. * `ConfigurationSettingManagementProvider`: Gets the value from the [IConfiguration service](../Configuration.md). It can not set the configuration value because it is not possible to change the configuration values on runtime. diff --git a/docs/en/MongoDB.md b/docs/en/MongoDB.md index 98769c13b8..7fae9c3892 100644 --- a/docs/en/MongoDB.md +++ b/docs/en/MongoDB.md @@ -382,3 +382,44 @@ context.Services.AddMongoDbContext(options => ``` In this example, `OtherMongoDbContext` implements `IBookStoreMongoDbContext`. This feature allows you to have multiple MongoDbContext (one per module) on development, but single MongoDbContext (implements all interfaces of all MongoDbContexts) on runtime. + +### Customize Bulk Operations + +If you have better logic or using an external library for bulk operations, you can override the logic via implementing `IMongoDbBulkOperationProvider`. + +- You may use example template below: + +```csharp +public class MyCustomMongoDbBulkOperationProvider : IMongoDbBulkOperationProvider, ITransientDependency +{ + public async Task DeleteManyAsync(IMongoDbRepository repository, + IEnumerable entities, + IClientSessionHandle sessionHandle, + bool autoSave, + CancellationToken cancellationToken) + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task InsertManyAsync(IMongoDbRepository repository, + IEnumerable entities, + IClientSessionHandle sessionHandle, + bool autoSave, + CancellationToken cancellationToken) + where TEntity : class, IEntity + { + // Your logic here. + } + + public async Task UpdateManyAsync(IMongoDbRepository repository, + IEnumerable entities, + IClientSessionHandle sessionHandle, + bool autoSave, + CancellationToken cancellationToken) + where TEntity : class, IEntity + { + // Your logic here. + } +} +``` \ No newline at end of file diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index 308be4bfa7..72265b2690 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -410,3 +410,7 @@ ObjectExtensionManager.Instance ```` See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. + +## See Also + +* [Module Entity Extensions](Module-Entity-Extensions.md) \ No newline at end of file diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 5ff8372af8..5ad3107ca6 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -87,6 +87,11 @@ If your entity is a soft-delete entity, you can use the `HardDeleteAsync` method See the [Data Filtering](Data-Filtering.md) documentation for more about soft-delete. +## Bulk Operations +You can execute bulk operations with `InsertManyAsync`, `UpdateManyAsync`, `DeleteManyAsync` methods. + +> **WARNING:** ConcurrencyStamp can't be checked at bulk operations! + ## Custom Repositories Default generic repositories will be sufficient for most cases. However, you may need to create a custom repository class for your entity. diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md index e0e8c94c45..b5dd0fab29 100644 --- a/docs/en/Samples/Index.md +++ b/docs/en/Samples/Index.md @@ -63,8 +63,7 @@ While there is no Razor Pages & MongoDB combination, you can check both document * **GRPC Demo**: Shows how to add a gRPC service to an ABP Framework based web application and consume it from a console application. * [Source code](https://github.com/abpframework/abp-samples/tree/master/GrpcDemo) * **Telerik Blazor Integration**: Shows how to install and use Telerik Blazor components with the ABP Framework. - * [Source code](https://github.com/abpframework/abp-samples/tree/master/DevExpress-Blazor) - * [Article](https://community.abp.io/articles/using-devexpress-blazor-ui-components-with-the-abp-framework-wrpoa8rw) + * [Article](https://community.abp.io/articles/how-to-integrate-the-telerik-blazor-components-to-the-abp-blazor-ui-q8g31abb) * **Angular Material Integration**: Implemented the web application tutorial using the Angular Material library. * [Source code](https://github.com/abpframework/abp-samples/tree/master/AcmeBookStoreAngularMaterial) * [Article](https://community.abp.io/articles/using-angular-material-components-with-the-abp-framework-af8ft6t9) diff --git a/docs/en/Samples/Microservice-Demo.md b/docs/en/Samples/Microservice-Demo.md index 492dfb203b..285c2b10a0 100644 --- a/docs/en/Samples/Microservice-Demo.md +++ b/docs/en/Samples/Microservice-Demo.md @@ -842,7 +842,7 @@ It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. I ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", "Blogging": "mongodb://localhost/MsDemo_Blogging" } ```` @@ -968,8 +968,8 @@ There are two connection strings in the `appsettings.json` file: ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", - "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", + "ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True" } ```` diff --git a/docs/en/Text-Templating.md b/docs/en/Text-Templating.md index b080087ba8..bdea7cd00b 100644 --- a/docs/en/Text-Templating.md +++ b/docs/en/Text-Templating.md @@ -423,7 +423,7 @@ This example simply adds a header and footer to the template and renders the con **3)** Configure the embedded resources in the `.csproj` file * Add [Microsoft.Extensions.FileProviders.Embedded](https://www.nuget.org/packages/Microsoft.Extensions.FileProviders.Embedded) NuGet package to the project. -* Add `true` into the `...` section of your `.csproj` file. +* Add `true` into the `...` section of your `.csproj` file. * Add the following code into your `.csproj` file: ````xml diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md index 6df847d6d0..54a1869c0e 100644 --- a/docs/en/UI/Angular/Component-Replacement.md +++ b/docs/en/UI/Angular/Component-Replacement.md @@ -350,7 +350,7 @@ import { ApplicationConfiguration, AuthService, ConfigState, - SessionState, + SessionStateService, SetLanguage, } from '@abp/ng.core'; import { Component, AfterViewInit } from '@angular/core'; @@ -396,10 +396,10 @@ export class NavItemsComponent implements AfterViewInit { } get selectedLangCulture(): string { - return this.store.selectSnapshot(SessionState.getLanguage); + return this.sessionState.getLanguage(); } - constructor(private store: Store, private authService: AuthService) {} + constructor(private store: Store, private authService: AuthService, private sessionState: SessionStateService) {} ngAfterViewInit() { fromEvent(window, 'resize') diff --git a/docs/en/UI/Angular/Data-Table-Column-Extensions.md b/docs/en/UI/Angular/Data-Table-Column-Extensions.md new file mode 100644 index 0000000000..6f1367d934 --- /dev/null +++ b/docs/en/UI/Angular/Data-Table-Column-Extensions.md @@ -0,0 +1,320 @@ +# Data Table Column (or Entity Prop) Extensions for Angular UI + + +## Introduction + +Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below: + +Entity Prop Extension Example: 'Name' Column + +You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells. + +## How to Set Up + +In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../Modules/Identity.md). + +### Step 1. Create Entity Prop Contributors + +The following code prepares a constant named `identityEntityPropContributors`, ready to be imported and used in your root module: + +```js +// src/app/entity-prop-contributors.ts + +import { + eIdentityComponents, + IdentityEntityPropContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { EntityProp, EntityPropList, ePropType } from '@abp/ng.theme.shared/extensions'; + +const nameProp = new EntityProp({ + type: ePropType.String, + name: 'name', + displayName: 'AbpIdentity::Name', + sortable: true, + columnWidth: 250, +}); + +export function namePropContributor(propList: EntityPropList) { + propList.addAfter(nameProp, 'userName', (value, name) => value.name === name); +} + +export const identityEntityPropContributors: IdentityEntityPropContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + namePropContributor, + // You can add more contributors here + ], +}; + +``` + +The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addAfter` method, which adds a node with given value after the first node that has the previous value. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Entity Prop Contributors + +Import `identityEntityPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityEntityPropContributors } from './entity-prop-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + entityPropContributors: identityEntityPropContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `IdentityModule`. + +## How to Render Custom HTML in Cells + +You can use the `valueResolver` to render an HTML string in the table. Imagine we want to show a red times icon (❌) next to unconfirmed emails and phones, instead of showing a green check icon next to confirmed emails and phones. The contributors below would do that for you. + +Entity Prop Extension Example: Custom Cell Render + +```js +// src/app/entity-prop-contributors.ts + +import { + eIdentityComponents, + IdentityEntityPropContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { EntityProp, EntityPropList } from '@abp/ng.theme.shared/extensions'; +import { of } from 'rxjs'; + +export function emailPropContributor(propList: EntityPropList) { + const index = propList.indexOf('email', (value, name) => value.name === name); + const droppedNode = propList.dropByIndex(index); + const emailProp = new EntityProp({ + ...droppedNode.value, + valueResolver: data => { + const { email, emailConfirmed } = data.record; + const icon = email && !emailConfirmed ? `` : ''; + + return of((email || '') + icon); // should return an observable + }, + }); + + propList.addByIndex(emailProp, index); +} + +export function phonePropContributor(propList: EntityPropList) { + const index = propList.indexOf('phoneNumber', (value, name) => value.name === name); + const droppedNode = propList.dropByIndex(index); + const phoneProp = new EntityProp({ + ...droppedNode.value, + valueResolver: data => { + const { phoneNumber, phoneNumberConfirmed } = data.record; + const icon = + phoneNumber && !phoneNumberConfirmed ? `` : ''; + + return of((phoneNumber || '') + icon); // should return an observable + }, + }); + + propList.addByIndex(phoneProp, index); +} + +export const identityEntityPropContributors: IdentityEntityPropContributors = { + [eIdentityComponents.Users]: [emailPropContributor, phonePropContributor], +}; + +``` + +> The `valueResolver` method should return an observable. You can wrap your return values with `of` from RxJS for that. + +## Object Extensions + +Extra properties defined on an existing entity will be included in the table based on their configuration. The values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior. + +## API + +### PropData\ + +`PropData` is the shape of the parameter passed to all callbacks or predicates in an `EntityProp`. + +It has the following properties: + +- **record** is the row data, i.e. current value rendered in the table. + + ```js + { + type: ePropType.String, + name: 'name', + valueResolver: data => { + const name = data.record.name || ''; + return of(name.toUpperCase()); + }, + } + ``` + +- **index** is the table index where the record is at. + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleTableComponent`, including, but not limited to, its parent component. + + ```js + { + type: ePropType.String, + name: 'name', + valueResolver: data => { + const restService = data.getInjected(RestService); + const usersComponent = data.getInjected(UsersComponent); + + // Use restService and usersComponent public props and methods here + }, + } + ``` + +### PropCallback\ + +`PropCallback` is the type of the callback function that can be passed to an `EntityProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type PropCallback = (data?: PropData) => R; +``` + +### PropPredicate\ + +`PropPredicate` is the type of the predicate function that can be passed to an `EntityProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type PropPredicate = (data?: PropData) => boolean; +``` + +### EntityPropOptions\ + +`EntityPropOptions` is the type that defines required and optional properties you have to pass in order to create an entity prop. + +Its type definition is as follows: + +```js +type EntityPropOptions = { + type: ePropType; + name: string; + displayName?: string; + valueResolver?: PropCallback>; + sortable?: boolean; + columnWidth?: number; + permission?: string; + visible?: PropPredicate; +}; +``` + +As you see, passing `type` and `name` is enough to create an entity prop. Here is what each property is good for: + +- **type** is the type of the prop value. It is used for custom rendering in the table. (_required_) +- **name** is the property name (or key) which will be used to read the value of the prop. (_required_) +- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`) +- **valueResolver** is a callback that is called when the cell is rendered. It must return an observable. (_default:_ `data => of(data.record[options.name])`) +- **sortable** defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (_default:_ `false`) +- **columnWidth** defines a minimum width for the column. Good for horizontal scroll. (_default:_ `undefined`) +- **permission** is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (_default:_ `() => true`) + +> Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be `undefined`. Second, if some cells are displayed and others are not, the table will be broken. Use the `valueResolver` and render an empty cell when you need to hide a specific cell. + +You may find a full example below. + +### EntityProp\ + +`EntityProp` is the class that defines your entity props. It takes an `EntityPropOptions` and sets the default values to the properties, creating an entity prop that can be passed to an entity contributor. + +```js +const options: EntityPropOptions = { + type: ePropType.String, + name: 'email', + displayName: 'AbpIdentity::EmailAddress', + valueResolver: data => { + const { email, emailConfirmed } = data.record; + + return of( + (email || '') + (emailConfirmed ? `` : ''), + ); + }, + sortable: true, + columnWidth: 250, + permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical + visible: data => { + const store = data.getInjected(Store); + const selectSensitiveDataVisibility = ConfigState.getSetting( + 'Abp.Identity.IsSensitiveDataVisible' // hypothetical + ); + + return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true'; + } +}; + +const prop = new EntityProp(options); +``` + +It also has two static methods to create its instances: + +- **EntityProp.create\\(options: EntityPropOptions\\)** is used to create an instance of `EntityProp`. + ```js + const prop = EntityProp.create(options); + ``` +- **EntityProp.createMany\\(options: EntityPropOptions\\[\]\)** is used to create multiple instances of `EntityProp` with given array of `EntityPropOptions`. + ```js + const props = EntityProp.createMany(optionsArray); + ``` + +### EntityPropList\ + +`EntityPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + propList: EntityPropList, +) { + // drop email node + const emailPropNode = propList.dropByValue( + 'AbpIdentity::EmailAddress', + (prop, text) => prop.text === text, + ); + + // add it back after phoneNumber + propList.addAfter( + emailPropNode.value, + 'phoneNumber', + (value, name) => value.name === name, + ); +} +``` + +### EntityPropContributorCallback\ + +`EntityPropContributorCallback` is the type that you can pass as entity prop contributor callbacks to static `forLazy` methods of the modules. + +```js +export function isLockedOutPropContributor( + propList: EntityPropList, +) { + // add isLockedOutProp as 2nd column + propList.add(isLockedOutProp).byIndex(1); +} + +export const identityEntityPropContributors = { + [eIdentityComponents.Users]: [isLockedOutPropContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Dynamic-Form-Extensions.md b/docs/en/UI/Angular/Dynamic-Form-Extensions.md new file mode 100644 index 0000000000..d7780ceec0 --- /dev/null +++ b/docs/en/UI/Angular/Dynamic-Form-Extensions.md @@ -0,0 +1,315 @@ +# Dynamic Form (or Form Prop) Extensions for Angular UI + + +## Introduction + +Form prop extension system allows you to add a new field to the create and/or edit forms for a form or change/remove an already existing one. A "Date of Birth" field was added to the user management page below: + +Form Prop Extension Example: 'Date of Birth' Field + +You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contibutor for an edit form. + +## How to Set Up + +In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../Modules/Identity.md) and validate it. + +### Step 1. Create Form Prop Contributors + +The following code prepares two constants named `identityCreateFormPropContributors` and `identityEditFormPropContributors`, ready to be imported and used in your root module: + +```js +// src/app/form-prop-contributors.ts + +import { + eIdentityComponents, + IdentityCreateFormPropContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { ePropType, FormProp, FormPropList } from '@abp/ng.theme.shared/extensions'; +import { Validators } from '@angular/forms'; + +const birthdayProp = new FormProp({ + type: ePropType.Date, + name: 'birthday', + displayName: 'AbpIdentity::Birthday', + validators: () => [Validators.required], +}); + +export function birthdayPropContributor(propList: FormPropList) { + propList.addByIndex(birthdayProp, 4); +} + +export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + birthdayPropContributor, + // You can add more contributors here + ], +}; + +export const identityEditFormPropContributors = identityCreateFormPropContributors; +// you may define different contributors for edit form if you like + +``` + + +The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addByIndex` method, which adds the given value to the specified index of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Form Prop Contributors + +Import `identityCreateFormPropContributors` and `identityEditFormPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { + identityCreateFormPropContributors, + identityEditFormPropContributors, +} from './form-prop-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + createFormPropContributors: identityCreateFormPropContributors, + editFormPropContributors: identityEditFormPropContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `IdentityModule`. + +## Object Extensions + +Extra properties defined on an existing entity will be included in the create and edit forms and validated based on their configuration. The form values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior. + +## API + +### PropData\ + +`PropData` is the shape of the parameter passed to all callbacks or predicates in a `FormProp`. + +It has the following properties: + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleFormPropComponent`, including, but not limited to, its parent components. + + ```js + { + type: ePropType.Enum, + name: 'myField', + options: data => { + const restService = data.getInjected(RestService); + const usersComponent = data.getInjected(UsersComponent); + + // Use restService and usersComponent public props and methods here + } + }, + ``` + +- **record** is the row data, i.e. current value of the selected item to edit. This property is _available only on edit forms_. + + ```js + { + type: ePropType.String, + name: 'myProp', + readonly: data => data.record.someOtherProp, + } + ``` + +### PropCallback\ + +`PropCallback` is the type of the callback function that can be passed to a `FormProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type PropCallback = (data?: PropData) => R; +``` + +### PropPredicate\ + +`PropPredicate` is the type of the predicate function that can be passed to a `FormProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type PropPredicate = (data?: PropData) => boolean; +``` + +### FormPropOptions\ + +`FormPropOptions` is the type that defines required and optional properties you have to pass in order to create a form prop. + +Its type definition is as follows: + +```js +type FormPropOptions = { + type: ePropType; + name: string; + displayName?: string; + id?: string; + permission?: string; + visible?: PropPredicate; + readonly?: PropPredicate; + disabled?: PropPredicate; + validators?: PropCallback; + asyncValidators?: PropCallback; + defaultValue?: boolean | number | string | Date; + options?: PropCallback[]>>; + autocomplete?: string; + isExtra? boolean; +}; +``` + +As you see, passing `type` and `name` is enough to create a form prop. Here is what each property is good for: + +- **type** is the type of the prop value. It defines which input is rendered for the prop in the form. (_required_) +- **name** is the property name (or key) which will be used to read the value of the prop. (_required_) +- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`) +- **id** will be set as the `for` attribute of the label and the `id` attribute of the input for the field. (_default:_ `options.name`) +- **permission** is the permission context which will be used to decide if a column for this form prop should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if this prop should be displayed on the form or not. (_default:_ `() => true`) +- **readonly** is a predicate that will be used to decide if this prop should be readonly or not. (_default:_ `() => false`) +- **disabled** is a predicate that will be used to decide if this prop should be disabled or not. (_default:_ `() => false`) +- **validators** is a callback that returns validators for the prop. (_default:_ `() => []`) +- **asyncValidators** is a callback that returns async validators for the prop. (_default:_ `() => []`) +- **defaultValue** is the initial value the field will have. (_default:_ `null`) +- **options** is a callback that is called when a dropdown is needed. It must return an observable. (_default:_ `undefined`) +- **autocomplete** will be set as the `autocomplete` attribute of the input for the field. Please check [possible values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#Values). (_default:_ `'off'`) +- **isExtra** indicates this prop is an object extension. When `true`, the value of the field will be mapped from and to `extraProperties` of the entity. (_default:_ `undefined`) + +> Important Note: Do not use `record` property of `PropData` in create form predicates and callbacks, because it will be `undefined`. You can use it on edit form contributors though. + +You may find a full example below. + +### FormProp\ + +`FormProp` is the class that defines your form props. It takes a `FormPropOptions` and sets the default values to the properties, creating a form prop that can be passed to a form contributor. + +```js +const options: FormPropOptions = { + type: ePropType.Enum, + name: 'myProp', + displayName: 'Default::MyPropName', + id: 'my-prop', + permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical + visible: data => { + const store = data.getInjected(Store); + const selectSensitiveDataVisibility = ConfigState.getSetting( + 'Abp.Identity.IsSensitiveDataVisible' // hypothetical + ); + + return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true'; + }, + readonly: data => data.record.someProp, + disabled: data => data.record.someOtherProp, + validators: () => [Validators.required], + asyncValidators: data => { + const http = data.getInjected(HttpClient); + + function validate(control: AbstractControl): Observable { + if (control.pristine) return of(null); + + return http + .get('https://api.my-brand.io/hypothetical/endpoint/' + control.value) + .pipe(map(response => (response.valid ? null : { invalid: true }))); + } + + return [validate]; + }, + defaultValue: 0, + options: data => { + const service = data.getInjected(MyIdentityService); + + return service.getMyPropOptions() + .pipe( + map(({items}) => items.map( + item => ({key: item.name, value: item.id }) + )), + ); + }, + autocomplete: 'off', + isExtra: true, +}; + +const prop = new FormProp(options); +``` + +It also has two static methods to create its instances: + +- **FormProp.create\\(options: FormPropOptions\\)** is used to create an instance of `FormProp`. + ```js + const prop = FormProp.create(options); + ``` +- **FormProp.createMany\\(options: FormPropOptions\\[\]\)** is used to create multiple instances of `FormProp` with given array of `FormPropOptions`. + ```js + const props = FormProp.createMany(optionsArray); + ``` + +### FormPropList\ + +`FormPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + propList: FormPropList, +) { + // drop email node + const emailPropNode = propList.dropByValue( + 'AbpIdentity::EmailAddress', + (prop, displayName) => prop.displayName === displayName, + ); + + // add it back after phoneNumber + propList.addAfter( + emailPropNode.value, + 'phoneNumber', + (value, name) => value.name === name, + ); +} +``` + +### CreateFormPropContributorCallback\ + +`CreateFormPropContributorCallback` is the type that you can pass as **create form** prop contributor callbacks to static `forLazy` methods of the modules. + +```js +export function myPropCreateContributor( + propList: FormPropList, +) { + // add myProp as 2nd field from the start + propList.add(myProp).byIndex(1); +} + +export const identityCreateFormPropContributors = { + [eIdentityComponents.Users]: [myPropCreateContributor], +}; +``` + +### EditFormPropContributorCallback\ + +`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `forLazy` methods of the modules. + +```js +export function myPropEditContributor( + propList: FormPropList, +) { + // add myProp as 2nd field from the end + propList.add(myProp).byIndex(-1); +} + +export const identityEditFormPropContributors = { + [eIdentityComponents.Users]: [myPropEditContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Entity-Action-Extensions.md b/docs/en/UI/Angular/Entity-Action-Extensions.md new file mode 100644 index 0000000000..fd22e293fd --- /dev/null +++ b/docs/en/UI/Angular/Entity-Action-Extensions.md @@ -0,0 +1,400 @@ +# Entity Action Extensions for Angular UI + +## Introduction + +Entity action extension system allows you to add a new action to the action menu for an entity. A "Click Me" action was added to the user management page below: + +Entity Action Extension Example: 'Click Me!' Action + +You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access the current entity in your code. + +## How to Set Up + +In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../Modules/Identity.md). + +### Step 1. Create Entity Action Contributors + +The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module: + +```js +// src/app/entity-action-contributors.ts + +import { + eIdentityComponents, + IdentityEntityActionContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions'; + +const alertUserName = new EntityAction({ + text: 'Click Me!', + action: data => { + // Replace alert with your custom code + alert(data.record.userName); + }, + // See EntityActionOptions in API section for all options +}); + +export function alertUserNameContributor(actionList: EntityActionList) { + actionList.addTail(alertUserName); +} + +export const identityEntityActionContributors: IdentityEntityActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + alertUserNameContributor, + // You can add more contributors here + ], +}; +``` + +The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addTail` method, which adds the given value to the end of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Entity Action Contributors + +Import `identityEntityActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityEntityActionContributors } from './entity-action-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + entityActionContributors: identityEntityActionContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the "Users" page (`UsersComponent`) of the `IdentityModule`. + +## How to Place a Custom Modal and Trigger It by Entity Actions + +Let's employ dependency injection to extend the functionality of `IdentityModule` and add a quick view action for the User entity. We will take a lazy-loaded approach. + +Entity Action Extension Example: Custom Modal + +1. Create a folder at this path: `src/app/identity-extended` + +2. Add an entity action similar to this: + ```js + // src/app/identity-extended/entity-action-contributors.ts + + import { + eIdentityComponents, + IdentityEntityActionContributors, + IdentityUserDto, + } from '@abp/ng.identity'; + import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions'; + import { IdentityExtendedComponent } from './identity-extended.component'; + + const quickViewAction = new EntityAction({ + text: 'Quick View', + action: data => { + const component = data.getInjected(IdentityExtendedComponent); + component.openUserQuickView(data.record); + }, + }); + + export function customModalContributor(actionList: EntityActionList) { + actionList.addTail(quickViewAction); + } + + export const identityEntityActionContributors: IdentityEntityActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + customModalContributor, + // You can add more contributors here + ], + }; + ``` + +3. Create a parent component to the identity module. + ```js + // src/app/identity-extended/identity-extended.component.ts + + import { IdentityUserDto } from '@abp/ng.identity'; + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-identity-extended', + templateUrl: './identity-extended.component.html', + }) + export class IdentityExtendedComponent { + isUserQuickViewVisible: boolean; + + user: IdentityUserDto; + + openUserQuickView(record: IdentityUserDto) { + this.user = new Proxy(record, { + get: (target, prop) => target[prop] || '—', + }); + this.isUserQuickViewVisible = true; + } + } + ``` + +4. Add a router outlet and a modal to the parent component. + ```html + + + + + + +

{%{{{ user.userName }}}%}

+
+ + + + + + + + + + + + + + + + + + + + + +
{%{{{ 'AbpIdentity::DisplayName:Name' | abpLocalization }}}%}{%{{{ user.name }}}%}
{%{{{ 'AbpIdentity::DisplayName:Surname' | abpLocalization }}}%}{%{{{ user.surname }}}%}
{%{{{ 'AbpIdentity::EmailAddress' | abpLocalization }}}%}{%{{{ user.email }}}%}
{%{{{ 'AbpIdentity::PhoneNumber' | abpLocalization }}}%}{%{{{ user.phoneNumber }}}%}
+
+ + + + +
+ ``` + +5. Add a module for the component and load `IdentityModule` as seen below: + ```js + // src/app/identity-extended/identity-extended.module.ts + + import { CoreModule } from '@abp/ng.core'; + import { IdentityModule } from '@abp/ng.identity'; + import { ThemeSharedModule } from '@abp/ng.theme.shared'; + import { NgModule } from '@angular/core'; + import { RouterModule } from '@angular/router'; + import { identityEntityActionContributors } from './entity-action-contributors'; + import { IdentityExtendedComponent } from './identity-extended.component'; + + @NgModule({ + imports: [ + CoreModule, + ThemeSharedModule, + RouterModule.forChild([ + { + path: '', + component: IdentityExtendedComponent, + children: [ + { + path: '', + loadChildren: () => + IdentityModule.forLazy({ + entityActionContributors: identityEntityActionContributors, + }), + }, + ], + }, + ]), + ], + declarations: [IdentityExtendedComponent], + }) + export class IdentityExtendedModule {} + ``` + +6. Load `IdentityExtendedModule` instead of `IdentityModule` in your root routing module. + ```js + // src/app/app-routing.module.ts + + const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('./identity-extended/identity-extended.module') + .then(m => m.IdentityExtendedModule), + }, + + // other routes + ]; + ``` + +That's it. As you see, we reached the `IdentityExtendedComponent` through dependency injection and called one of its methods in our action. The specific user was also available via `data.record`, so we were able to display a summary view. + +## API + +### ActionData\ + +`ActionData` is the shape of the parameter passed to all callbacks or predicates in an `EntityAction`. + +It has the following properties: + +- **record** is the row data, i.e. current value rendered in the table. + + ```js + { + text: 'Click Me!', + action: data => { + alert(data.record.userName); + }, + } + ``` + +- **index** is the table index where the record is at. + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `GridActionsComponent`, including, but not limited to, its parent component. + + ```js + { + text: 'Click Me!', + action: data => { + const restService = data.getInjected(RestService); + + // Use restService public props and methods here + }, + visible: data => { + const usersComponent = data.getInjected(UsersComponent); + + // Use usersComponent public props and methods here + }, + } + ``` + +### ActionCallback\ + +`ActionCallback` is the type of the callback function that can be passed to an `EntityAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type ActionCallback = (data?: ActionData) => R; +``` + +### ActionPredicate\ + +`ActionPredicate` is the type of the predicate function that can be passed to an `EntityAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type ActionPredicate = (data?: ActionData) => boolean; +``` + +### EntityActionOptions\ + +`EntityActionOptions` is the type that defines required and optional properties you have to pass in order to create an entity action. + +Its type definition is as follows: + +```js +type EntityActionOptions = { + action: ActionCallback, + text: string, + icon?: string, + permission?: string, + visible?: ActionPredicate, +}; +``` + +As you see, passing `action` and `text` is enough to create an entity action. Here is what each property is good for: + +- **action** is a callback that is called when the grid action is clicked. (_required_) +- **text** is the button text which will be localized. (_required_) +- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`) +- **permission** is the permission context which will be used to decide if this type of grid action should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if the current record should have this grid action or not. (_default:_ `() => true`) + +You may find a full example below. + +### EntityAction\ + +`EntityAction` is the class that defines your entity actions. It takes an `EntityActionOptions` and sets the default values to the properties, creating an entity action that can be passed to an entity contributor. + +```js +const options: EntityActionOptions = { + action: data => { + const component = data.getInjected(IdentityExtendedComponent); + component.unlock(data.record.id); + }, + text: 'AbpIdentity::Unlock', + icon: 'fa fa-unlock', + permission: 'AbpIdentity.Users.Update', + visible: data => data.record.isLockedOut, +}; + +const action = new EntityAction(options); +``` + +It also has two static methods to create its instances: + +- **EntityAction.create\\(options: EntityActionOptions\\)** is used to create an instance of `EntityAction`. + ```js + const action = EntityAction.create(options); + ``` +- **EntityAction.createMany\\(options: EntityActionOptions\\[\]\)** is used to create multiple instances of `EntityAction` with given array of `EntityActionOptions`. + ```js + const actions = EntityAction.createMany(optionsArray); + ``` + +### EntityActionList\ + +`EntityActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + actionList: EntityActionList, +) { + // drop "Unlock" button + const unlockActionNode = actionList.dropByValue( + 'AbpIdentity::Unlock', + (action, text) => action.text === text, + ); + + // add it back to the head of the list + actionList.addHead(unlockActionNode.value); +} +``` + +### EntityActionContributorCallback\ + +`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `forLazy` methods of the modules. + +```js +// lockUserContributor should have EntityActionContributorCallback type + +export function lockUserContributor( + actionList: EntityActionList, +) { + // add lockUser as 3rd action + actionList.add(lockUser).byIndex(2); +} + +export const identityEntityActionContributors = { + [eIdentityComponents.Users]: [lockUserContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Form-Validation.md b/docs/en/UI/Angular/Form-Validation.md index 38e7969eea..a178fa213c 100644 --- a/docs/en/UI/Angular/Form-Validation.md +++ b/docs/en/UI/Angular/Form-Validation.md @@ -6,23 +6,52 @@ Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npm ## How to Add New Error Messages -You can add a new error message by providing the `VALIDATION_BLUEPRINTS` injection token from your root module. +You can add a new error message by passing validation options to the `ThemeSharedModule` in your root module. ```js import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; @NgModule({ + imports: [ + ThemeSharedModule.forRoot({ + validation: { + blueprints: { + uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", + }, + }, + + // rest of theme shared config + }), + + // other imports + ], + // rest of the module metadata +}) +export class AppModule {} +``` + +Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ providers: [ - // other providers { provide: VALIDATION_BLUEPRINTS, useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]", }, }, + + // other providers ], + + // rest of the module metadata }) export class AppModule {} ``` @@ -40,7 +69,7 @@ In this example; ## How to Change Existing Error Messages -You can overwrite an existing error message by providing `VALIDATION_BLUEPRINTS` injection token from your root module. Let's imagine you have a custom localization resource for required inputs. +You can overwrite an existing error message by passing validation options to the `ThemeSharedModule` in your root module. Let's imagine you have a custom localization resource for required inputs. ```json "RequiredInput": "Oops! We need this input." @@ -50,19 +79,48 @@ To use this instead of the built-in required input message, all you need to do i ```js import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; @NgModule({ + imports: [ + ThemeSharedModule.forRoot({ + validation: { + blueprints: { + required: "::RequiredInput", + }, + }, + + // rest of theme shared config + }), + + // other imports + ], + // rest of the module metadata +}) +export class AppModule {} +``` + +Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work. +```js +import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core"; +import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared"; + +@NgModule({ providers: [ - // other providers { provide: VALIDATION_BLUEPRINTS, useValue: { + ...DEFAULT_VALIDATION_BLUEPRINTS, required: "::RequiredInput", }, }, + + // other providers ], + + // rest of the module metadata }) export class AppModule {} ``` diff --git a/docs/en/UI/Angular/List-Service.md b/docs/en/UI/Angular/List-Service.md index 016b891159..03e2b5341a 100644 --- a/docs/en/UI/Angular/List-Service.md +++ b/docs/en/UI/Angular/List-Service.md @@ -1,6 +1,6 @@ # Working with Lists -`ListService` is a utility service to provide an easy pagination, sorting, and search implementation. +`ListService` is a utility service to provide easy pagination, sorting, and search implementation. @@ -35,7 +35,10 @@ class BookComponent { constructor( public readonly list: ListService, private bookService: BookService, - ) {} + ) { + // change ListService defaults here + this.list.maxResultCount = 20; + } ngOnInit() { // A function that gets query and returns an observable @@ -164,7 +167,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide ``` -> We do not recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page. +> We do not recommend using the NGXS store for CRUD pages unless your application needs to share list information between components or use it later on in another page. ## How to Refresh Table on Create/Update/Delete @@ -186,7 +189,7 @@ You may use observables in combination with [AsyncPipe](https://angular.io/guide this.store.dispatch(new DeleteBook(id)).subscribe(this.list.get); ``` -> We donot recommend using NGXS store for CRUD pages, unless your application needs to share list information between components or use it later on in another page. +> We do not recommend using the NGXS store for CRUD pages unless your application needs to share list information between components or use it later on in another page. ## How to Implement Server-Side Search in a Table @@ -210,7 +213,7 @@ We had to modify the `ListService` to make it work with `ngx-datatable`. Previou > ``` -As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for inital page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change: +As of v3.0, with ngx-datatable, the `page` property has to be set as `0` for the initial page. Therefore, if you used `ListService` on your tables before and are going to keep `abp-table`, you need to make the following change: ```html diff --git a/docs/en/UI/Angular/Page-Toolbar-Extensions.md b/docs/en/UI/Angular/Page-Toolbar-Extensions.md new file mode 100644 index 0000000000..7e63950224 --- /dev/null +++ b/docs/en/UI/Angular/Page-Toolbar-Extensions.md @@ -0,0 +1,402 @@ +# Page Toolbar Extensions for Angular UI + +## Introduction + +Page toolbar extension system allows you to add a new action to the toolbar of a page. A "Click Me" action was added to the user management page below: + +Page Toolbar Extension Example: 'Click Me!' Action + +You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button. + +## How to Add an Action to Page Toolbar + +In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console. + +### Step 1. Create Toolbar Action Contributors + +The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module: + +```js +// src/app/toolbar-action-contributors.ts + +import { + eIdentityComponents, + IdentityToolbarActionContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { ToolbarAction, ToolbarActionList } from '@abp/ng.theme.shared/extensions'; + +const logUserNames = new ToolbarAction({ + text: 'Click Me!', + action: data => { + // Replace log with your custom code + data.record.forEach(user => console.log(user.userName)); + }, + // See ToolbarActionOptions in API section for all options +}); + +export function logUserNamesContributor(actionList: ToolbarActionList) { + actionList.addHead(logUserNames); +} + +export const identityToolbarActionContributors: IdentityToolbarActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + logUserNamesContributor, + // You can add more contributors here + ], +}; + +``` + +The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 2. Import and Use Toolbar Action Contributors + +Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityToolbarActionContributors } from './toolbar-action-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + toolbarActionContributors: identityToolbarActionContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule`. + +## How to Add a Custom Component to Page Toolbar + +In this example, we will add a custom "Click Me!" button and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console. + +Page Toolbar Extension Example: Custom Component + +### Step 1. Create A Custom Component + +We need to have a component before we can pass it to the toolbar action contributors: + +```js +// src/app/click-me-button.component.ts + +import { IdentityUserDto } from '@abp/ng.identity'; +import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.theme.shared/extensions'; +import { Component, Inject } from '@angular/core'; + +@Component({ + selector: 'app-click-me-button', + template: ``, +}) +export class ClickMeButtonComponent { + constructor( + @Inject(EXTENSIONS_ACTION_DATA) + private data: ActionData + ) {} + + handleClick() { + this.data.record.forEach(user => console.log(user.userName)); + } +} + +``` + +Here, `EXTENSIONS_ACTION_DATA` token provides us the context from the page toolbar. Therefore, we are able to reach the page data via `record`, which is an array of users, i.e. `IdentityUserDto[]`. + +> We could also import `EXTENSIONS_ACTION_CALLBACK` from **@abp/ng.theme.shared/extensions** package, which is a higher order function that triggers the predefined `action` when called. It passes `ActionData` as the first parameter, so you do not have to pass it explicitly. In other words, `EXTENSIONS_ACTION_CALLBACK` can be called without any parameters and it will not fail. + +### Step 2. Create Toolbar Action Contributors + +The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in: + +```js +// src/app/toolbar-action-contributors.ts + +import { + eIdentityComponents, + IdentityToolbarActionContributors, + IdentityUserDto, +} from '@abp/ng.identity'; +import { ToolbarActionList, ToolbarComponent } from '@abp/ng.theme.shared/extensions'; +import { ClickMeButtonComponent } from './click-me-button.component'; + +const logUserNames = new ToolbarComponent({ + component: ClickMeButtonComponent, + // See ToolbarActionOptions in API section for all options +}); + +export function logUserNamesContributor(actionList: ToolbarActionList) { + actionList.addHead(logUserNames); +} + +export const identityToolbarActionContributors: IdentityToolbarActionContributors = { + // enum indicates the page to add contributors to + [eIdentityComponents.Users]: [ + logUserNamesContributor, + // You can add more contributors here + ], +}; + +``` + +The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md). + +### Step 3. Import and Use Toolbar Action Contributors + +Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below. + +```js +// src/app/app-routing.module.ts + +// other imports +import { identityToolbarActionContributors } from './toolbar-action-contributors'; + +const routes: Routes = [ + // other routes + + { + path: 'identity', + loadChildren: () => + import('@abp/ng.identity').then(m => + m.IdentityModule.forLazy({ + toolbarActionContributors: identityToolbarActionContributors, + }) + ), + }, + + // other routes +]; +``` + +That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule` and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components. + +## How to Place a Custom Modal and Trigger It by Toolbar Actions + +Please check the same topic in [entity action extensions document](Entity-Action-Extensions.md) and replace entity action with a toolbar action. + +## API + +### ActionData\ + +`ActionData` is the shape of the parameter passed to all callbacks or predicates in a `ToolbarAction`. + +It has the following properties: + +- **record** is the page data, the main record on a page, usually an entity list (e.g. list of users). + + ```js + { + text: 'Click Me!', + action: data => { + data.record.forEach(user => { + console.lof(user.userName); + }); + }, + } + ``` + +- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `PageToolbarComponent`, including, but not limited to, its parent component. + + ```js + { + text: 'Click Me!', + action: data => { + const restService = data.getInjected(RestService); + + // Use restService public props and methods here + }, + visible: data => { + const usersComponent = data.getInjected(UsersComponent); + + // Use usersComponent public props and methods here + }, + } + ``` + +### ActionCallback\ + +`ActionCallback` is the type of the callback function that can be passed to a `ToolbarAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation: + +```js +type ActionCallback = (data?: ActionData) => R; +``` + +### ActionPredicate\ + +`ActionPredicate` is the type of the predicate function that can be passed to a `ToolbarAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation: + +```js +type ActionPredicate = (data?: ActionData) => boolean; +``` + +### ToolbarActionOptions\ + +`ToolbarActionOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar action. + +Its type definition is as follows: + +```js +type ToolbarActionOptions = { + action: ActionCallback, + text: string, + icon?: string, + permission?: string, + visible?: ActionPredicate, +}; +``` + +As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for: + +- **action** is a callback that is called when the toolbar action is clicked. (_required_) +- **text** is the button text which will be localized. (_required_) +- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`) +- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`) + +You may find a full example below. + +### ToolbarAction\ + +`ToolbarAction` is the class that defines your toolbar actions. It takes an `ToolbarActionOptions` and sets the default values to the properties, creating an toolbar action that can be passed to an toolbar contributor. + +```js +const options: ToolbarActionOptions = { + action: data => { + const service = data.getInjected(MyCustomIdentityService); + const lockedUsers = data.record.filter(user => user.isLockedOut); + service.unlockAll(lockedUsers); + }, + text: 'MyProjectName::UnlockAll', + icon: 'fa fa-unlock', + permission: 'AbpIdentity.Users.Update', + visible: data => data.record.some(user => user.isLockedOut), +}; + +const action = new ToolbarAction(options); +``` + +It also has two static methods to create its instances: + +- **ToolbarAction.create\\(options: ToolbarActionOptions\\)** is used to create an instance of `ToolbarAction`. + ```js + const action = ToolbarAction.create(options); + ``` +- **ToolbarAction.createMany\\(options: ToolbarActionOptions\\[\]\)** is used to create multiple instances of `ToolbarAction` with given array of `ToolbarActionOptions`. + +### ToolbarComponentOptions\ + +`ToolbarComponentOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar component. + +Its type definition is as follows: + +```js +type ToolbarComponentOptions = { + component: Type, + action?: ActionCallback, + permission?: string, + visible?: ActionPredicate, +}; +``` + +As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for: + +- **component** is the constructor of the component to be projected. (_required_) +- **action** is a predefined callback that you can reach in your component via `EXTENSIONS_ACTION_CALLBACK` token and trigger. (_optional_) +- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`) +- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`) + +You may find a full example below. + +### ToolbarComponent\ + +`ToolbarComponent` is the class that defines toolbar actions which project a custom component. It takes an `ToolbarComponentOptions` and sets the default values to the properties, creating a toolbar action that can be passed to an toolbar contributor. + +```js +const options: ToolbarComponentOptions = { + component: UnlockAllButton, + action: data => { + const service = data.getInjected(MyCustomIdentityService); + const lockedUsers = data.record.filter(user => user.isLockedOut); + service.unlockAll(lockedUsers); + }, + permission: 'AbpIdentity.Users.Update', + visible: data => data.record.some(user => user.isLockedOut), +}; + +const action = new ToolbarComponent(options); +``` + +It also has two static methods to create its instances: + +- **ToolbarComponent.create\\(options: ToolbarComponentOptions\\)** is used to create an instance of `ToolbarComponent`. + ```js + const action = ToolbarComponent.create(options); + ``` +- **ToolbarComponent.createMany\\(options: ToolbarComponentOptions\\[\]\)** is used to create multiple instances of `ToolbarComponent` with given array of `ToolbarComponentOptions`. + ```js + const actions = ToolbarComponent.createMany(optionsArray); + ``` + +### ToolbarActionList\ + +`ToolbarActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md). + +The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this: + +```js +export function reorderUserContributors( + actionList: ToolbarActionList, +) { + // drop "New User" button + const newUserActionNode = actionList.dropByValue( + 'AbpIdentity::NewUser', + (action, text) => action['text'] === text, + ); + + // add it back to the head of the list + actionList.addHead(newUserActionNode.value); +} + +export const identityEntityActionContributors = { + [eIdentityComponents.Users]: [ + logUserNamesContributor, + reorderUserContributors, + ], +}; +``` + +### ToolbarActionContributorCallback\ + +`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `forLazy` methods of the modules. + +```js +// exportUsersContributor should have ToolbarActionContributorCallback type + +export function exportUsersContributor( + actionList: ToolbarActionList, +) { + // add exportUsers just before the last action + actionList.add(exportUsers).byIndex(-1); +} + +export const identityEntityActionContributors = { + [eIdentityComponents.Users]: [exportUsersContributor], +}; +``` + +## See Also + +- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md) diff --git a/docs/en/UI/Angular/Testing.md b/docs/en/UI/Angular/Testing.md index f24ae62982..7b93d48585 100644 --- a/docs/en/UI/Angular/Testing.md +++ b/docs/en/UI/Angular/Testing.md @@ -1,3 +1,376 @@ -# Angular UI: Testing +# Unit Testing Angular UI -TODO \ No newline at end of file +ABP Angular UI is tested like any other Angular application. So, [the guide here](https://angular.io/guide/testing) applies to ABP too. That said, we would like to point out some **unit testing topics specific to ABP Angular applications**. + +## Setup + +In Angular, unit tests use [Karma](https://karma-runner.github.io/) and [Jasmine](https://jasmine.github.io) by default. Although we like Jest more, we chose not to deviate from these defaults, so **the application template you download will have Karma and Jasmine preconfigured**. You can find the Karma configuration inside the _karma.conf.js_ file in the root folder. You don't have to do anything. Adding a spec file and running `npm test` will work. + +## Basics + +An over-simplified spec file looks like this: + +```ts +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { MyComponent } from "./my.component"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [MyComponent], + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + ], + }).compileComponents(); + }) + ); + + beforeEach(() => { + fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); +}); +``` + +If you take a look at the imports, you will notice that we have prepared some testing modules to replace built-in ABP modules. This is necessary for providing mocks for some features which otherwise would break your tests. Please remember to **use testing modules** and **call their `withConfig` static method**. + +## Tips + +### Angular Testing Library + +Although you can test your code with Angular TestBed, you may find [Angular Testing Library](https://testing-library.com/docs/angular-testing-library/intro) a good alternative. + +The simple example above can be written with Angular Testing Library as follows: + +```ts +import { CoreTestingModule } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture } from "@angular/core/testing"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { render } from "@testing-library/angular"; +import { MyComponent } from "./my.component"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + const result = await render(MyComponent, { + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + ], + providers: [ + /* mock providers here */ + ], + }); + + fixture = result.fixture; + }); + + it("should be initiated", () => { + expect(fixture.componentInstance).toBeTruthy(); + }); +}); +``` + +Very similar, as you can see. The real difference kicks in when we use queries and fire events. + +```ts +// other imports +import { getByLabelText, screen } from "@testing-library/angular"; +import userEvent from "@testing-library/user-event"; + +describe("MyComponent", () => { + beforeEach(/* removed for sake of brevity */); + + it("should display advanced filters", () => { + const filters = screen.getByTestId("author-filters"); + const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; + expect(nameInput.offsetWidth).toBe(0); + + const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); + userEvent.click(advancedFiltersBtn); + + expect(nameInput.offsetWidth).toBeGreaterThan(0); + + userEvent.type(nameInput, "fooo{backspace}"); + expect(nameInput.value).toBe("foo"); + }); +}); +``` + +The **queries in Angular Testing Library follow practices for maintainable tests**, the user event package provides a **human-like interaction** with the DOM, and the library in general has **a clear API** that simplifies component testing. Please find some useful links below: + +- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries) +- [User Event](https://testing-library.com/docs/ecosystem-user-event) +- [Examples](https://github.com/testing-library/angular-testing-library/tree/master/apps/example-app/app/examples) + +### Clearing DOM After Each Spec + +One thing to remember is that Karma runs tests in real browser instances. That means, you will be able to see the result of your test code, but also have problems with components attached to the document body which may not get cleared after each test, even when you configure Karma to do so. + +We have prepared a simple function with which you can clear any leftover DOM elements after each test. + +```ts +// other imports +import { clearPage } from "@abp/ng.core/testing"; + +describe("MyComponent", () => { + let fixture: ComponentFixture; + + afterEach(() => clearPage(fixture)); + + beforeEach(async () => { + const result = await render(MyComponent, { + /* removed for sake of brevity */ + }); + fixture = result.fixture; + }); + + // specs here +}); +``` + +Please make sure you use it because Karma will fail to remove dialogs otherwise and you will have multiple copies of modals, confirmation boxes, and alike. + +### Waiting + +Some components, modals, in particular, work off-detection-cycle. In other words, you cannot reach DOM elements inserted by these components immediately after opening them. Similarly, inserted elements are not immediately destroyed upon closing them. + +For this purpose, we have prepared a `wait` function. + +```ts +// other imports +import { wait } from "@abp/ng.core/testing"; + +describe("MyComponent", () => { + beforeEach(/* removed for sake of brevity */); + + it("should open a modal", async () => { + const openModalBtn = screen.getByRole("button", { name: "Open Modal" }); + userEvent.click(openModalBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + + expect(modal).toBeTruthy(); + + /* wait again after closing the modal */ + }); +}); +``` + +The `wait` function takes a second parameter, i.e. timeout (default: `0`). Try not to use it though. Using a timeout bigger than `0` is usually a signal that something is not quite right. + +## Testing Example + +Here is an example test suite. It doesn't cover all, but gives quite a good idea about what the testing experience will be like. + +```ts +import { clearPage, CoreTestingModule, wait } from "@abp/ng.core/testing"; +import { ThemeBasicTestingModule } from "@abp/ng.theme.basic/testing"; +import { ThemeSharedTestingModule } from "@abp/ng.theme.shared/testing"; +import { ComponentFixture } from "@angular/core/testing"; +import { + NgbCollapseModule, + NgbDatepickerModule, + NgbDropdownModule, +} from "@ng-bootstrap/ng-bootstrap"; +import { NgxValidateCoreModule } from "@ngx-validate/core"; +import { CountryService } from "@proxy/countries"; +import { + findByText, + getByLabelText, + getByRole, + getByText, + queryByRole, + render, + screen, +} from "@testing-library/angular"; +import userEvent from "@testing-library/user-event"; +import { BehaviorSubject, of } from "rxjs"; +import { CountryComponent } from "./country.component"; + +const list$ = new BehaviorSubject({ + items: [{ id: "ID_US", name: "United States of America" }], + totalCount: 1, +}); + +describe("Country", () => { + let fixture: ComponentFixture; + + afterEach(() => clearPage(fixture)); + + beforeEach(async () => { + const result = await render(CountryComponent, { + imports: [ + CoreTestingModule.withConfig(), + ThemeSharedTestingModule.withConfig(), + ThemeBasicTestingModule.withConfig(), + NgxValidateCoreModule, + NgbCollapseModule, + NgbDatepickerModule, + NgbDropdownModule, + ], + providers: [ + { + provide: CountryService, + useValue: { + getList: () => list$, + }, + }, + ], + }); + + fixture = result.fixture; + }); + + it("should display advanced filters", () => { + const filters = screen.getByTestId("country-filters"); + const nameInput = getByLabelText(filters, /name/i) as HTMLInputElement; + expect(nameInput.offsetWidth).toBe(0); + + const advancedFiltersBtn = screen.getByRole("link", { name: /advanced/i }); + userEvent.click(advancedFiltersBtn); + + expect(nameInput.offsetWidth).toBeGreaterThan(0); + + userEvent.type(nameInput, "fooo{backspace}"); + expect(nameInput.value).toBe("foo"); + + userEvent.click(advancedFiltersBtn); + expect(nameInput.offsetWidth).toBe(0); + }); + + it("should have a heading", () => { + const heading = screen.getByRole("heading", { name: "Countries" }); + expect(heading).toBeTruthy(); + }); + + it("should render list in table", async () => { + const table = await screen.findByTestId("country-table"); + + const name = getByText(table, "United States of America"); + expect(name).toBeTruthy(); + }); + + it("should display edit modal", async () => { + const actionsBtn = screen.queryByRole("button", { name: /actions/i }); + userEvent.click(actionsBtn); + + const editBtn = screen.getByRole("button", { name: /edit/i }); + userEvent.click(editBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const modalHeading = queryByRole(modal, "heading", { name: /edit/i }); + expect(modalHeading).toBeTruthy(); + + const closeBtn = getByText(modal, "×"); + userEvent.click(closeBtn); + + await wait(fixture); + + expect(screen.queryByRole("dialog")).toBeFalsy(); + }); + + it("should display create modal", async () => { + const newBtn = screen.getByRole("button", { name: /new/i }); + userEvent.click(newBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const modalHeading = queryByRole(modal, "heading", { name: /new/i }); + + expect(modalHeading).toBeTruthy(); + }); + + it("should validate required name field", async () => { + const newBtn = screen.getByRole("button", { name: /new/i }); + userEvent.click(newBtn); + + await wait(fixture); + + const modal = screen.getByRole("dialog"); + const nameInput = getByRole(modal, "textbox", { + name: /^name/i, + }) as HTMLInputElement; + + userEvent.type(nameInput, "x"); + userEvent.type(nameInput, "{backspace}"); + + const nameError = await findByText(modal, /required/i); + expect(nameError).toBeTruthy(); + }); + + it("should delete a country", () => { + const getSpy = spyOn(fixture.componentInstance.list, "get"); + const deleteSpy = jasmine.createSpy().and.returnValue(of(null)); + fixture.componentInstance.service.delete = deleteSpy; + + const actionsBtn = screen.queryByRole("button", { name: /actions/i }); + userEvent.click(actionsBtn); + + const deleteBtn = screen.getByRole("button", { name: /delete/i }); + userEvent.click(deleteBtn); + + const confirmText = screen.getByText("AreYouSure"); + expect(confirmText).toBeTruthy(); + + const confirmBtn = screen.getByRole("button", { name: "Yes" }); + userEvent.click(confirmBtn); + + expect(deleteSpy).toHaveBeenCalledWith(list$.value.items[0].id); + expect(getSpy).toHaveBeenCalledTimes(1); + }); +}); +``` + +## CI Configuration + +You would need a different configuration for your CI environment. To set up a new configuration for your unit tests, find the test project in _angular.json_ file and add one as seen below: + +```json +// angular.json + +"test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { /* several options here */ }, + "configurations": { + "production": { + "karmaConfig": "karma.conf.prod.js" + } + } +} +``` + +Now you can copy the _karma.conf.js_ as _karma.conf.prod.js_ and use any configuration you like in it. Please check [Karma configuration file document](http://karma-runner.github.io/5.2/config/configuration-file.html) for config options. + +Finally, don't forget to run your CI tests with the following command: + +```sh +npm test -- --prod +``` diff --git a/docs/en/UI/Angular/images/entity-action-extensions---click-me.gif b/docs/en/UI/Angular/images/entity-action-extensions---click-me.gif new file mode 100644 index 0000000000..6f5191874d Binary files /dev/null and b/docs/en/UI/Angular/images/entity-action-extensions---click-me.gif differ diff --git a/docs/en/UI/Angular/images/entity-action-extensions---custom-modal.gif b/docs/en/UI/Angular/images/entity-action-extensions---custom-modal.gif new file mode 100644 index 0000000000..0b1010e029 Binary files /dev/null and b/docs/en/UI/Angular/images/entity-action-extensions---custom-modal.gif differ diff --git a/docs/en/UI/Angular/images/entity-prop-extensions---custom-cell.gif b/docs/en/UI/Angular/images/entity-prop-extensions---custom-cell.gif new file mode 100644 index 0000000000..2f3d79f701 Binary files /dev/null and b/docs/en/UI/Angular/images/entity-prop-extensions---custom-cell.gif differ diff --git a/docs/en/UI/Angular/images/entity-prop-extensions---name-column.gif b/docs/en/UI/Angular/images/entity-prop-extensions---name-column.gif new file mode 100644 index 0000000000..7c8be58759 Binary files /dev/null and b/docs/en/UI/Angular/images/entity-prop-extensions---name-column.gif differ diff --git a/docs/en/UI/Angular/images/form-prop-extensions---birthday-field.gif b/docs/en/UI/Angular/images/form-prop-extensions---birthday-field.gif new file mode 100644 index 0000000000..dd19f834e0 Binary files /dev/null and b/docs/en/UI/Angular/images/form-prop-extensions---birthday-field.gif differ diff --git a/docs/en/UI/Angular/images/toolbar-action-extensions---click-me.gif b/docs/en/UI/Angular/images/toolbar-action-extensions---click-me.gif new file mode 100644 index 0000000000..b6585184f1 Binary files /dev/null and b/docs/en/UI/Angular/images/toolbar-action-extensions---click-me.gif differ diff --git a/docs/en/UI/Angular/images/toolbar-action-extensions---custom-component.gif b/docs/en/UI/Angular/images/toolbar-action-extensions---custom-component.gif new file mode 100644 index 0000000000..9775dbd1f0 Binary files /dev/null and b/docs/en/UI/Angular/images/toolbar-action-extensions---custom-component.gif differ diff --git a/docs/en/UI/Angular/images/user-action-extension-click-me-ng.png b/docs/en/UI/Angular/images/user-action-extension-click-me-ng.png new file mode 100644 index 0000000000..675f05217a Binary files /dev/null and b/docs/en/UI/Angular/images/user-action-extension-click-me-ng.png differ diff --git a/docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png b/docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png new file mode 100644 index 0000000000..fcd36e4a7b Binary files /dev/null and b/docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png differ diff --git a/docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png b/docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png new file mode 100644 index 0000000000..d8fd054eda Binary files /dev/null and b/docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png differ diff --git a/docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png b/docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png new file mode 100644 index 0000000000..948c4a9fd5 Binary files /dev/null and b/docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png differ diff --git a/docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png b/docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png new file mode 100644 index 0000000000..82d72b40ec Binary files /dev/null and b/docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png differ diff --git a/docs/en/UI/AspNetCore/Branding.md b/docs/en/UI/AspNetCore/Branding.md index e4a9ee0f3b..aa1500401d 100644 --- a/docs/en/UI/AspNetCore/Branding.md +++ b/docs/en/UI/AspNetCore/Branding.md @@ -20,13 +20,15 @@ namespace MyProject.Web public class MyProjectBrandingProvider : DefaultBrandingProvider { public override string AppName => "Book Store"; + + public override string LogoUrl => "logo.png"; } } ```` The result will be like shown below: -![branding-appname](../../images/branding-appname.png) +![bookstore-added-logo](../../images/bookstore-added-logo.png) `IBrandingProvider` has the following properties: @@ -38,8 +40,4 @@ The result will be like shown below: ## Overriding the Branding Area -The [Basic Theme](Basic-Theme.md) doesn't implement the logos. However, you can see the [UI Customization Guide](Customization-User-Interface.md) to learn how you can replace the branding area with a custom view component. - -An example screenshot with an image is used in the branding area: - -![bookstore-added-logo](../../images/bookstore-added-logo.png) \ No newline at end of file +You can see the [UI Customization Guide](Customization-User-Interface.md) to learn how you can replace the branding area with a custom view component. diff --git a/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md b/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md new file mode 100644 index 0000000000..db466bebdb --- /dev/null +++ b/docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md @@ -0,0 +1,163 @@ +# Page Toolbar Extensions for ASP.NET Core UI + +Page toolbar system allows you to add components to the toolbar of any page. The page toolbar is the area right to the header of a page. A button ("Import users from excel") was added to the user management page below: + +![page-toolbar-button](../../images/page-toolbar-button.png) + +You can add any type of view component item to the page toolbar or modify existing items. + +## How to Set Up + +In this example, we will add an "Import users from excel" button and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md). + +### Add a New Button to the User Management Page + +Write the following code inside the `ConfigureServices` of your web module class: + +````csharp +Configure(options => +{ + options.Configure(toolbar => + { + toolbar.AddButton( + LocalizableString.Create("ImportFromExcel"), + icon: "file-import", + id: "ImportUsersFromExcel", + type: AbpButtonType.Secondary + ); + }); +}); +```` + +`AddButton` is a shortcut to simply add a button component. Note that you need to add the `ImportFromExcel` to your localization dictionary (json file) to localize the text. + +When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `order` to set the order of the button component relative to the other components). + +### Create a JavaScript File + +Now, we can go to the client side to handle click event of the new button. First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project: + +![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png) + +Here, the content of this JavaScript file: + +````js +$(function () { + $('#ImportUsersFromExcel').click(function (e) { + e.preventDefault(); + alert('TODO: import users from excel'); + }); +}); +```` + +In the `click` event, you can do anything you need to do. + +### Add the File to the User Management Page + +Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](Bundling-Minification.md). + +Write the following code inside the `ConfigureServices` of your module class: + +````csharp +Configure(options => +{ + options.ScriptBundles.Configure( + typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName, + bundleConfiguration => + { + bundleConfiguration.AddFiles( + "/Pages/Identity/Users/my-user-extensions.js" + ); + }); +}); +```` + +This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules. + +## Advanced Use Cases + +While you typically want to add a button action to the page toolbar, it is possible to add any type of component. + +### Add View Component to a Page Toolbar + +First, create a new view component in your project: + +![page-toolbar-custom-component](../../images/page-toolbar-custom-component.png) + +For this example, we've created a `MyToolbarItem` view component under the `/Pages/Identity/Users/MyToolbarItem` folder. + +`MyToolbarItemViewComponent.cs` content: + +````csharp +public class MyToolbarItemViewComponent : AbpViewComponent +{ + public IViewComponentResult Invoke() + { + return View("~/Pages/Identity/Users/MyToolbarItem/Default.cshtml"); + } +} +```` + +`Default.cshtml` content: + +````xml + + + +```` + +* `.cshtml` file can contain any type of component(s). It is a typical view component. +* `MyToolbarItemViewComponent` can inject and use any service if you need. + +Then you can add the `MyToolbarItemViewComponent` to the user management page: + +````csharp +Configure(options => +{ + options.Configure( + toolbar => + { + toolbar.AddComponent(); + } + ); +}); +```` + +* If your component accepts arguments (in the `Invoke`/`InvokeAsync` method), you can pass them to the `AddComponent` method as an anonymous object. + +#### Permissions + +If your button/component should be available based on a [permission/policy](../../Authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods. + +### Add a Page Toolbar Contributor + +If you perform advanced custom logic while adding an item to a page toolbar, you can create a class that implements the `IPageToolbarContributor` interface or inherits from the `PageToolbarContributor` class: + +````csharp +public class MyToolbarContributor : PageToolbarContributor +{ + public override Task ContributeAsync(PageToolbarContributionContext context) + { + context.Items.Insert(0, new PageToolbarItem(typeof(MyToolbarItemViewComponent))); + + return Task.CompletedTask; + } +} +```` + +* You can use `context.ServiceProvider` to resolve dependencies if you need. + +Then add your class to the `Contributors` list: + +````csharp +Configure(options => +{ + options.Configure( + toolbar => + { + toolbar.Contributors.Add(new MyToolbarContributor()); + } + ); +}); +```` + diff --git a/docs/en/UI/Blazor/Navigation-Menu.md b/docs/en/UI/Blazor/Navigation-Menu.md index 331de7a4cb..7f09a65dbb 100644 --- a/docs/en/UI/Blazor/Navigation-Menu.md +++ b/docs/en/UI/Blazor/Navigation-Menu.md @@ -137,7 +137,7 @@ var myService = context.ServiceProvider.GetRequiredService(); ### The Administration Menu -There is a special menu item in the menu menu that is added by the ABP Framework: The *Administration* menu. It is typically used by the pre-built admin [application modules](../../Modules/Index.md): +There is a special menu item in the menu that is added by the ABP Framework: The *Administration* menu. It is typically used by the pre-built admin [application modules](../../Modules/Index.md): ![nav-main-menu-administration](../../images/nav-main-menu-administration.png) diff --git a/docs/en/UI/Blazor/Page-Progress.md b/docs/en/UI/Blazor/Page-Progress.md new file mode 100644 index 0000000000..b1d5b0df12 --- /dev/null +++ b/docs/en/UI/Blazor/Page-Progress.md @@ -0,0 +1,57 @@ +# Blazor UI: Page Progress + +Page Progress is used to show a progress bar indicator on top of the page and to show to the user that currently a long running process is in the work. + +By default you don't need to do anything to show the progress indicator, as all the work is done automatically by the ABP Framework internals. This means that all calls to the ABP backend (through your HTTP API) will activate page progress and show the loading indicator. + +This doesn't mean that you don't have the control over it. On the contrary, if you want to show progress for your own processes, it is really easy to do. All you have to do is to use inject and use the `IUiPageProgressService`. + +## Example + +First, inject the `IUiPageProgressService` into your page/component. + +```cs +@inject IUiPageProgressService pageProgressService +``` + +Next, invoke the `Go` method in `IUiPageProgressService`. It's that simple: + +```cs +Task OnClick() +{ + return pageProgressService.Go(null); +} +``` + +The previous example will show the progress with a default settings. If, for example you want to change the progress color you can override it by setting the options through the `Go` method. + +```cs +Task OnClick() +{ + return pageProgressService.Go(null, options => + { + options.Type = UiPageProgressType.Warning; + }); +} +``` + +## Breakdown + +The first parameter of the `Go` needs a little explanation. In the previous example we have set it to `null` which means, once called it will show an _indeterminate_ indicator and will cycle the loading animation indefinitely, until we hide the progress. You also have the option of defining the actual percentage of the progress and the code is the same, just instead of sending it the `null` you will send it a number between `0` and `100`. + +```cs +pageProgressService.Go(25) +``` + +### Valid values + +1. `null` - show _indeterminate_ indicator +2. `>= 0` and `<= 100` - show the regular _percentage_ progress + +### Hiding progress + +To hide the progress just set the actual values to something other then the _Valid value_. + +```cs +pageProgressService.Go(-1) +``` \ No newline at end of file diff --git a/docs/en/UI/Blazor/Theming.md b/docs/en/UI/Blazor/Theming.md index cbc6341dab..00bf911ff9 100644 --- a/docs/en/UI/Blazor/Theming.md +++ b/docs/en/UI/Blazor/Theming.md @@ -63,7 +63,7 @@ A theme is simply a Razor Class Library. ### The Easy Way -The easiest way to create a new theme is to copy the [Basic Theme Source Code](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme) and customize it. Once you get a copy of the theme in your solution, remove the `Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic` NuGet package and reference to the local project. +The easiest way to create a new theme is to copy the [Basic Theme Source Code](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme) and customize it. Once you get a copy of the theme in your solution, remove the `Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme` NuGet package and reference to the local project. ### Global Styles / Scripts diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index edfebc9544..250fed91f0 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -353,22 +353,8 @@ "path": "PlugIn-Modules.md" }, { - "text": "Customizing the Application Modules", - "path": "Customizing-Application-Modules-Guide.md", - "items": [ - { - "text": "Extending Entities", - "path": "Customizing-Application-Modules-Extending-Entities.md" - }, - { - "text": "Overriding Services", - "path": "Customizing-Application-Modules-Overriding-Services.md" - }, - { - "text": "Overriding the User Interface", - "path": "Customizing-Application-Modules-Overriding-User-Interface.md" - } - ] + "text": "Customizing/Extending Modules", + "path": "Customizing-Application-Modules-Guide.md" }, { "text": "Best Practices", @@ -703,7 +689,7 @@ "items": [ { "text": "SubmitButton", - "path": "UI/Blazor/SubmitButton.md" + "path": "UI/Blazor/Components/SubmitButton.md" } ] }, @@ -754,6 +740,10 @@ { "text": "PWA Configuration", "path": "UI/Angular/PWA-Configuration.md" + }, + { + "text": "Unit Testing", + "path": "UI/Angular/Testing.md" } ] }, diff --git a/docs/en/images/extension-navigation-property-form.png b/docs/en/images/extension-navigation-property-form.png new file mode 100644 index 0000000000..5e55ce3afd Binary files /dev/null and b/docs/en/images/extension-navigation-property-form.png differ diff --git a/docs/en/images/extension-navigation-property-table.png b/docs/en/images/extension-navigation-property-table.png new file mode 100644 index 0000000000..a3f333919c Binary files /dev/null and b/docs/en/images/extension-navigation-property-table.png differ diff --git a/docs/en/images/page-toolbar-button.png b/docs/en/images/page-toolbar-button.png new file mode 100644 index 0000000000..05e3f9da11 Binary files /dev/null and b/docs/en/images/page-toolbar-button.png differ diff --git a/docs/en/images/page-toolbar-custom-component.png b/docs/en/images/page-toolbar-custom-component.png new file mode 100644 index 0000000000..a15ef4854c Binary files /dev/null and b/docs/en/images/page-toolbar-custom-component.png differ diff --git a/docs/zh-Hans/Blob-Storing-Aliyun.md b/docs/zh-Hans/Blob-Storing-Aliyun.md index ded28fbc93..c8a1e68bc7 100644 --- a/docs/zh-Hans/Blob-Storing-Aliyun.md +++ b/docs/zh-Hans/Blob-Storing-Aliyun.md @@ -55,7 +55,7 @@ Configure(options => * **RoleSessionName** ([NotNull]string): 用来标识临时访问凭证的名称,建议使用不同的应用程序用户来区分. * **Policy** (string): 在扮演角色的时候额外添加的权限限制. 请参见[基于RAM Policy的权限控制](https://help.aliyun.com/document_detail/100680.html). * **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为3600. -* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): +* **ContainerName** (string): 你可以在aliyun中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aliyun有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://help.aliyun.com/knowledge_detail/39668.html): * 只能包含小写字母,数字和短横线(-) * 必须以小写字母和数字开头和结尾 * Bucket名称的长度限制在**3**到**63**个字符之间 diff --git a/docs/zh-Hans/Blob-Storing-Aws.md b/docs/zh-Hans/Blob-Storing-Aws.md index 0ecb59bbf5..7bfb998297 100644 --- a/docs/zh-Hans/Blob-Storing-Aws.md +++ b/docs/zh-Hans/Blob-Storing-Aws.md @@ -59,7 +59,7 @@ Configure(options => * **Region** (string): 服务的地区名称. * **Policy** (string): JSON格式的IAM策略. * **DurationSeconds** (int): 设置临时访问凭证的有效期,单位是s,最小为900,最大为129600. -* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): +* **ContainerName** (string): 你可以在Aws中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Aws有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): * Bucket名称必须介于 3 到 63 个字符之间. * Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. * Bucket名称必须以字母或数字开头和结尾. diff --git a/docs/zh-Hans/Blob-Storing-Azure.md b/docs/zh-Hans/Blob-Storing-Azure.md index b6f54cd72c..8eecc1fde3 100644 --- a/docs/zh-Hans/Blob-Storing-Azure.md +++ b/docs/zh-Hans/Blob-Storing-Azure.md @@ -40,7 +40,7 @@ Configure(options => ### 选项 * **ConnectionString** (string): 连接字符串包括应用程序在运行时使用共享密钥授权访问Azure存储帐户中的数据所需的授权信息. 请参考[Azure文档](https://docs.microsoft.com/en-us/azure/storage/common/storage-configure-connection-string). -* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlogContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): +* **ContainerName** (string): 你可以在azure中指定容器名称. 如果没有指定它将使用 `BlobContainerName` 属性定义的BLOB容器的名称(请参阅[BLOB存储文档](Blob-Storing.md)). 请注意Azure有一些**命名容器的规则**,容器名称必须是有效的DNS名称,[符合以下命名规则](https://docs.microsoft.com/en-us/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata#container-names): * 容器名称必须以字母或数字开头或结尾,并且只能包含字母,数字和破折号(-)字符. * 每个破折号(-)必须紧跟在字母或数字之后;容器名称中不允许使用连续的破折号. * 容器名称中的所有字母都必须**小写**. diff --git a/docs/zh-Hans/Blob-Storing-Minio.md b/docs/zh-Hans/Blob-Storing-Minio.md index 3c4bec721d..fb98281f02 100644 --- a/docs/zh-Hans/Blob-Storing-Minio.md +++ b/docs/zh-Hans/Blob-Storing-Minio.md @@ -43,7 +43,7 @@ Configure(options => * **EndPoint** (string): 你的Minio对象存储服务的URL, 查看文档:https://docs.min.io/docs/dotnet-client-quickstart-guide.html * **AccessKey** (string): Access key是唯一标识你的账户的用户ID, * **SecretKey** (string): Access key是唯一标识你的账户的用户ID -* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlogContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): +* **BucketName** (string):你可以指定bucket名称,如果没有指定,将使用 `BlobContainerName` 属性定义的BLOB容器的名称(查阅[BLOB storing document](Blob-Storing.md)),MinIO完全兼容S3标准,所以有一些 **bucket命名规则**,必须符合[规则](https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html): * Bucket名称必须介于 3 到 63 个字符之间. * Bucket名称只能由小写字母、数字、句点 (.) 和连字符 (-) 组成. * Bucket名称必须以字母或数字开头和结尾. diff --git a/docs/zh-Hans/Blob-Storing.md b/docs/zh-Hans/Blob-Storing.md index c233f3359e..9321c51cb9 100644 --- a/docs/zh-Hans/Blob-Storing.md +++ b/docs/zh-Hans/Blob-Storing.md @@ -10,7 +10,7 @@ ABP框架为BLOB提供了抽象,并提供了一些可以轻松集成到的预构 * 你可以**轻松的更改**BLOB存储,而不用改变你的应用程序代码. * 如果你想创建**可重用的应用程序模块**,无需假设BLOB的存储方式. -ABP BLOG存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md). +ABP BLOB存储系统兼容ABP框架其他功能,如[多租户](Multi-Tenancy.md). ## BLOB 存储提供程序 @@ -18,9 +18,9 @@ ABP框架已经有以下存储提供程序的实现; * [File System](Blob-Storing-File-System.md):将BLOB作为标准文件存储在本地文件系统的文件夹中. * [Database](Blob-Storing-Database.md): 将BLOB存储在数据库中. -* [Azure](Blob-Storing-Azure.md): 将BLOG存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中. +* [Azure](Blob-Storing-Azure.md): 将BLOB存储在 [Azure BLOB storage](https://azure.microsoft.com/en-us/services/storage/blobs/)中. * [Aliyun](Blob-Storing-Aliyun.md): 将BLOB存储在[Aliyun Storage Service](https://help.aliyun.com/product/31815.html)中. -* [Ninio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中. +* [Minio](Blob-Storing-Minio.md): 将BLOB存储在[MinIO Object storage](https://min.io/)中. * [Aws](Blob-Storing-Aws.md): 将BLOB存储在[Amazon Simple Storage Service](https://aws.amazon.com/s3/)中. 以后会实现更多的提供程序,你可以为自己喜欢的提供程序创建[请求](https://github.com/abpframework/abp/issues/new),或者你也可以[自己实现](Blob-Storing-Custom-Provider.md)它并[贡献](Contribution/Index.md)到ABP框架. @@ -95,9 +95,9 @@ namespace AbpDemo ### 读取/获取 BLOB * `GetAsync`: 返回给定BLOB名称可用于读取BLOB内容的 `Stream` 对象. 使用后始终要**dispose流**. 如果找不到具有给定名称的BLOB,则抛出异常. -* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOG,则返回 `null`. +* `GetOrNullAsync`: 与 `GetAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`. * `GetAllBytesAsync`: 返回 `byte[]` 而不是 `Stream`. 如果找不到具有给定名称的BLOB,则抛出异常. -* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOG,则返回 `null`. +* `GetAllBytesOrNullAsync`: 与 `GetAllBytesAsync` 方法相反,如果未找到给定名称的BLOB,则返回 `null`. ### 删除 BLOB @@ -300,7 +300,7 @@ Configure(options => ## BLOB 存储 vs 文件管理系统 -注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOG的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的. +注意BLOB存储不是一个文件管理系统. 它是一个用于保存,获取和删除命名BLOB的低级别系统. 它不提供目录那样的层次结构,这是典型文件系统所期望的. 如果你想创建文件夹并在文件夹之间移动文件,为文件分配权限并在用户之间共享文件,那么你需要在BLOB存储系统上实现你自己的应用程序. diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md index 4c63dcd818..4a02b1c3c8 100644 --- a/docs/zh-Hans/CLI.md +++ b/docs/zh-Hans/CLI.md @@ -103,7 +103,7 @@ abp new Acme.BookStore * `--preview`: 使用最新的预览版本. * `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D:\local-templat` 或 `https://.../my-template-file.zip`). * `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中. -* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后). +* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后). * `--local-framework-ref --abp-path`: 使用对项目的本地引用,而不是替换为NuGet包引用. ### update diff --git a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md index 8a6ec1de9b..db434ab365 100644 --- a/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md +++ b/docs/zh-Hans/Domain-Driven-Design-Implementation-Guide.md @@ -1920,7 +1920,7 @@ public class OrganizationAppService : ApplicationService 你可能想知道为什么付款逻辑代码不在`OrganizationManager`中.付款是非常**重要的事情**,我们不能**遗漏任何一次付款**. -它确实非常重要,但是,它不能放到领域服务中.我们可能还又**其它用例**来创建组织但不收取任何费用.例如: +它确实非常重要,但是,它不能放到领域服务中.我们可能还有**其它用例**来创建组织但不收取任何费用.例如: * 管理员可以在后台管理系统创建新组织,而无需支付任何费用. * 后台作业系统导入,集成,同步组织而无需支付费用. diff --git a/docs/zh-Hans/Entity-Framework-Core-Migrations.md b/docs/zh-Hans/Entity-Framework-Core-Migrations.md index 266c0f46b0..8708516f76 100644 --- a/docs/zh-Hans/Entity-Framework-Core-Migrations.md +++ b/docs/zh-Hans/Entity-Framework-Core-Migrations.md @@ -588,7 +588,7 @@ public class IdentityRoleExtendingService : ITransientDependency ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True" } ```` @@ -596,10 +596,10 @@ public class IdentityRoleExtendingService : ITransientDependency ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true", - "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=localhost;Database=BookStore;Trusted_Connection=True", + "AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True", + "AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True" } ```` diff --git a/docs/zh-Hans/Modules/Docs.md b/docs/zh-Hans/Modules/Docs.md index acc956be1b..1d09552e50 100644 --- a/docs/zh-Hans/Modules/Docs.md +++ b/docs/zh-Hans/Modules/Docs.md @@ -47,7 +47,7 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块. ```json { "ConnectionStrings": { - "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true" + "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True" } } ``` diff --git a/docs/zh-Hans/Samples/Microservice-Demo.md b/docs/zh-Hans/Samples/Microservice-Demo.md index 59fa98e057..ca8b90a03f 100644 --- a/docs/zh-Hans/Samples/Microservice-Demo.md +++ b/docs/zh-Hans/Samples/Microservice-Demo.md @@ -843,7 +843,7 @@ Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://lo ````json "ConnectionStrings": { - "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true", + "Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True", "Blogging": "mongodb://localhost/MsDemo_Blogging" } ```` @@ -969,8 +969,8 @@ public class ProductServiceMigrationDbContext : AbpDbContext Task.CompletedTask); + + options.Events.OnAuthorizationCodeReceived = receivedContext => { - OnAuthorizationCodeReceived = receivedContext => - { - var tenantKey = receivedContext.HttpContext.RequestServices - .GetRequiredService>().Value.TenantKey; - - if (receivedContext.HttpContext.Request != null && - receivedContext.Request.Cookies.ContainsKey(tenantKey)) - { - receivedContext.TokenEndpointRequest.SetParameter(tenantKey, - receivedContext.Request.Cookies[tenantKey]); - } - - return Task.CompletedTask; - } + SetAbpTenantId(receivedContext); + return authorizationCodeReceived.Invoke(receivedContext); }; - - configureOptions?.Invoke(options); }); } + + private static void SetAbpTenantId(AuthorizationCodeReceivedContext receivedContext) + { + var tenantKey = receivedContext.HttpContext.RequestServices + .GetRequiredService>().Value.TenantKey; + + if (receivedContext.Request.Cookies.ContainsKey(tenantKey)) + { + receivedContext.TokenEndpointRequest.SetParameter(tenantKey, + receivedContext.Request.Cookies[tenantKey]); + } + } } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor index ac629423ce..5e0537b2b2 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/Branding.razor @@ -1,3 +1,9 @@ -@using Volo.Abp.Ui.Branding +@using Volo.Abp.Ui.Branding @inject IBrandingProvider BrandingProvider -@BrandingProvider.AppName + + @if (!BrandingProvider.LogoUrl.IsNullOrWhiteSpace()) + { + @BrandingProvider.AppName + } + @BrandingProvider.AppName + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor index cf6703ade9..b2fab0807d 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor @@ -10,7 +10,7 @@ if (MenuItem.Url != null) {